1use std::collections::VecDeque;
2use std::error::Error;
3use std::fmt::{self, Display, Formatter};
4use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
5use std::sync::{Arc, Mutex};
6use std::time::{Duration, SystemTime, UNIX_EPOCH};
7
8use lodestone_proto::{MAX_PUBLIC_SESSION_BYTES, PUBLIC_SESSION_FORMAT_VERSION};
9use lodestone_wire::{
10 Action, Command, CoreBinding, DiscoverRequest, DiscoverResult, ErrorBody, ErrorDisposition, GetBindingRequest,
11 GetBindingResult, Identity, LODESTONE_API_ALPN, LODESTONE_API_COMMAND_CONTRACTS, LODESTONE_API_COMMAND_OUTCOMES,
12 LODESTONE_API_COMMAND_SEMANTICS, LODESTONE_API_FORMAT_VERSION, LODESTONE_API_RESPONSE_META_FORMAT_VERSION,
13 LODESTONE_API_SCHEMA_ID, LODESTONE_API_SPEC_SHA256, LODESTONE_API_STORAGE_PROFILE_ATTESTATION_FORMAT_VERSION,
14 LODESTONE_API_STREAM_CONTRACTS, RawJson, RecoveryAction, RequestBody, RequestEnvelope, RequestMeta,
15 ResponseEnvelope, ResponseMeta, ResultBody, StorageProfileAttestation, StorageProfileRole, U64, WorkloadClass,
16 command_for_result, validate_error_body,
17};
18use rustls::pki_types::{CertificateDer, ServerName};
19use tokio::io::{AsyncReadExt, AsyncWriteExt};
20use tokio::net::TcpStream;
21use tokio::sync::{Mutex as AsyncMutex, Notify, Semaphore};
22use tokio_rustls::TlsConnector;
23use tokio_rustls::client::TlsStream;
24
25use crate::config::{
26 CellTransportConfig, CellTransportIdentity, ClientConfigError, CollectionSessionConfig, PoolConfig, parse_identity,
27 parse_u64,
28};
29use crate::credential::{CredentialMaterial, CredentialProviderError};
30use crate::generated::{
31 GeneratedCollectionIndexRef, MAX_NATIVE_REQUEST_BYTES, MAX_NATIVE_RESPONSE_BYTES, request_collection_index_ref,
32 request_command,
33};
34
35const MAX_REQUEST_ID_BYTES: usize = 128;
36const MAX_CALL_TIMEOUT: Duration = Duration::from_secs(10 * 60);
37const DEFAULT_CALL_TIMEOUT: Duration = Duration::from_secs(30);
38pub const MAX_VERIFIED_SESSION_METADATA: usize = 4_096;
39
40#[derive(Clone)]
41pub struct CallCancellation {
42 inner: Arc<CallCancellationInner>,
43}
44
45struct CallCancellationInner {
46 cancelled: AtomicBool,
47 notify: Notify,
48}
49
50impl CallCancellation {
51 #[must_use]
52 pub fn new() -> Self {
53 Self {
54 inner: Arc::new(CallCancellationInner {
55 cancelled: AtomicBool::new(false),
56 notify: Notify::new(),
57 }),
58 }
59 }
60
61 pub fn cancel(&self) {
62 if !self.inner.cancelled.swap(true, Ordering::AcqRel) {
63 self.inner.notify.notify_waiters();
64 }
65 }
66
67 #[must_use]
68 pub fn is_cancelled(&self) -> bool {
69 self.inner.cancelled.load(Ordering::Acquire)
70 }
71
72 pub async fn cancelled(&self) {
74 loop {
75 let notified = self.inner.notify.notified();
76 if self.is_cancelled() {
77 return;
78 }
79 notified.await;
80 }
81 }
82}
83
84impl Default for CallCancellation {
85 fn default() -> Self {
86 Self::new()
87 }
88}
89
90#[derive(Clone)]
91pub struct CallOptions {
92 deadline: CallDeadline,
93 cancellation: CallCancellation,
94 index_name: Option<String>,
95 workload_class: Option<WorkloadClass>,
96}
97
98#[derive(Clone, Copy)]
99struct CallDeadline {
100 absolute_unix_millis: Option<u64>,
101 local: tokio::time::Instant,
102}
103
104impl CallOptions {
105 pub fn new(timeout: Duration) -> Result<Self, ClientError> {
110 Ok(Self {
111 deadline: CallDeadline::after(timeout)?,
112 cancellation: CallCancellation::new(),
113 index_name: None,
114 workload_class: None,
115 })
116 }
117
118 pub fn from_absolute_deadline_unix_millis(absolute_unix_millis: u64) -> Result<Self, ClientError> {
122 Ok(Self {
123 deadline: CallDeadline::from_absolute_unix_millis(absolute_unix_millis)?,
124 cancellation: CallCancellation::new(),
125 index_name: None,
126 workload_class: None,
127 })
128 }
129
130 #[must_use]
131 pub fn with_cancellation(mut self, cancellation: CallCancellation) -> Self {
132 self.cancellation = cancellation;
133 self
134 }
135
136 pub fn with_index_name(mut self, index_name: String) -> Result<Self, ClientError> {
137 validate_index_name(&index_name)?;
138 self.index_name = Some(index_name);
139 Ok(self)
140 }
141
142 #[must_use]
145 pub fn with_workload_class(mut self, workload_class: WorkloadClass) -> Self {
146 self.workload_class = Some(workload_class);
147 self
148 }
149
150 #[must_use]
151 pub const fn cancellation(&self) -> &CallCancellation {
152 &self.cancellation
153 }
154
155 pub fn absolute_deadline_unix_millis(&self) -> Result<u64, ClientError> {
158 self.deadline.absolute_unix_millis.ok_or(ClientError::ClockUnavailable)
159 }
160
161 pub fn remaining(&self) -> Result<Duration, ClientError> {
164 self.local_deadline()?
165 .checked_duration_since(tokio::time::Instant::now())
166 .ok_or(ClientError::Transport {
167 kind: TransportErrorKind::Deadline,
168 disposition: SubmissionDisposition::NotSubmitted,
169 })
170 }
171
172 pub(crate) fn local_deadline(&self) -> Result<tokio::time::Instant, ClientError> {
173 self.absolute_deadline_unix_millis().map(|_| self.deadline.local)
174 }
175
176 pub(crate) fn for_resolution(&self) -> Self {
177 Self {
178 deadline: self.deadline,
179 cancellation: CallCancellation::new(),
180 index_name: self.index_name.clone(),
181 workload_class: self.workload_class,
182 }
183 }
184}
185
186impl CallDeadline {
187 fn after(timeout: Duration) -> Result<Self, ClientError> {
188 let timeout = normalized_timeout(timeout)?;
189 let local_now = tokio::time::Instant::now();
190 let wall_deadline = SystemTime::now()
191 .checked_add(timeout)
192 .ok_or(ClientError::ClockUnavailable)?;
193 let since_epoch = wall_deadline
194 .duration_since(UNIX_EPOCH)
195 .map_err(|_| ClientError::ClockUnavailable)?;
196 Ok(Self {
197 absolute_unix_millis: Some(duration_millis_ceil(since_epoch)?),
198 local: local_now + timeout,
199 })
200 }
201
202 fn from_absolute_unix_millis(absolute_unix_millis: u64) -> Result<Self, ClientError> {
203 let local_now = tokio::time::Instant::now();
204 let wall_deadline = UNIX_EPOCH
205 .checked_add(Duration::from_millis(absolute_unix_millis))
206 .ok_or(ClientError::InvalidCallOptions)?;
207 let remaining = wall_deadline
208 .duration_since(SystemTime::now())
209 .map_err(|_| ClientError::InvalidCallOptions)?;
210 if remaining.is_zero() || remaining > MAX_CALL_TIMEOUT {
211 return Err(ClientError::InvalidCallOptions);
212 }
213 Ok(Self {
214 absolute_unix_millis: Some(absolute_unix_millis),
215 local: local_now + remaining,
216 })
217 }
218}
219
220impl Default for CallOptions {
221 fn default() -> Self {
222 match Self::new(DEFAULT_CALL_TIMEOUT) {
223 Ok(options) => options,
224 Err(_) => Self {
225 deadline: CallDeadline {
226 absolute_unix_millis: None,
227 local: tokio::time::Instant::now(),
228 },
229 cancellation: CallCancellation::new(),
230 index_name: None,
231 workload_class: None,
232 },
233 }
234 }
235}
236
237#[derive(Clone, Debug, PartialEq)]
238pub struct ProfileVerification {
239 pub artifact_version: String,
240 pub principal_id: Identity,
241 pub collection_id: Identity,
242 pub policy_revision: U64,
243 pub cluster_id: Identity,
244 pub configuration_generation: U64,
245 pub binding_revision: U64,
246 pub schema_id: String,
247 pub schema_digest: lodestone_wire::Digest,
248 pub native_endpoints: Vec<String>,
249 pub selected_stream_endpoint: String,
250 pub storage_profile: StorageProfileAttestation,
251}
252
253#[derive(Clone, Copy, Debug, Eq, PartialEq)]
254pub struct CellTransportMetricsSnapshot {
255 pub transport_count: usize,
256 pub live_connections: usize,
257 pub idle_connections: usize,
258 pub live_collection_sessions: usize,
259 pub session_cache_entries: usize,
260 pub session_cache_hits: u64,
261 pub session_cache_misses: u64,
262 pub session_cache_evictions: u64,
263 pub credential_fetches: u64,
264 pub bulkhead_saturation: u64,
265 pub isolation_failures: u64,
266}
267
268#[derive(Default)]
269struct CellTransportMetrics {
270 live_connections: AtomicUsize,
271 idle_connections: AtomicUsize,
272 live_collection_sessions: AtomicUsize,
273 session_cache_entries: AtomicUsize,
274 session_cache_hits: AtomicU64,
275 session_cache_misses: AtomicU64,
276 session_cache_evictions: AtomicU64,
277 credential_fetches: AtomicU64,
278 bulkhead_saturation: AtomicU64,
279 isolation_failures: AtomicU64,
280}
281
282impl CellTransportMetrics {
283 fn snapshot(&self) -> CellTransportMetricsSnapshot {
284 CellTransportMetricsSnapshot {
285 transport_count: 1,
286 live_connections: self.live_connections.load(Ordering::Relaxed),
287 idle_connections: self.idle_connections.load(Ordering::Relaxed),
288 live_collection_sessions: self.live_collection_sessions.load(Ordering::Relaxed),
289 session_cache_entries: self.session_cache_entries.load(Ordering::Relaxed),
290 session_cache_hits: self.session_cache_hits.load(Ordering::Relaxed),
291 session_cache_misses: self.session_cache_misses.load(Ordering::Relaxed),
292 session_cache_evictions: self.session_cache_evictions.load(Ordering::Relaxed),
293 credential_fetches: self.credential_fetches.load(Ordering::Relaxed),
294 bulkhead_saturation: self.bulkhead_saturation.load(Ordering::Relaxed),
295 isolation_failures: self.isolation_failures.load(Ordering::Relaxed),
296 }
297 }
298}
299
300#[derive(Clone, Debug, Eq, PartialEq)]
301struct VerifiedSessionCacheKey {
302 collection_id: String,
303 principal_id: String,
304 profile_generation: String,
305 credential_id: String,
306 credential_generation: String,
307 secret_reference: String,
308}
309
310impl VerifiedSessionCacheKey {
311 fn from_config(config: &CollectionSessionConfig) -> Self {
312 let profile = config.profile.as_wire();
313 Self {
314 collection_id: profile.expected_collection_id.value.clone(),
315 principal_id: profile.expected_principal_id.value.clone(),
316 profile_generation: profile.profile_generation.value.clone(),
317 credential_id: profile.credential_id.value.clone(),
318 credential_generation: profile.credential_generation.value.clone(),
319 secret_reference: profile.secret_reference.clone(),
320 }
321 }
322}
323
324struct VerifiedSessionMetadata {
325 key: VerifiedSessionCacheKey,
326 verification: ProfileVerification,
327}
328
329#[derive(Default)]
330struct VerifiedSessionCache {
331 entries: VecDeque<VerifiedSessionMetadata>,
332}
333
334impl VerifiedSessionCache {
335 fn get(&mut self, key: &VerifiedSessionCacheKey, metrics: &CellTransportMetrics) -> Option<ProfileVerification> {
336 let Some(position) = self.entries.iter().position(|entry| &entry.key == key) else {
337 metrics.session_cache_misses.fetch_add(1, Ordering::Relaxed);
338 return None;
339 };
340 let entry = self.entries.remove(position)?;
341 let verification = entry.verification.clone();
342 self.entries.push_back(entry);
343 metrics.session_cache_hits.fetch_add(1, Ordering::Relaxed);
344 Some(verification)
345 }
346
347 fn insert(
348 &mut self,
349 key: VerifiedSessionCacheKey,
350 verification: ProfileVerification,
351 metrics: &CellTransportMetrics,
352 ) {
353 if let Some(position) = self.entries.iter().position(|entry| entry.key == key) {
354 let _ = self.entries.remove(position);
355 } else if self.entries.len() == MAX_VERIFIED_SESSION_METADATA {
356 let _ = self.entries.pop_front();
357 metrics.session_cache_evictions.fetch_add(1, Ordering::Relaxed);
358 }
359 self.entries.push_back(VerifiedSessionMetadata { key, verification });
360 metrics
361 .session_cache_entries
362 .store(self.entries.len(), Ordering::Relaxed);
363 }
364}
365
366#[derive(Clone)]
367pub struct CellTransport {
368 inner: Arc<CellTransportInner>,
369}
370
371struct CellTransportInner {
372 config: CellTransportConfig,
373 tls: Arc<rustls::ClientConfig>,
374 pool: AsyncMutex<Vec<NativeConnection>>,
375 connection_slots: Arc<Semaphore>,
376 next_endpoint: AtomicUsize,
377 verified_sessions: Mutex<VerifiedSessionCache>,
378 metrics: Arc<CellTransportMetrics>,
379}
380
381#[derive(Clone)]
382pub struct Client {
383 inner: Arc<ClientInner>,
384}
385
386pub type CollectionSession = Client;
387
388struct ClientInner {
389 transport: CellTransport,
390 config: CollectionSessionConfig,
391 verified: Mutex<Option<ProfileVerification>>,
392}
393
394impl Drop for ClientInner {
395 fn drop(&mut self) {
396 self.transport
397 .inner
398 .metrics
399 .live_collection_sessions
400 .fetch_sub(1, Ordering::Relaxed);
401 }
402}
403
404struct NativeConnection {
405 stream: TlsStream<TcpStream>,
406 metrics: Arc<CellTransportMetrics>,
407 idle_since: tokio::time::Instant,
408}
409
410impl Drop for NativeConnection {
411 fn drop(&mut self) {
412 self.metrics.live_connections.fetch_sub(1, Ordering::Relaxed);
413 }
414}
415
416impl CellTransport {
417 pub fn new(config: CellTransportConfig) -> Result<Self, ClientError> {
418 let tls = build_tls_config(&config)?;
419 let metrics = Arc::new(CellTransportMetrics::default());
420 Ok(Self {
421 inner: Arc::new(CellTransportInner {
422 connection_slots: Arc::new(Semaphore::new(config.pool.maximum_connections)),
423 config,
424 tls: Arc::new(tls),
425 pool: AsyncMutex::new(Vec::new()),
426 next_endpoint: AtomicUsize::new(0),
427 verified_sessions: Mutex::new(VerifiedSessionCache::default()),
428 metrics,
429 }),
430 })
431 }
432
433 #[must_use]
434 pub fn identity(&self) -> &CellTransportIdentity {
435 self.inner.config.identity()
436 }
437
438 #[must_use]
439 pub fn pool_config(&self) -> PoolConfig {
440 self.inner.config.pool
441 }
442
443 #[must_use]
444 pub fn metrics(&self) -> CellTransportMetricsSnapshot {
445 self.inner.metrics.snapshot()
446 }
447}
448
449impl Client {
450 pub async fn connect(
453 transport: CellTransport,
454 config: CollectionSessionConfig,
455 options: CallOptions,
456 ) -> Result<Self, ClientError> {
457 let client = Self::new(transport, config)?;
458 client.verify_connection_profile(options).await?;
459 Ok(client)
460 }
461
462 pub fn new(transport: CellTransport, config: CollectionSessionConfig) -> Result<Self, ClientError> {
463 if let Err(error) = config.validate_transport(&transport.inner.config) {
464 transport
465 .inner
466 .metrics
467 .isolation_failures
468 .fetch_add(1, Ordering::Relaxed);
469 return Err(error.into());
470 }
471 let cache_key = VerifiedSessionCacheKey::from_config(&config);
472 let cached = transport
473 .inner
474 .verified_sessions
475 .lock()
476 .map_err(|_| ClientError::InternalStateUnavailable)?
477 .get(&cache_key, &transport.inner.metrics);
478 if let Some(verification) = &cached
479 && validate_cached_verification(&config, verification).is_err()
480 {
481 transport
482 .inner
483 .metrics
484 .isolation_failures
485 .fetch_add(1, Ordering::Relaxed);
486 return Err(ClientError::ProfileIdentityMismatch);
487 }
488 transport
489 .inner
490 .metrics
491 .live_collection_sessions
492 .fetch_add(1, Ordering::Relaxed);
493 Ok(Self {
494 inner: Arc::new(ClientInner {
495 transport,
496 config,
497 verified: Mutex::new(None),
501 }),
502 })
503 }
504
505 #[must_use]
506 pub fn connection_profile(&self) -> &crate::ValidatedConnectionProfile {
507 &self.inner.config.profile
508 }
509
510 #[must_use]
513 pub fn cell_transport(&self) -> CellTransport {
514 self.inner.transport.clone()
515 }
516
517 #[must_use]
518 pub fn profile_verification(&self) -> Option<ProfileVerification> {
519 self.inner.verified.lock().ok().and_then(|verified| verified.clone())
520 }
521
522 pub async fn verify_connection_profile(&self, options: CallOptions) -> Result<ProfileVerification, ClientError> {
523 let discovered = self.discover(DiscoverRequest {}, options.clone()).await?;
524 self.validate_discovery_observation(&discovered)?;
525 let binding = self.get_binding(GetBindingRequest {}, options).await?;
526 self.verify_authority_binding_observation(&discovered, &binding)
527 }
528
529 pub fn validate_discovery_observation(&self, discovered: &DiscoverResult) -> Result<(), ClientError> {
532 validate_discovery(&self.inner.config, discovered)
533 }
534
535 pub fn verify_authority_binding_observation(
538 &self,
539 discovered: &DiscoverResult,
540 binding: &GetBindingResult,
541 ) -> Result<ProfileVerification, ClientError> {
542 let verified = self.validate_authority_binding_observation(discovered, binding)?;
543 self.install_profile_verification(verified)
544 }
545
546 pub fn validate_authority_binding_observation(
550 &self,
551 discovered: &DiscoverResult,
552 binding: &GetBindingResult,
553 ) -> Result<ProfileVerification, ClientError> {
554 validate_discovery(&self.inner.config, discovered)?;
555 validate_binding(&self.inner.config, &binding.binding, binding.derived, &binding.revision)?;
556 Ok(ProfileVerification {
557 artifact_version: discovered.artifact_version.clone(),
558 principal_id: discovered.principal_id.clone(),
559 collection_id: discovered.collection_id.clone(),
560 policy_revision: discovered.policy_revision.clone(),
561 cluster_id: binding.binding.cluster_id.clone(),
562 configuration_generation: binding.binding.configuration_generation.clone(),
563 binding_revision: binding.revision.clone(),
564 schema_id: binding.binding.schema_id.clone(),
565 schema_digest: binding.binding.schema_digest.clone(),
566 native_endpoints: binding.binding.native_endpoints.clone(),
567 selected_stream_endpoint: binding.binding.selected_stream_endpoint.clone(),
568 storage_profile: binding.binding.storage_profile.clone(),
569 })
570 }
571
572 fn install_profile_verification(&self, verified: ProfileVerification) -> Result<ProfileVerification, ClientError> {
573 let mut slot = self
574 .inner
575 .verified
576 .lock()
577 .map_err(|_| ClientError::InternalStateUnavailable)?;
578 *slot = Some(verified.clone());
579 self.inner
580 .transport
581 .inner
582 .verified_sessions
583 .lock()
584 .map_err(|_| ClientError::InternalStateUnavailable)?
585 .insert(
586 VerifiedSessionCacheKey::from_config(&self.inner.config),
587 verified.clone(),
588 &self.inner.transport.inner.metrics,
589 );
590 Ok(verified)
591 }
592
593 pub async fn call(&self, request: RequestBody, options: CallOptions) -> Result<ResultBody, ClientError> {
600 self.call_internal(request, options, None).await
601 }
602
603 pub async fn call_with_diagnostics(
611 &self,
612 request: RequestBody,
613 options: CallOptions,
614 ) -> Result<ClientCallDiagnosticResult, ClientCallDiagnosticFailure> {
615 let response_meta = Arc::new(Mutex::new(None));
616 let result = self
617 .call_internal(request, options, Some(Arc::clone(&response_meta)))
618 .await;
619 let observed = response_meta.lock().ok().and_then(|value| value.clone());
620 match result {
621 Ok(result) => Ok(ClientCallDiagnosticResult {
622 result,
623 response_meta: observed,
624 }),
625 Err(error) => Err(ClientCallDiagnosticFailure {
626 error: Box::new(error),
627 response_meta: observed,
628 }),
629 }
630 }
631
632 async fn call_internal(
633 &self,
634 request: RequestBody,
635 options: CallOptions,
636 diagnostic_meta: Option<Arc<Mutex<Option<ResponseMeta>>>>,
637 ) -> Result<ResultBody, ClientError> {
638 let local_deadline = options.local_deadline()?;
639 if tokio::time::Instant::now() >= local_deadline {
640 return Err(ClientError::Transport {
641 kind: TransportErrorKind::Deadline,
642 disposition: SubmissionDisposition::NotSubmitted,
643 });
644 }
645 let command = request_command(&request);
646 let command_contract = LODESTONE_API_COMMAND_CONTRACTS
647 .iter()
648 .find(|contract| contract.command == command)
649 .ok_or(ClientError::MissingCommandContract)?;
650 let outcome_contract = LODESTONE_API_COMMAND_OUTCOMES
651 .iter()
652 .find(|contract| contract.command == command)
653 .ok_or(ClientError::MissingCommandContract)?;
654 let mutation = outcome_contract.possible_submission_states.contains(&"submitted");
655 if mutation && self.profile_verification().is_none() {
656 return Err(ClientError::ProfileNotVerified);
657 }
658 let index_name = resolve_index_name(&request, options.index_name.as_deref())?;
659 let correlation = random_identity()?;
660 let deadline = options.absolute_deadline_unix_millis()?;
661 let request_id = format!("r-{}", lower_hex(&correlation));
662 let workload_class = options
663 .workload_class
664 .unwrap_or_else(|| default_workload_class(command_contract.action));
665 let envelope = RequestEnvelope {
666 meta: RequestMeta {
667 format_version: LODESTONE_API_FORMAT_VERSION,
668 request_id: request_id.clone(),
669 correlation_id: Identity {
670 value: lower_hex(&correlation),
671 },
672 absolute_deadline_unix_millis: U64 {
673 value: deadline.to_string(),
674 },
675 workload_class,
676 },
677 request,
678 };
679 let payload = canonical_request(&envelope)?;
680 let started = Arc::new(AtomicBool::new(false));
681 let maximum_attempts = usize::from(self.pool_config().maximum_read_retries).saturating_add(1);
682 let mut attempt = 0_usize;
683 loop {
684 attempt = attempt.saturating_add(1);
685 let result = tokio::select! {
686 result = tokio::time::timeout_at(
687 local_deadline,
688 self.transport_attempt(
689 &payload,
690 &request_id,
691 command,
692 command_contract.action,
693 workload_class,
694 index_name.as_deref(),
695 correlation,
696 Arc::clone(&started),
697 diagnostic_meta.as_ref(),
698 ),
699 ) => match result {
700 Ok(result) => result,
701 Err(_) => Err(ClientError::Transport {
702 kind: TransportErrorKind::Deadline,
703 disposition: started_disposition(&started),
704 }),
705 },
706 () = options.cancellation.cancelled() => Err(ClientError::Transport {
707 kind: TransportErrorKind::Cancelled,
708 disposition: started_disposition(&started),
709 }),
710 };
711 match result {
712 Err(ref error)
713 if attempt < maximum_attempts
714 && !options.cancellation.is_cancelled()
715 && error.automatic_retry_admitted() =>
716 {
717 continue;
718 }
719 result => return result,
720 }
721 }
722 }
723
724 #[allow(clippy::too_many_arguments)]
725 async fn transport_attempt(
726 &self,
727 payload: &[u8],
728 request_id: &str,
729 command: Command,
730 action: Action,
731 workload_class: WorkloadClass,
732 index_name: Option<&str>,
733 correlation: [u8; 16],
734 started: Arc<AtomicBool>,
735 diagnostic_meta: Option<&Arc<Mutex<Option<ResponseMeta>>>>,
736 ) -> Result<ResultBody, ClientError> {
737 let credential = self.credential().await?;
741 let native_credential = native_credential(&credential)?;
742 let session = PublicSession {
743 format_version: PUBLIC_SESSION_FORMAT_VERSION,
744 api_key_credential: native_credential.as_slice(),
745 collection_id: &self.inner.config.profile.as_wire().expected_collection_id.value,
746 request_id,
747 action: action_name(action),
748 workload_class,
749 index_name,
750 request_correlation_id: correlation,
751 };
752 let session = SensitiveBuffer(serde_json::to_vec(&session).map_err(|_| ClientError::RequestCodec)?);
753 if session.is_empty() || session.len() > MAX_PUBLIC_SESSION_BYTES {
754 return Err(ClientError::RequestTooLarge);
755 }
756 let mut wire = SensitiveBuffer::new();
757 wire.extend_u32(session.len())?;
758 wire.extend(session.as_slice());
759 wire.extend_u32(payload.len())?;
760 wire.extend(payload);
761
762 if self.inner.transport.inner.connection_slots.available_permits() == 0 {
763 self.inner
764 .transport
765 .inner
766 .metrics
767 .bulkhead_saturation
768 .fetch_add(1, Ordering::Relaxed);
769 }
770 let _permit = self
771 .inner
772 .transport
773 .inner
774 .connection_slots
775 .clone()
776 .acquire_owned()
777 .await
778 .map_err(|_| ClientError::Transport {
779 kind: TransportErrorKind::PoolClosed,
780 disposition: SubmissionDisposition::NotSubmitted,
781 })?;
782 let endpoint_index = self.inner.transport.inner.next_endpoint.fetch_add(1, Ordering::Relaxed)
783 % self.inner.transport.inner.config.native_endpoints.len();
784 let mut connection = {
785 let mut pool = self.inner.transport.inner.pool.lock().await;
786 let now = tokio::time::Instant::now();
794 let connection = loop {
795 match pool.pop() {
796 Some(connection) if connection_idle_is_reusable(connection.idle_since, now) => {
797 break Some(connection);
798 }
799 Some(_) => {}
800 None => break None,
801 }
802 };
803 self.inner
804 .transport
805 .inner
806 .metrics
807 .idle_connections
808 .store(pool.len(), Ordering::Relaxed);
809 connection
810 };
811 if connection.is_none() {
812 connection = Some(self.connect_native(endpoint_index).await?);
813 }
814 let mut connection = connection.ok_or(ClientError::Transport {
815 kind: TransportErrorKind::Connect,
816 disposition: SubmissionDisposition::NotSubmitted,
817 })?;
818 started.store(true, Ordering::Release);
819 if connection.stream.write_all(wire.as_slice()).await.is_err() || connection.stream.flush().await.is_err() {
820 return Err(ClientError::Transport {
821 kind: TransportErrorKind::Write,
822 disposition: SubmissionDisposition::Indeterminate,
823 });
824 }
825 let response_length = connection.stream.read_u32().await.map_err(|_| ClientError::Transport {
826 kind: TransportErrorKind::Read,
827 disposition: SubmissionDisposition::Indeterminate,
828 })?;
829 let response_length = usize::try_from(response_length).map_err(|_| ClientError::ResponseTooLarge)?;
830 if response_length == 0 || response_length > MAX_NATIVE_RESPONSE_BYTES {
831 return Err(ClientError::ResponseTooLarge);
832 }
833 let mut response = vec![0; response_length];
834 connection
835 .stream
836 .read_exact(&mut response)
837 .await
838 .map_err(|_| ClientError::Transport {
839 kind: TransportErrorKind::Read,
840 disposition: SubmissionDisposition::Indeterminate,
841 })?;
842 let decoded: ResponseEnvelope = serde_json::from_slice(&response).map_err(|_| ClientError::ResponseCodec)?;
843 if serde_json::to_vec(&decoded).map_err(|_| ClientError::ResponseCodec)? != response {
844 return Err(ClientError::NonCanonicalResponse);
845 }
846 let result = match decoded {
847 ResponseEnvelope::Success { meta, result } if response_meta_matches(&meta, request_id, &correlation) => {
848 record_response_meta(diagnostic_meta, &meta)?;
849 if command_for_result(&result) != command {
850 Err(ClientError::ResponseCommandMismatch)
851 } else {
852 Ok(*result)
853 }
854 }
855 ResponseEnvelope::Success { .. } => Err(ClientError::ResponseIdentityMismatch),
856 ResponseEnvelope::Error { meta, error } => {
857 if !response_meta_matches(&meta, request_id, &correlation) {
858 return Err(ClientError::ResponseIdentityMismatch);
859 }
860 record_response_meta(diagnostic_meta, &meta)?;
861 validate_error_body(&error).map_err(|_| ClientError::InvalidErrorShape)?;
862 let semantic = LODESTONE_API_COMMAND_SEMANTICS
863 .iter()
864 .find(|contract| contract.command == command)
865 .ok_or(ClientError::MissingCommandContract)?;
866 if !semantic.admissible_errors.contains(&error.code) {
867 return Err(ClientError::InvalidErrorShape);
868 }
869 Err(ClientError::Server(error))
870 }
871 };
872 if response_keeps_connection(&result) {
880 let mut pool = self.inner.transport.inner.pool.lock().await;
881 if pool.len() < self.pool_config().maximum_idle_connections {
882 connection.idle_since = tokio::time::Instant::now();
883 pool.push(connection);
884 self.inner
885 .transport
886 .inner
887 .metrics
888 .idle_connections
889 .store(pool.len(), Ordering::Relaxed);
890 }
891 }
892 result
893 }
894
895 async fn connect_native(&self, endpoint_index: usize) -> Result<NativeConnection, ClientError> {
896 let endpoint = self
897 .inner
898 .transport
899 .inner
900 .config
901 .native_endpoints
902 .get(endpoint_index)
903 .ok_or(ClientError::InvalidEndpoint)?;
904 let stream = tokio::time::timeout(
905 self.pool_config().connect_timeout,
906 TcpStream::connect((endpoint.host(), endpoint.port())),
907 )
908 .await
909 .map_err(|_| ClientError::Transport {
910 kind: TransportErrorKind::Connect,
911 disposition: SubmissionDisposition::NotSubmitted,
912 })?
913 .map_err(|_| ClientError::Transport {
914 kind: TransportErrorKind::Connect,
915 disposition: SubmissionDisposition::NotSubmitted,
916 })?;
917 let server_name = ServerName::try_from(self.inner.transport.inner.config.identity.tls_server_name().to_owned())
918 .map_err(|_| ClientError::Config(ClientConfigError::InvalidServerName))?;
919 let connector = TlsConnector::from(Arc::clone(&self.inner.transport.inner.tls));
920 let stream = tokio::time::timeout(
921 self.pool_config().connect_timeout,
922 connector.connect(server_name, stream),
923 )
924 .await
925 .map_err(|_| ClientError::Transport {
926 kind: TransportErrorKind::Tls,
927 disposition: SubmissionDisposition::NotSubmitted,
928 })?
929 .map_err(|_| ClientError::Transport {
930 kind: TransportErrorKind::Tls,
931 disposition: SubmissionDisposition::NotSubmitted,
932 })?;
933 if stream.get_ref().1.alpn_protocol() != Some(LODESTONE_API_ALPN.as_bytes()) {
934 return Err(ClientError::Transport {
935 kind: TransportErrorKind::Tls,
936 disposition: SubmissionDisposition::NotSubmitted,
937 });
938 }
939 self.inner
940 .transport
941 .inner
942 .metrics
943 .live_connections
944 .fetch_add(1, Ordering::Relaxed);
945 Ok(NativeConnection {
946 stream,
947 metrics: Arc::clone(&self.inner.transport.inner.metrics),
948 idle_since: tokio::time::Instant::now(),
949 })
950 }
951
952 pub(crate) async fn credential(&self) -> Result<CredentialMaterial, ClientError> {
953 let profile = self.inner.config.profile.as_wire();
954 self.inner
955 .transport
956 .inner
957 .metrics
958 .credential_fetches
959 .fetch_add(1, Ordering::Relaxed);
960 let material = self
961 .inner
962 .config
963 .credentials
964 .provide(&profile.secret_reference)
965 .await
966 .map_err(ClientError::Credential)?;
967 if material.api_key_id() != &profile.credential_id || material.generation() != &profile.credential_generation {
968 return Err(ClientError::Credential(CredentialProviderError::InvalidMaterial));
969 }
970 Ok(material)
971 }
972
973 pub(crate) fn transport_config(&self) -> &CellTransportConfig {
974 &self.inner.transport.inner.config
975 }
976
977 pub(crate) fn expected_collection_id(&self) -> &str {
978 &self.inner.config.profile.as_wire().expected_collection_id.value
979 }
980
981 pub(crate) fn pool_config(&self) -> PoolConfig {
982 self.inner.transport.pool_config()
983 }
984}
985
986fn response_keeps_connection(result: &Result<ResultBody, ClientError>) -> bool {
987 result.is_ok()
988}
989
990fn record_response_meta(
991 destination: Option<&Arc<Mutex<Option<ResponseMeta>>>>,
992 meta: &ResponseMeta,
993) -> Result<(), ClientError> {
994 let Some(destination) = destination else {
995 return Ok(());
996 };
997 let mut destination = destination.lock().map_err(|_| ClientError::InternalStateUnavailable)?;
998 *destination = Some(meta.clone());
999 Ok(())
1000}
1001
1002const MAX_REUSABLE_CONNECTION_IDLE: Duration = Duration::from_secs(1);
1003
1004fn connection_idle_is_reusable(idle_since: tokio::time::Instant, now: tokio::time::Instant) -> bool {
1005 now.checked_duration_since(idle_since)
1006 .is_some_and(|idle| idle <= MAX_REUSABLE_CONNECTION_IDLE)
1007}
1008
1009const fn default_workload_class(action: Action) -> WorkloadClass {
1010 match action {
1011 Action::DataRead | Action::SubscriptionRead => WorkloadClass::InteractiveSearch,
1012 Action::DataWrite | Action::Append | Action::ResolveAppend => WorkloadClass::StreamingIngest,
1013 Action::IdentityDiscovery
1014 | Action::IndexAdmin
1015 | Action::CatalogRead
1016 | Action::TaskRead
1017 | Action::DiagnosticsRead
1018 | Action::DiagnosticsCreate
1019 | Action::SecurityRead
1020 | Action::SecurityAdmin
1021 | Action::AuditRead
1022 | Action::QuotaRead
1023 | Action::QuotaAdmin
1024 | Action::BindingRead
1025 | Action::LifecycleAdmin
1026 | Action::AdmissionRead
1027 | Action::AdmissionAdmin
1028 | Action::UsageRead
1029 | Action::OperationRead
1030 | Action::OperationCancel
1031 | Action::CollectionExport
1032 | Action::CollectionRestore
1033 | Action::CollectionTransferExport
1034 | Action::CollectionTransferRestore
1035 | Action::CollectionStandbyAdmission
1036 | Action::CollectionStandbyActivation
1037 | Action::IndexExport
1038 | Action::IndexImport => WorkloadClass::BatchAnalytics,
1039 }
1040}
1041
1042#[derive(serde::Serialize)]
1043struct PublicSession<'a> {
1044 format_version: u16,
1045 api_key_credential: &'a [u8],
1046 collection_id: &'a str,
1047 request_id: &'a str,
1048 action: &'static str,
1049 workload_class: WorkloadClass,
1050 #[serde(skip_serializing_if = "Option::is_none")]
1051 index_name: Option<&'a str>,
1052 request_correlation_id: [u8; 16],
1053}
1054
1055fn canonical_request(envelope: &RequestEnvelope) -> Result<Vec<u8>, ClientError> {
1056 if envelope.meta.request_id.is_empty()
1057 || envelope.meta.request_id.len() > MAX_REQUEST_ID_BYTES
1058 || envelope.meta.request_id.bytes().any(|byte| byte.is_ascii_control())
1059 {
1060 return Err(ClientError::InvalidRequestIdentity);
1061 }
1062 let payload = serde_json::to_vec(envelope).map_err(|_| ClientError::RequestCodec)?;
1063 if payload.is_empty() || payload.len() > MAX_NATIVE_REQUEST_BYTES {
1064 return Err(ClientError::RequestTooLarge);
1065 }
1066 let decoded: RequestEnvelope = serde_json::from_slice(&payload).map_err(|_| ClientError::RequestCodec)?;
1067 if decoded != *envelope {
1068 return Err(ClientError::RequestCodec);
1069 }
1070 Ok(payload)
1071}
1072
1073fn build_tls_config(config: &CellTransportConfig) -> Result<rustls::ClientConfig, ClientError> {
1074 let mut roots = rustls::RootCertStore::empty();
1075 for certificate in config.trust.certificates_der() {
1076 roots
1077 .add(CertificateDer::from(certificate.clone()))
1078 .map_err(|_| ClientError::Config(ClientConfigError::InvalidTrustMaterial))?;
1079 }
1080 let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
1081 let mut tls = rustls::ClientConfig::builder_with_provider(provider)
1082 .with_protocol_versions(&[&rustls::version::TLS13])
1083 .map_err(|_| ClientError::Config(ClientConfigError::InvalidTrustMaterial))?
1084 .with_root_certificates(roots)
1085 .with_no_client_auth();
1086 tls.alpn_protocols = vec![LODESTONE_API_ALPN.as_bytes().to_vec()];
1087 Ok(tls)
1088}
1089
1090fn validate_discovery(config: &CollectionSessionConfig, discovered: &DiscoverResult) -> Result<(), ClientError> {
1091 let profile = config.profile.as_wire();
1092 if discovered.collection_id != profile.expected_collection_id
1093 || discovered.principal_id != profile.expected_principal_id
1094 || discovered.schema_id != LODESTONE_API_SCHEMA_ID
1095 || discovered.schema_digest.value != LODESTONE_API_SPEC_SHA256
1096 {
1097 return Err(ClientError::ProfileIdentityMismatch);
1098 }
1099 let expected = LODESTONE_API_STREAM_CONTRACTS
1100 .first()
1101 .ok_or(ClientError::MissingCommandContract)?;
1102 if discovered.streams.len() != 1
1103 || discovered.streams.first().is_none_or(|stream| {
1104 stream.name != expected.name
1105 || stream.transport != expected.transport
1106 || stream.service != expected.service
1107 || stream.method != expected.method
1108 || stream.endpoint != profile.selected_stream_endpoint
1109 || stream.auth_metadata != expected.auth_metadata
1110 || stream.auth_scheme != expected.auth_scheme
1111 || stream.semantic_format_version != u32::from(expected.semantic_format_version)
1112 || stream.schema_digest.value != LODESTONE_API_SPEC_SHA256
1113 })
1114 {
1115 return Err(ClientError::ProfileStreamMismatch);
1116 }
1117 Ok(())
1118}
1119
1120fn validate_binding(
1121 config: &CollectionSessionConfig,
1122 binding: &CoreBinding,
1123 derived: bool,
1124 revision: &U64,
1125) -> Result<(), ClientError> {
1126 let profile = config.profile.as_wire();
1127 if !derived
1128 || binding.cluster_id != profile.expected_cluster_id
1129 || binding.collection_id != profile.expected_collection_id
1130 || binding.native_endpoints != profile.native_endpoints
1131 || binding.selected_stream_endpoint != profile.selected_stream_endpoint
1132 || binding.schema_id != profile.schema_id
1133 || binding.schema_digest != profile.schema_digest
1134 || parse_u64(&binding.configuration_generation.value).is_none_or(|generation| generation == 0)
1135 || parse_u64(&revision.value).is_none_or(|revision| revision == 0)
1136 {
1137 return Err(ClientError::ProfileIdentityMismatch);
1138 }
1139 validate_storage_profile(profile, binding, revision)?;
1140 Ok(())
1141}
1142
1143fn validate_storage_profile(
1144 profile: &lodestone_wire::ConnectionProfile,
1145 binding: &CoreBinding,
1146 binding_revision: &U64,
1147) -> Result<(), ClientError> {
1148 let attestation = &binding.storage_profile;
1149 let expected_roles = [
1150 StorageProfileRole::Control,
1151 StorageProfileRole::DefaultWal,
1152 StorageProfileRole::DefaultData,
1153 ];
1154 let roles_are_canonical = attestation.roles.len() == expected_roles.len()
1155 && attestation
1156 .roles
1157 .iter()
1158 .zip(expected_roles)
1159 .all(|(binding, expected)| binding.role == expected)
1160 && canonical_authority_groups(&attestation.roles);
1161 if attestation.format_version != u32::from(LODESTONE_API_STORAGE_PROFILE_ATTESTATION_FORMAT_VERSION)
1162 || parse_u64(&attestation.configuration_generation.value) != parse_u64(&binding.configuration_generation.value)
1163 || parse_u64(&binding_revision.value).is_none_or(|revision| revision == 0)
1164 || !roles_are_canonical
1165 || attestation.default_policy_digest.validate().is_err()
1166 || attestation.storage_profile_digest.validate().is_err()
1167 || attestation.attestation_digest.validate().is_err()
1168 {
1169 return Err(ClientError::ProfileIdentityMismatch);
1170 }
1171 if profile.storage_profile_expectation.as_ref().is_some_and(|expectation| {
1172 expectation
1173 .required_posture
1174 .is_some_and(|required| required != attestation.overall_posture)
1175 || expectation
1176 .expected_storage_profile_digest
1177 .as_ref()
1178 .is_some_and(|expected| expected != &attestation.storage_profile_digest)
1179 }) {
1180 return Err(ClientError::StorageProfileMismatch);
1181 }
1182 Ok(())
1183}
1184
1185fn canonical_authority_groups(roles: &[lodestone_wire::StorageProfileRoleBinding]) -> bool {
1186 let Some(first) = roles.first() else {
1187 return false;
1188 };
1189 if first.authority_group != 0 {
1190 return false;
1191 }
1192 let mut group_count = 1_usize;
1193 for role in roles.iter().skip(1) {
1194 let Ok(group) = usize::try_from(role.authority_group) else {
1195 return false;
1196 };
1197 if group < group_count {
1198 continue;
1199 }
1200 if group != group_count {
1201 return false;
1202 }
1203 group_count += 1;
1204 }
1205 true
1206}
1207
1208fn validate_cached_verification(
1209 config: &CollectionSessionConfig,
1210 verification: &ProfileVerification,
1211) -> Result<(), ClientError> {
1212 let profile = config.profile.as_wire();
1213 if verification.collection_id != profile.expected_collection_id
1214 || verification.principal_id != profile.expected_principal_id
1215 || verification.cluster_id != profile.expected_cluster_id
1216 || verification.schema_id != profile.schema_id
1217 || verification.schema_digest != profile.schema_digest
1218 || verification.native_endpoints != profile.native_endpoints
1219 || verification.selected_stream_endpoint != profile.selected_stream_endpoint
1220 || verification
1221 .configuration_generation
1222 .value
1223 .parse::<u64>()
1224 .ok()
1225 .is_none_or(|generation| generation == 0)
1226 || verification
1227 .binding_revision
1228 .value
1229 .parse::<u64>()
1230 .ok()
1231 .is_none_or(|revision| revision == 0)
1232 {
1233 return Err(ClientError::ProfileIdentityMismatch);
1234 }
1235 let cached_binding = CoreBinding {
1236 cluster_id: verification.cluster_id.clone(),
1237 collection_id: verification.collection_id.clone(),
1238 configuration_generation: verification.configuration_generation.clone(),
1239 native_endpoints: verification.native_endpoints.clone(),
1240 selected_stream_endpoint: verification.selected_stream_endpoint.clone(),
1241 schema_id: verification.schema_id.clone(),
1242 schema_digest: verification.schema_digest.clone(),
1243 storage_profile: verification.storage_profile.clone(),
1244 };
1245 validate_storage_profile(profile, &cached_binding, &verification.binding_revision)?;
1246 Ok(())
1247}
1248
1249fn resolve_index_name(request: &RequestBody, supplied: Option<&str>) -> Result<Option<String>, ClientError> {
1250 let scope = request_collection_index_ref(request)?;
1251 let inferred = match &scope {
1252 GeneratedCollectionIndexRef::Exact(name) => Some(name.clone()),
1253 GeneratedCollectionIndexRef::None | GeneratedCollectionIndexRef::Caller => None,
1254 };
1255 if let Some(supplied) = supplied {
1256 validate_index_name(supplied)?;
1257 if inferred.as_deref().is_some_and(|inferred| inferred != supplied) {
1258 return Err(ClientError::CollectionIndexRefMismatch);
1259 }
1260 }
1261 let resolved = supplied.map(str::to_owned).or(inferred);
1262 if let Some(name) = &resolved {
1263 validate_index_name(name)?;
1264 }
1265 match scope {
1266 GeneratedCollectionIndexRef::Caller | GeneratedCollectionIndexRef::Exact(_) if resolved.is_none() => {
1267 return Err(ClientError::CollectionIndexRefRequired);
1268 }
1269 GeneratedCollectionIndexRef::None if resolved.is_some() => return Err(ClientError::CollectionIndexRefMismatch),
1270 GeneratedCollectionIndexRef::None
1271 | GeneratedCollectionIndexRef::Caller
1272 | GeneratedCollectionIndexRef::Exact(_) => {}
1273 }
1274 Ok(resolved)
1275}
1276
1277const fn action_name(action: Action) -> &'static str {
1278 match action {
1279 Action::IdentityDiscovery => "identity_discovery",
1280 Action::DataRead => "data_read",
1281 Action::DataWrite => "data_write",
1282 Action::Append => "append",
1283 Action::ResolveAppend => "resolve_append",
1284 Action::IndexAdmin => "index_admin",
1285 Action::CatalogRead => "catalog_read",
1286 Action::TaskRead => "task_read",
1287 Action::SubscriptionRead => "subscription_read",
1288 Action::DiagnosticsRead => "diagnostics_read",
1289 Action::DiagnosticsCreate => "diagnostics_create",
1290 Action::SecurityRead => "security_read",
1291 Action::SecurityAdmin => "security_admin",
1292 Action::AuditRead => "audit_read",
1293 Action::QuotaRead => "quota_read",
1294 Action::QuotaAdmin => "quota_admin",
1295 Action::BindingRead => "binding_read",
1296 Action::LifecycleAdmin => "lifecycle_admin",
1297 Action::AdmissionRead => "admission_read",
1298 Action::AdmissionAdmin => "admission_admin",
1299 Action::UsageRead => "usage_read",
1300 Action::OperationRead => "operation_read",
1301 Action::OperationCancel => "operation_cancel",
1302 Action::CollectionExport => "collection_export",
1303 Action::CollectionRestore => "collection_restore",
1304 Action::CollectionTransferExport => "collection_transfer_export",
1305 Action::CollectionTransferRestore => "collection_transfer_restore",
1306 Action::CollectionStandbyAdmission => "collection_standby_admission",
1307 Action::CollectionStandbyActivation => "collection_standby_activation",
1308 Action::IndexExport => "index_export",
1309 Action::IndexImport => "index_import",
1310 }
1311}
1312
1313fn native_credential(material: &CredentialMaterial) -> Result<SensitiveBuffer, ClientError> {
1314 let identifier = parse_identity(&material.api_key_id().value)
1315 .ok_or(ClientError::Credential(CredentialProviderError::InvalidMaterial))?;
1316 let mut output = SensitiveBuffer::new();
1317 output.extend(identifier.to_string().as_bytes());
1318 output.extend(b":");
1319 output.extend(&base64_url_no_pad(material.secret().expose()));
1320 Ok(output)
1321}
1322
1323pub(crate) fn authorization_value(material: &CredentialMaterial) -> Result<SensitiveBuffer, ClientError> {
1324 let native = native_credential(material)?;
1325 let mut output = SensitiveBuffer::new();
1326 output.extend(b"ApiKey ");
1327 output.extend(&base64_standard(native.as_slice()));
1328 Ok(output)
1329}
1330
1331fn base64_url_no_pad(bytes: &[u8]) -> Vec<u8> {
1332 base64_encode(
1333 bytes,
1334 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",
1335 false,
1336 )
1337}
1338
1339fn base64_standard(bytes: &[u8]) -> Vec<u8> {
1340 base64_encode(
1341 bytes,
1342 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
1343 true,
1344 )
1345}
1346
1347pub fn raw_json_text(text: &str) -> Result<RawJson, ClientError> {
1350 let bytes_base64 = String::from_utf8(base64_standard(text.as_bytes())).map_err(|_| ClientError::RequestCodec)?;
1351 Ok(RawJson { bytes_base64 })
1352}
1353
1354fn base64_encode(bytes: &[u8], alphabet: &[u8; 64], padded: bool) -> Vec<u8> {
1355 let mut output = Vec::with_capacity(bytes.len().div_ceil(3).saturating_mul(4));
1356 for chunk in bytes.chunks(3) {
1357 let first = chunk[0];
1358 let second = chunk.get(1).copied().unwrap_or(0);
1359 let third = chunk.get(2).copied().unwrap_or(0);
1360 output.push(alphabet[usize::from(first >> 2)]);
1361 output.push(alphabet[usize::from(((first & 0x03) << 4) | (second >> 4))]);
1362 if chunk.len() > 1 {
1363 output.push(alphabet[usize::from(((second & 0x0f) << 2) | (third >> 6))]);
1364 } else if padded {
1365 output.push(b'=');
1366 }
1367 if chunk.len() > 2 {
1368 output.push(alphabet[usize::from(third & 0x3f)]);
1369 } else if padded {
1370 output.push(b'=');
1371 }
1372 }
1373 output
1374}
1375
1376fn random_identity() -> Result<[u8; 16], ClientError> {
1377 let mut output = [0; 16];
1378 getrandom::fill(&mut output).map_err(|_| ClientError::RandomUnavailable)?;
1379 if output == [0; 16] {
1380 return Err(ClientError::RandomUnavailable);
1381 }
1382 Ok(output)
1383}
1384
1385fn normalized_timeout(timeout: Duration) -> Result<Duration, ClientError> {
1386 if timeout.is_zero() || timeout > MAX_CALL_TIMEOUT {
1387 return Err(ClientError::InvalidCallOptions);
1388 }
1389 let mut millis = timeout.as_millis();
1390 if !timeout.subsec_nanos().is_multiple_of(1_000_000) {
1391 millis = millis.checked_add(1).ok_or(ClientError::InvalidCallOptions)?;
1392 }
1393 let millis = u64::try_from(millis).map_err(|_| ClientError::InvalidCallOptions)?;
1394 let normalized = Duration::from_millis(millis);
1395 if normalized > MAX_CALL_TIMEOUT {
1396 return Err(ClientError::InvalidCallOptions);
1397 }
1398 Ok(normalized)
1399}
1400
1401fn duration_millis_ceil(duration: Duration) -> Result<u64, ClientError> {
1402 let mut millis = duration.as_millis();
1403 if !duration.subsec_nanos().is_multiple_of(1_000_000) {
1404 millis = millis.checked_add(1).ok_or(ClientError::ClockUnavailable)?;
1405 }
1406 u64::try_from(millis).map_err(|_| ClientError::ClockUnavailable)
1407}
1408
1409pub(crate) fn validate_index_name(value: &str) -> Result<(), ClientError> {
1410 if value.is_empty()
1411 || value.len() > 255
1412 || value
1413 .bytes()
1414 .any(|byte| byte.is_ascii_control() || byte.is_ascii_whitespace())
1415 {
1416 return Err(ClientError::InvalidCollectionIndexRef);
1417 }
1418 Ok(())
1419}
1420
1421fn lower_hex(bytes: &[u8]) -> String {
1422 const HEX: &[u8; 16] = b"0123456789abcdef";
1423 let mut output = String::with_capacity(bytes.len().saturating_mul(2));
1424 for byte in bytes {
1425 output.push(char::from(HEX[usize::from(byte >> 4)]));
1426 output.push(char::from(HEX[usize::from(byte & 0x0f)]));
1427 }
1428 output
1429}
1430
1431pub(crate) struct SensitiveBuffer(Vec<u8>);
1432
1433impl SensitiveBuffer {
1434 fn new() -> Self {
1435 Self(Vec::new())
1436 }
1437
1438 fn extend(&mut self, bytes: &[u8]) {
1439 self.0.extend_from_slice(bytes);
1440 }
1441
1442 fn extend_u32(&mut self, value: usize) -> Result<(), ClientError> {
1443 let value = u32::try_from(value).map_err(|_| ClientError::RequestTooLarge)?;
1444 self.extend(&value.to_be_bytes());
1445 Ok(())
1446 }
1447
1448 fn is_empty(&self) -> bool {
1449 self.0.is_empty()
1450 }
1451
1452 fn len(&self) -> usize {
1453 self.0.len()
1454 }
1455
1456 pub(crate) fn as_slice(&self) -> &[u8] {
1457 &self.0
1458 }
1459}
1460
1461impl Drop for SensitiveBuffer {
1462 fn drop(&mut self) {
1463 self.0.fill(0);
1464 std::hint::black_box(&mut self.0);
1465 }
1466}
1467
1468fn started_disposition(started: &AtomicBool) -> SubmissionDisposition {
1469 if started.load(Ordering::Acquire) {
1470 SubmissionDisposition::Indeterminate
1471 } else {
1472 SubmissionDisposition::NotSubmitted
1473 }
1474}
1475
1476#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1477pub enum SubmissionDisposition {
1478 NotSubmitted,
1479 Submitted,
1480 Resolved,
1481 KnownRetired,
1482 Indeterminate,
1483}
1484
1485#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1486pub enum TransportErrorKind {
1487 PoolClosed,
1488 Connect,
1489 Tls,
1490 Write,
1491 Read,
1492 Deadline,
1493 Cancelled,
1494}
1495
1496#[derive(Debug)]
1497pub enum ClientError {
1498 Config(ClientConfigError),
1499 Credential(CredentialProviderError),
1500 InvalidCallOptions,
1501 InvalidEndpoint,
1502 InvalidCollectionIndexRef,
1503 InvalidStateIndex,
1504 InvalidStateActionId,
1505 InvalidStateKey,
1506 InvalidStateBatch,
1507 UnexpectedStateResult,
1508 CollectionIndexRefRequired,
1509 CollectionIndexRefMismatch,
1510 ProfileNotVerified,
1511 ProfileIdentityMismatch,
1512 ProfileStreamMismatch,
1513 StorageProfileMismatch,
1514 MissingCommandContract,
1515 RequestCodec,
1516 RequestTooLarge,
1517 InvalidRequestIdentity,
1518 ResponseCodec,
1519 ResponseTooLarge,
1520 NonCanonicalResponse,
1521 ResponseIdentityMismatch,
1522 ResponseCommandMismatch,
1523 InvalidErrorShape,
1524 RandomUnavailable,
1525 ClockUnavailable,
1526 InternalStateUnavailable,
1527 Transport {
1528 kind: TransportErrorKind,
1529 disposition: SubmissionDisposition,
1530 },
1531 Server(ErrorBody),
1532}
1533
1534#[derive(Debug)]
1536pub struct ClientCallDiagnosticResult {
1537 pub result: ResultBody,
1538 pub response_meta: Option<ResponseMeta>,
1539}
1540
1541#[derive(Debug)]
1544pub struct ClientCallDiagnosticFailure {
1545 pub error: Box<ClientError>,
1546 pub response_meta: Option<ResponseMeta>,
1547}
1548
1549impl ClientError {
1550 #[must_use]
1551 pub fn submission_disposition(&self) -> SubmissionDisposition {
1552 match self {
1553 Self::Transport { disposition, .. } => *disposition,
1554 Self::Server(error) => server_error_disposition(error),
1555 Self::UnexpectedStateResult
1556 | Self::ResponseCodec
1557 | Self::ResponseTooLarge
1558 | Self::NonCanonicalResponse
1559 | Self::ResponseIdentityMismatch
1560 | Self::ResponseCommandMismatch
1561 | Self::InvalidErrorShape => SubmissionDisposition::Indeterminate,
1562 _ => SubmissionDisposition::NotSubmitted,
1563 }
1564 }
1565
1566 #[must_use]
1570 pub fn automatic_retry_admitted(&self) -> bool {
1571 matches!(
1572 self.retry_directive(),
1573 Some((
1574 ErrorDisposition::NotSubmitted,
1575 RecoveryAction::Retry | RecoveryAction::RetryAfter
1576 ))
1577 )
1578 }
1579
1580 fn retry_directive(&self) -> Option<(ErrorDisposition, RecoveryAction)> {
1581 match self {
1582 Self::Server(error) => Some((error.disposition, error.recovery)),
1583 Self::Transport {
1584 kind: TransportErrorKind::Deadline | TransportErrorKind::Cancelled,
1585 ..
1586 } => Some((ErrorDisposition::NotSubmitted, RecoveryAction::Stop)),
1587 Self::Transport {
1588 disposition: SubmissionDisposition::NotSubmitted,
1589 ..
1590 } => Some((ErrorDisposition::NotSubmitted, RecoveryAction::Retry)),
1591 Self::Transport { .. } => Some((ErrorDisposition::Indeterminate, RecoveryAction::Resolve)),
1592 _ => None,
1593 }
1594 }
1595
1596 pub(crate) fn is_deadline_exceeded(&self) -> bool {
1597 matches!(
1598 self,
1599 Self::Transport {
1600 kind: TransportErrorKind::Deadline,
1601 ..
1602 } | Self::Server(ErrorBody {
1603 code: lodestone_wire::ErrorCode::DeadlineExceeded,
1604 ..
1605 })
1606 )
1607 }
1608}
1609
1610fn server_error_disposition(error: &ErrorBody) -> SubmissionDisposition {
1611 match error.disposition {
1612 lodestone_wire::ErrorDisposition::NotSubmitted => SubmissionDisposition::NotSubmitted,
1613 lodestone_wire::ErrorDisposition::Submitted => SubmissionDisposition::Submitted,
1614 lodestone_wire::ErrorDisposition::Resolved => SubmissionDisposition::Resolved,
1615 lodestone_wire::ErrorDisposition::KnownRetired => SubmissionDisposition::KnownRetired,
1616 lodestone_wire::ErrorDisposition::Indeterminate => SubmissionDisposition::Indeterminate,
1617 }
1618}
1619
1620fn response_meta_matches(meta: &lodestone_wire::ResponseMeta, request_id: &str, correlation: &[u8; 16]) -> bool {
1621 meta.format_version == u32::from(LODESTONE_API_RESPONSE_META_FORMAT_VERSION)
1622 && meta.request_id == request_id
1623 && meta.correlation_id.value == lower_hex(correlation)
1624 && canonical_identity(&meta.diagnostic_id.value)
1625}
1626
1627fn canonical_identity(value: &str) -> bool {
1628 value.len() == 32
1629 && value
1630 .bytes()
1631 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
1632 && value.bytes().any(|byte| byte != b'0')
1633}
1634
1635impl From<ClientConfigError> for ClientError {
1636 fn from(error: ClientConfigError) -> Self {
1637 Self::Config(error)
1638 }
1639}
1640
1641impl Display for ClientError {
1642 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
1643 formatter.write_str(match self {
1644 Self::Config(_) => "Core client configuration is invalid",
1645 Self::Credential(_) => "Core credential material is unavailable or mismatched",
1646 Self::InvalidCallOptions => "Core call options are invalid",
1647 Self::InvalidEndpoint => "Core endpoint is invalid",
1648 Self::InvalidCollectionIndexRef => "Core request index scope is invalid",
1649 Self::InvalidStateIndex => "Lodestone state index identity is invalid",
1650 Self::InvalidStateActionId => "Lodestone state action identity is invalid",
1651 Self::InvalidStateKey => "Lodestone state key must contain 1 to 254 bytes",
1652 Self::InvalidStateBatch => "Lodestone state key batch is empty or exceeds its bound",
1653 Self::UnexpectedStateResult => "Lodestone state result differs from the requested operation",
1654 Self::CollectionIndexRefRequired => "Core request requires an exact index scope",
1655 Self::CollectionIndexRefMismatch => "Core request and admission index scopes differ",
1656 Self::ProfileNotVerified => "Core mutation is refused until discovery and binding are verified",
1657 Self::ProfileIdentityMismatch => "Core discovery or binding differs from the connection profile",
1658 Self::ProfileStreamMismatch => "Core selected stream differs from the connection profile",
1659 Self::StorageProfileMismatch => "Core storage profile differs from the connection profile expectation",
1660 Self::MissingCommandContract => "generated Core command metadata is incomplete",
1661 Self::RequestCodec => "Core request is not a canonical current Lodestone API value",
1662 Self::RequestTooLarge => "Core request exceeds its generated byte bound",
1663 Self::InvalidRequestIdentity => "Core request identity is invalid",
1664 Self::ResponseCodec => "Core response is malformed or unsupported",
1665 Self::ResponseTooLarge => "Core response exceeds its generated byte bound",
1666 Self::NonCanonicalResponse => "Core response encoding is noncanonical",
1667 Self::ResponseIdentityMismatch => "Core response changed the request identity or version",
1668 Self::ResponseCommandMismatch => "Core response returned another command result",
1669 Self::InvalidErrorShape => "Core error detail does not match its generated code",
1670 Self::RandomUnavailable => "secure request identity generation is unavailable",
1671 Self::ClockUnavailable => "absolute Core request deadline cannot be calculated",
1672 Self::InternalStateUnavailable => "Core client verification state is unavailable",
1673 Self::Transport { .. } => "Core transport failed",
1674 Self::Server(_) => "Core returned a typed request error",
1675 })
1676 }
1677}
1678
1679impl Error for ClientError {}
1680
1681#[cfg(test)]
1682mod tests {
1683 use std::error::Error;
1684
1685 use lodestone_wire::{GetIndexRequest, Identity, OpenPitRequest, Resource, U64};
1686
1687 use super::*;
1688 use crate::SecretBytes;
1689
1690 #[test]
1691 fn diagnostic_metadata_is_request_local_and_exact() -> Result<(), Box<dyn Error>> {
1692 let first = Arc::new(Mutex::new(None::<ResponseMeta>));
1693 let second = Arc::new(Mutex::new(None::<ResponseMeta>));
1694 let meta = ResponseMeta {
1695 format_version: u32::from(LODESTONE_API_RESPONSE_META_FORMAT_VERSION),
1696 request_id: "r-diagnostic".to_owned(),
1697 correlation_id: Identity { value: "12".repeat(16) },
1698 diagnostic_id: Identity { value: "34".repeat(16) },
1699 };
1700 record_response_meta(Some(&first), &meta)?;
1701 assert_eq!(first.lock().map_err(|_| "first lock")?.as_ref(), Some(&meta));
1702 assert!(second.lock().map_err(|_| "second lock")?.is_none());
1703 Ok(())
1704 }
1705
1706 #[test]
1707 fn response_failures_preserve_indeterminate_submission() {
1708 for error in [
1709 ClientError::UnexpectedStateResult,
1710 ClientError::ResponseCodec,
1711 ClientError::ResponseTooLarge,
1712 ClientError::NonCanonicalResponse,
1713 ClientError::ResponseIdentityMismatch,
1714 ClientError::ResponseCommandMismatch,
1715 ClientError::InvalidErrorShape,
1716 ] {
1717 assert_eq!(error.submission_disposition(), SubmissionDisposition::Indeterminate);
1718 }
1719 assert_eq!(
1720 ClientError::RequestCodec.submission_disposition(),
1721 SubmissionDisposition::NotSubmitted
1722 );
1723 }
1724
1725 #[test]
1726 fn response_metadata_requires_exact_echo_and_nonzero_canonical_diagnostic() {
1727 let correlation = [0x12; 16];
1728 let mut meta = lodestone_wire::ResponseMeta {
1729 format_version: u32::from(LODESTONE_API_RESPONSE_META_FORMAT_VERSION),
1730 request_id: "request-1".to_owned(),
1731 correlation_id: Identity {
1732 value: lower_hex(&correlation),
1733 },
1734 diagnostic_id: Identity { value: "34".repeat(16) },
1735 };
1736 assert!(response_meta_matches(&meta, "request-1", &correlation));
1737
1738 meta.correlation_id.value = "56".repeat(16);
1739 assert!(!response_meta_matches(&meta, "request-1", &correlation));
1740 meta.correlation_id.value = lower_hex(&correlation);
1741
1742 meta.diagnostic_id.value = "00".repeat(16);
1743 assert!(!response_meta_matches(&meta, "request-1", &correlation));
1744 meta.diagnostic_id.value = "AB".repeat(16);
1745 assert!(!response_meta_matches(&meta, "request-1", &correlation));
1746 meta.diagnostic_id.value = "34".repeat(16);
1747
1748 meta.request_id = "request-2".to_owned();
1749 assert!(!response_meta_matches(&meta, "request-1", &correlation));
1750 meta.request_id = "request-1".to_owned();
1751 meta.format_version = meta.format_version.saturating_add(1);
1752 assert!(!response_meta_matches(&meta, "request-1", &correlation));
1753 }
1754
1755 fn cache_key(collection: u128, principal: u128, credential: u128) -> VerifiedSessionCacheKey {
1756 VerifiedSessionCacheKey {
1757 collection_id: format!("{collection:032x}"),
1758 principal_id: format!("{principal:032x}"),
1759 profile_generation: "1".to_owned(),
1760 credential_id: format!("{credential:032x}"),
1761 credential_generation: "1".to_owned(),
1762 secret_reference: format!("memory://session/{collection}/{principal}/{credential}"),
1763 }
1764 }
1765
1766 fn verification(collection: u128, principal: u128) -> ProfileVerification {
1767 ProfileVerification {
1768 artifact_version: "development".to_owned(),
1769 principal_id: Identity {
1770 value: format!("{principal:032x}"),
1771 },
1772 collection_id: Identity {
1773 value: format!("{collection:032x}"),
1774 },
1775 policy_revision: U64 { value: "1".to_owned() },
1776 cluster_id: Identity {
1777 value: "00000000000000000000000000000001".to_owned(),
1778 },
1779 configuration_generation: U64 { value: "1".to_owned() },
1780 binding_revision: U64 { value: "1".to_owned() },
1781 schema_id: LODESTONE_API_SCHEMA_ID.to_owned(),
1782 schema_digest: lodestone_wire::Digest {
1783 value: LODESTONE_API_SPEC_SHA256.to_owned(),
1784 },
1785 native_endpoints: vec!["lodestone+tls://core.example:8443".to_owned()],
1786 selected_stream_endpoint: "https://core.example:9443".to_owned(),
1787 storage_profile: test_storage_profile(),
1788 }
1789 }
1790
1791 fn test_storage_profile() -> StorageProfileAttestation {
1792 StorageProfileAttestation {
1793 format_version: u32::from(LODESTONE_API_STORAGE_PROFILE_ATTESTATION_FORMAT_VERSION),
1794 configuration_generation: U64 { value: "1".to_owned() },
1795 overall_posture: lodestone_wire::StorageOverallPosture::FilesystemDevelopment,
1796 roles: vec![
1797 lodestone_wire::StorageProfileRoleBinding {
1798 role: StorageProfileRole::Control,
1799 authority_group: 0,
1800 provider: lodestone_wire::StorageProviderPosture::FilesystemDevelopment,
1801 service_class: lodestone_wire::StorageBackendServiceClass::RegionalObject,
1802 },
1803 lodestone_wire::StorageProfileRoleBinding {
1804 role: StorageProfileRole::DefaultWal,
1805 authority_group: 0,
1806 provider: lodestone_wire::StorageProviderPosture::FilesystemDevelopment,
1807 service_class: lodestone_wire::StorageBackendServiceClass::RegionalObject,
1808 },
1809 lodestone_wire::StorageProfileRoleBinding {
1810 role: StorageProfileRole::DefaultData,
1811 authority_group: 0,
1812 provider: lodestone_wire::StorageProviderPosture::FilesystemDevelopment,
1813 service_class: lodestone_wire::StorageBackendServiceClass::RegionalObject,
1814 },
1815 ],
1816 default_durability: lodestone_wire::ResourceDurabilityClass::RegionalObject,
1817 default_storage: lodestone_wire::ResourceStorageClass::Standard,
1818 default_policy_digest: lodestone_wire::Digest { value: "11".repeat(32) },
1819 storage_profile_digest: lodestone_wire::Digest { value: "22".repeat(32) },
1820 attestation_digest: lodestone_wire::Digest { value: "33".repeat(32) },
1821 }
1822 }
1823
1824 fn test_session_with_storage_expectation(
1825 posture: Option<lodestone_wire::StorageOverallPosture>,
1826 digest: Option<&str>,
1827 ) -> Result<CollectionSessionConfig, Box<dyn Error>> {
1828 let profile = crate::ValidatedConnectionProfile::new(lodestone_wire::ConnectionProfile {
1829 format_version: u32::from(lodestone_wire::LODESTONE_API_CONNECTION_PROFILE_FORMAT_VERSION),
1830 profile_generation: U64 { value: "1".to_owned() },
1831 native_endpoints: vec!["lodestone+tls://core.example:8443".to_owned()],
1832 selected_stream_endpoint: "https://core.example:9443".to_owned(),
1833 tls_server_name: "core.example".to_owned(),
1834 tls_trust_sha256: lodestone_wire::Digest { value: "44".repeat(32) },
1835 expected_cluster_id: Identity {
1836 value: "00000000000000000000000000000001".to_owned(),
1837 },
1838 expected_collection_id: Identity {
1839 value: "00000000000000000000000000000002".to_owned(),
1840 },
1841 expected_principal_id: Identity {
1842 value: "00000000000000000000000000000003".to_owned(),
1843 },
1844 schema_id: LODESTONE_API_SCHEMA_ID.to_owned(),
1845 schema_digest: lodestone_wire::Digest {
1846 value: LODESTONE_API_SPEC_SHA256.to_owned(),
1847 },
1848 credential_id: Identity {
1849 value: "00000000000000000000000000000004".to_owned(),
1850 },
1851 credential_generation: U64 { value: "1".to_owned() },
1852 credential_expires_at: None,
1853 secret_reference: "memory://storage-profile-test".to_owned(),
1854 storage_profile_expectation: Some(lodestone_wire::StorageProfileExpectation {
1855 format_version: u32::from(lodestone_wire::LODESTONE_API_STORAGE_PROFILE_EXPECTATION_FORMAT_VERSION),
1856 required_posture: posture,
1857 expected_storage_profile_digest: digest.map(|value| lodestone_wire::Digest {
1858 value: value.to_owned(),
1859 }),
1860 }),
1861 })?;
1862 Ok(CollectionSessionConfig {
1863 profile,
1864 credentials: Arc::new(crate::FileCredentialProvider),
1865 })
1866 }
1867
1868 fn test_binding(storage_profile: StorageProfileAttestation) -> CoreBinding {
1869 CoreBinding {
1870 cluster_id: Identity {
1871 value: "00000000000000000000000000000001".to_owned(),
1872 },
1873 collection_id: Identity {
1874 value: "00000000000000000000000000000002".to_owned(),
1875 },
1876 configuration_generation: U64 { value: "1".to_owned() },
1877 native_endpoints: vec!["lodestone+tls://core.example:8443".to_owned()],
1878 selected_stream_endpoint: "https://core.example:9443".to_owned(),
1879 schema_id: LODESTONE_API_SCHEMA_ID.to_owned(),
1880 schema_digest: lodestone_wire::Digest {
1881 value: LODESTONE_API_SPEC_SHA256.to_owned(),
1882 },
1883 storage_profile,
1884 }
1885 }
1886
1887 #[test]
1888 fn storage_profile_expectations_fail_before_session_installation() -> Result<(), Box<dyn Error>> {
1889 let correct = test_session_with_storage_expectation(
1890 Some(lodestone_wire::StorageOverallPosture::FilesystemDevelopment),
1891 Some(&"22".repeat(32)),
1892 )?;
1893 let binding = test_binding(test_storage_profile());
1894 let revision = U64 { value: "1".to_owned() };
1895 validate_binding(&correct, &binding, true, &revision)?;
1896
1897 let wrong_posture = test_session_with_storage_expectation(
1898 Some(lodestone_wire::StorageOverallPosture::ProviderObjectStorage),
1899 None,
1900 )?;
1901 assert!(matches!(
1902 validate_binding(&wrong_posture, &binding, true, &revision),
1903 Err(ClientError::StorageProfileMismatch)
1904 ));
1905 assert_eq!(
1906 ClientError::StorageProfileMismatch.submission_disposition(),
1907 SubmissionDisposition::NotSubmitted
1908 );
1909
1910 let wrong_digest = test_session_with_storage_expectation(None, Some(&"55".repeat(32)))?;
1911 assert!(matches!(
1912 validate_binding(&wrong_digest, &binding, true, &revision),
1913 Err(ClientError::StorageProfileMismatch)
1914 ));
1915 Ok(())
1916 }
1917
1918 #[test]
1919 fn malformed_storage_profile_shape_is_an_identity_mismatch() -> Result<(), Box<dyn Error>> {
1920 let config = test_session_with_storage_expectation(
1921 Some(lodestone_wire::StorageOverallPosture::FilesystemDevelopment),
1922 None,
1923 )?;
1924 let revision = U64 { value: "1".to_owned() };
1925 let mut malformed = test_storage_profile();
1926 malformed.roles.swap(0, 1);
1927 assert!(matches!(
1928 validate_binding(&config, &test_binding(malformed), true, &revision),
1929 Err(ClientError::ProfileIdentityMismatch)
1930 ));
1931
1932 let mut malformed = test_storage_profile();
1933 malformed.roles[1].authority_group = 2;
1934 assert!(matches!(
1935 validate_binding(&config, &test_binding(malformed), true, &revision),
1936 Err(ClientError::ProfileIdentityMismatch)
1937 ));
1938
1939 let mut malformed = test_storage_profile();
1940 malformed.configuration_generation = U64 { value: "2".to_owned() };
1941 assert!(matches!(
1942 validate_binding(&config, &test_binding(malformed), true, &revision),
1943 Err(ClientError::ProfileIdentityMismatch)
1944 ));
1945 Ok(())
1946 }
1947
1948 #[test]
1949 fn automatic_retry_uses_only_typed_disposition_and_recovery() {
1950 let transport = |disposition| ClientError::Transport {
1951 kind: TransportErrorKind::Read,
1952 disposition,
1953 };
1954 assert!(transport(SubmissionDisposition::NotSubmitted).automatic_retry_admitted());
1955 assert!(!transport(SubmissionDisposition::Submitted).automatic_retry_admitted());
1956 assert!(!transport(SubmissionDisposition::Indeterminate).automatic_retry_admitted());
1957
1958 let server = |disposition, recovery| {
1959 ClientError::Server(ErrorBody {
1960 code: lodestone_wire::ErrorCode::TemporarilyUnavailable,
1961 message: "typed retry probe".to_owned(),
1962 disposition,
1963 recovery,
1964 details: lodestone_wire::ErrorDetail::TemporarilyUnavailable {
1965 retry_after_millis: None,
1966 },
1967 })
1968 };
1969 assert!(server(ErrorDisposition::NotSubmitted, RecoveryAction::Retry).automatic_retry_admitted());
1970 assert!(server(ErrorDisposition::NotSubmitted, RecoveryAction::RetryAfter).automatic_retry_admitted());
1971 assert!(!server(ErrorDisposition::Indeterminate, RecoveryAction::Resolve).automatic_retry_admitted());
1972 assert!(!server(ErrorDisposition::NotSubmitted, RecoveryAction::Stop).automatic_retry_admitted());
1973 }
1974
1975 #[test]
1976 fn one_deadline_survives_connect_state_and_resolution_clones() -> Result<(), Box<dyn Error>> {
1977 let options = CallOptions::new(Duration::from_secs(5))?;
1978 let absolute = options.absolute_deadline_unix_millis()?;
1979 let local = options.local_deadline()?;
1980
1981 for stage in [
1984 options.clone(),
1985 options.clone(),
1986 options.clone(),
1987 options.clone(),
1988 options.for_resolution(),
1989 ] {
1990 assert_eq!(stage.absolute_deadline_unix_millis()?, absolute);
1991 assert_eq!(stage.local_deadline()?, local);
1992 }
1993
1994 let resolution = options.for_resolution();
1995 options.cancellation().cancel();
1996 assert!(options.cancellation().is_cancelled());
1997 assert!(!resolution.cancellation().is_cancelled());
1998 assert_eq!(resolution.absolute_deadline_unix_millis()?, absolute);
1999 Ok(())
2000 }
2001
2002 #[test]
2003 fn caller_supplied_absolute_deadline_is_preserved_exactly() -> Result<(), Box<dyn Error>> {
2004 let now = SystemTime::now().duration_since(UNIX_EPOCH)?;
2005 let now_millis = u64::try_from(now.as_millis())?;
2006 let absolute = now_millis.checked_add(5_000).ok_or("test deadline overflow")?;
2007 let options = CallOptions::from_absolute_deadline_unix_millis(absolute)?;
2008 assert_eq!(options.absolute_deadline_unix_millis()?, absolute);
2009
2010 let too_far = now_millis
2011 .checked_add(u64::try_from(MAX_CALL_TIMEOUT.as_millis())?)
2012 .and_then(|value| value.checked_add(1_000))
2013 .ok_or("test deadline overflow")?;
2014 assert!(matches!(
2015 CallOptions::from_absolute_deadline_unix_millis(too_far),
2016 Err(ClientError::InvalidCallOptions)
2017 ));
2018 assert_eq!(normalized_timeout(Duration::from_nanos(1))?, Duration::from_millis(1));
2019 Ok(())
2020 }
2021
2022 #[test]
2023 fn private_api_key_encodings_match_the_core_admission_contract() -> Result<(), Box<dyn Error>> {
2024 let material = CredentialMaterial::new(
2025 Identity {
2026 value: "00000000000000000000000000000001".to_owned(),
2027 },
2028 U64 { value: "1".to_owned() },
2029 SecretBytes::from_array([0; 32]),
2030 )?;
2031 assert_eq!(
2032 native_credential(&material)?.as_slice(),
2033 b"1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
2034 );
2035 assert_eq!(
2036 authorization_value(&material)?.as_slice(),
2037 b"ApiKey MTpBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB"
2038 );
2039 Ok(())
2040 }
2041
2042 #[test]
2043 fn typed_refusal_discards_the_connection_before_a_dependent_follow_up() {
2044 let refusal = Err(ClientError::Server(ErrorBody {
2045 code: lodestone_wire::ErrorCode::PermissionDenied,
2046 message: "denied".to_owned(),
2047 disposition: lodestone_wire::ErrorDisposition::NotSubmitted,
2048 recovery: lodestone_wire::RecoveryAction::Stop,
2049 details: lodestone_wire::ErrorDetail::PermissionDenied {
2050 action: None,
2051 workload_class: None,
2052 },
2053 }));
2054 assert!(!response_keeps_connection(&refusal));
2055 let failure = Err(ClientError::Transport {
2056 kind: TransportErrorKind::Read,
2057 disposition: SubmissionDisposition::Indeterminate,
2058 });
2059 assert!(!response_keeps_connection(&failure));
2060 }
2061
2062 #[test]
2063 fn connection_idle_age_is_fenced_below_the_gateway_prelude_timeout() {
2064 let now = tokio::time::Instant::now();
2065 assert!(connection_idle_is_reusable(now - MAX_REUSABLE_CONNECTION_IDLE, now));
2066 assert!(!connection_idle_is_reusable(
2067 now - MAX_REUSABLE_CONNECTION_IDLE - Duration::from_millis(1),
2068 now
2069 ));
2070 }
2071
2072 #[test]
2073 fn verified_session_cache_is_bounded_and_never_crosses_principals() {
2074 let metrics = CellTransportMetrics::default();
2075 let mut cache = VerifiedSessionCache::default();
2076 let first = cache_key(1, 10, 100);
2077 let other_principal = cache_key(1, 11, 100);
2078 assert_ne!(first, other_principal);
2079 cache.insert(first.clone(), verification(1, 10), &metrics);
2080 assert!(cache.get(&other_principal, &metrics).is_none());
2081 assert_eq!(cache.get(&first, &metrics), Some(verification(1, 10)));
2082
2083 for offset in 0..=MAX_VERIFIED_SESSION_METADATA {
2084 let identity = u128::try_from(offset).unwrap_or_default().saturating_add(1_000);
2085 cache.insert(
2086 cache_key(identity, identity, identity),
2087 verification(identity, identity),
2088 &metrics,
2089 );
2090 }
2091 assert_eq!(cache.entries.len(), MAX_VERIFIED_SESSION_METADATA);
2092 assert!(metrics.session_cache_evictions.load(Ordering::Relaxed) > 0);
2093 assert_eq!(
2094 metrics.session_cache_entries.load(Ordering::Relaxed),
2095 MAX_VERIFIED_SESSION_METADATA
2096 );
2097 assert_eq!(metrics.session_cache_hits.load(Ordering::Relaxed), 1);
2098 assert_eq!(metrics.session_cache_misses.load(Ordering::Relaxed), 1);
2099 }
2100
2101 #[test]
2102 fn generated_multi_index_scope_requires_a_nonempty_distinct_set() {
2103 fn index(name: &str, incarnation: &str) -> Resource {
2104 Resource {
2105 kind: "index".to_owned(),
2106 name: name.to_owned(),
2107 incarnation: Identity {
2108 value: incarnation.to_owned(),
2109 },
2110 }
2111 }
2112
2113 fn request(indexes: Vec<Resource>) -> RequestBody {
2114 RequestBody::OpenPit(OpenPitRequest {
2115 indexes,
2116 keep_alive_millis: U64 {
2117 value: "1000".to_owned(),
2118 },
2119 })
2120 }
2121
2122 let first = index("orders", "00000000000000000000000000000001");
2123 let second = index("events", "00000000000000000000000000000002");
2124 assert!(matches!(
2125 resolve_index_name(&request(Vec::new()), None),
2126 Err(ClientError::CollectionIndexRefRequired)
2127 ));
2128 assert!(matches!(
2129 resolve_index_name(&request(vec![first.clone(), second.clone()]), None),
2130 Ok(None)
2131 ));
2132 assert!(matches!(
2133 resolve_index_name(&request(vec![first.clone(), first.clone()]), None),
2134 Err(ClientError::CollectionIndexRefMismatch)
2135 ));
2136 assert!(matches!(
2137 resolve_index_name(&request(vec![second.clone(), first.clone(), first]), None),
2138 Err(ClientError::CollectionIndexRefMismatch)
2139 ));
2140 }
2141
2142 #[test]
2143 fn generated_get_index_is_collection_scoped() {
2144 let request = RequestBody::GetIndex(GetIndexRequest {
2145 name: "events".to_owned(),
2146 expected_incarnation: None,
2147 });
2148
2149 assert!(matches!(resolve_index_name(&request, None), Ok(None)));
2150 assert!(matches!(
2151 resolve_index_name(&request, Some("events")),
2152 Err(ClientError::CollectionIndexRefMismatch)
2153 ));
2154 }
2155}