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, 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 let local_deadline = options.local_deadline()?;
601 if tokio::time::Instant::now() >= local_deadline {
602 return Err(ClientError::Transport {
603 kind: TransportErrorKind::Deadline,
604 disposition: SubmissionDisposition::NotSubmitted,
605 });
606 }
607 let command = request_command(&request);
608 let command_contract = LODESTONE_API_COMMAND_CONTRACTS
609 .iter()
610 .find(|contract| contract.command == command)
611 .ok_or(ClientError::MissingCommandContract)?;
612 let outcome_contract = LODESTONE_API_COMMAND_OUTCOMES
613 .iter()
614 .find(|contract| contract.command == command)
615 .ok_or(ClientError::MissingCommandContract)?;
616 let mutation = outcome_contract.possible_submission_states.contains(&"submitted");
617 if mutation && self.profile_verification().is_none() {
618 return Err(ClientError::ProfileNotVerified);
619 }
620 let index_name = resolve_index_name(&request, options.index_name.as_deref())?;
621 let correlation = random_identity()?;
622 let deadline = options.absolute_deadline_unix_millis()?;
623 let request_id = format!("r-{}", lower_hex(&correlation));
624 let workload_class = options
625 .workload_class
626 .unwrap_or_else(|| default_workload_class(command_contract.action));
627 let envelope = RequestEnvelope {
628 meta: RequestMeta {
629 format_version: LODESTONE_API_FORMAT_VERSION,
630 request_id: request_id.clone(),
631 correlation_id: Identity {
632 value: lower_hex(&correlation),
633 },
634 absolute_deadline_unix_millis: U64 {
635 value: deadline.to_string(),
636 },
637 workload_class,
638 },
639 request,
640 };
641 let payload = canonical_request(&envelope)?;
642 let started = Arc::new(AtomicBool::new(false));
643 let maximum_attempts = usize::from(self.pool_config().maximum_read_retries).saturating_add(1);
644 let mut attempt = 0_usize;
645 loop {
646 attempt = attempt.saturating_add(1);
647 let result = tokio::select! {
648 result = tokio::time::timeout_at(
649 local_deadline,
650 self.transport_attempt(
651 &payload,
652 &request_id,
653 command,
654 command_contract.action,
655 workload_class,
656 index_name.as_deref(),
657 correlation,
658 Arc::clone(&started),
659 ),
660 ) => match result {
661 Ok(result) => result,
662 Err(_) => Err(ClientError::Transport {
663 kind: TransportErrorKind::Deadline,
664 disposition: started_disposition(&started),
665 }),
666 },
667 () = options.cancellation.cancelled() => Err(ClientError::Transport {
668 kind: TransportErrorKind::Cancelled,
669 disposition: started_disposition(&started),
670 }),
671 };
672 match result {
673 Err(ref error)
674 if attempt < maximum_attempts
675 && !options.cancellation.is_cancelled()
676 && error.automatic_retry_admitted() =>
677 {
678 continue;
679 }
680 result => return result,
681 }
682 }
683 }
684
685 #[allow(clippy::too_many_arguments)]
686 async fn transport_attempt(
687 &self,
688 payload: &[u8],
689 request_id: &str,
690 command: Command,
691 action: Action,
692 workload_class: WorkloadClass,
693 index_name: Option<&str>,
694 correlation: [u8; 16],
695 started: Arc<AtomicBool>,
696 ) -> Result<ResultBody, ClientError> {
697 let credential = self.credential().await?;
701 let native_credential = native_credential(&credential)?;
702 let session = PublicSession {
703 format_version: PUBLIC_SESSION_FORMAT_VERSION,
704 api_key_credential: native_credential.as_slice(),
705 collection_id: &self.inner.config.profile.as_wire().expected_collection_id.value,
706 request_id,
707 action: action_name(action),
708 workload_class,
709 index_name,
710 request_correlation_id: correlation,
711 };
712 let session = SensitiveBuffer(serde_json::to_vec(&session).map_err(|_| ClientError::RequestCodec)?);
713 if session.is_empty() || session.len() > MAX_PUBLIC_SESSION_BYTES {
714 return Err(ClientError::RequestTooLarge);
715 }
716 let mut wire = SensitiveBuffer::new();
717 wire.extend_u32(session.len())?;
718 wire.extend(session.as_slice());
719 wire.extend_u32(payload.len())?;
720 wire.extend(payload);
721
722 if self.inner.transport.inner.connection_slots.available_permits() == 0 {
723 self.inner
724 .transport
725 .inner
726 .metrics
727 .bulkhead_saturation
728 .fetch_add(1, Ordering::Relaxed);
729 }
730 let _permit = self
731 .inner
732 .transport
733 .inner
734 .connection_slots
735 .clone()
736 .acquire_owned()
737 .await
738 .map_err(|_| ClientError::Transport {
739 kind: TransportErrorKind::PoolClosed,
740 disposition: SubmissionDisposition::NotSubmitted,
741 })?;
742 let endpoint_index = self.inner.transport.inner.next_endpoint.fetch_add(1, Ordering::Relaxed)
743 % self.inner.transport.inner.config.native_endpoints.len();
744 let mut connection = {
745 let mut pool = self.inner.transport.inner.pool.lock().await;
746 let now = tokio::time::Instant::now();
754 let connection = loop {
755 match pool.pop() {
756 Some(connection) if connection_idle_is_reusable(connection.idle_since, now) => {
757 break Some(connection);
758 }
759 Some(_) => {}
760 None => break None,
761 }
762 };
763 self.inner
764 .transport
765 .inner
766 .metrics
767 .idle_connections
768 .store(pool.len(), Ordering::Relaxed);
769 connection
770 };
771 if connection.is_none() {
772 connection = Some(self.connect_native(endpoint_index).await?);
773 }
774 let mut connection = connection.ok_or(ClientError::Transport {
775 kind: TransportErrorKind::Connect,
776 disposition: SubmissionDisposition::NotSubmitted,
777 })?;
778 started.store(true, Ordering::Release);
779 if connection.stream.write_all(wire.as_slice()).await.is_err() || connection.stream.flush().await.is_err() {
780 return Err(ClientError::Transport {
781 kind: TransportErrorKind::Write,
782 disposition: SubmissionDisposition::Indeterminate,
783 });
784 }
785 let response_length = connection.stream.read_u32().await.map_err(|_| ClientError::Transport {
786 kind: TransportErrorKind::Read,
787 disposition: SubmissionDisposition::Indeterminate,
788 })?;
789 let response_length = usize::try_from(response_length).map_err(|_| ClientError::ResponseTooLarge)?;
790 if response_length == 0 || response_length > MAX_NATIVE_RESPONSE_BYTES {
791 return Err(ClientError::ResponseTooLarge);
792 }
793 let mut response = vec![0; response_length];
794 connection
795 .stream
796 .read_exact(&mut response)
797 .await
798 .map_err(|_| ClientError::Transport {
799 kind: TransportErrorKind::Read,
800 disposition: SubmissionDisposition::Indeterminate,
801 })?;
802 let decoded: ResponseEnvelope = serde_json::from_slice(&response).map_err(|_| ClientError::ResponseCodec)?;
803 if serde_json::to_vec(&decoded).map_err(|_| ClientError::ResponseCodec)? != response {
804 return Err(ClientError::NonCanonicalResponse);
805 }
806 let result = match decoded {
807 ResponseEnvelope::Success { meta, result } if response_meta_matches(&meta, request_id, &correlation) => {
808 if command_for_result(&result) != command {
809 Err(ClientError::ResponseCommandMismatch)
810 } else {
811 Ok(*result)
812 }
813 }
814 ResponseEnvelope::Success { .. } => Err(ClientError::ResponseIdentityMismatch),
815 ResponseEnvelope::Error { meta, error } => {
816 if !response_meta_matches(&meta, request_id, &correlation) {
817 return Err(ClientError::ResponseIdentityMismatch);
818 }
819 validate_error_body(&error).map_err(|_| ClientError::InvalidErrorShape)?;
820 let semantic = LODESTONE_API_COMMAND_SEMANTICS
821 .iter()
822 .find(|contract| contract.command == command)
823 .ok_or(ClientError::MissingCommandContract)?;
824 if !semantic.admissible_errors.contains(&error.code) {
825 return Err(ClientError::InvalidErrorShape);
826 }
827 Err(ClientError::Server(error))
828 }
829 };
830 if response_keeps_connection(&result) {
838 let mut pool = self.inner.transport.inner.pool.lock().await;
839 if pool.len() < self.pool_config().maximum_idle_connections {
840 connection.idle_since = tokio::time::Instant::now();
841 pool.push(connection);
842 self.inner
843 .transport
844 .inner
845 .metrics
846 .idle_connections
847 .store(pool.len(), Ordering::Relaxed);
848 }
849 }
850 result
851 }
852
853 async fn connect_native(&self, endpoint_index: usize) -> Result<NativeConnection, ClientError> {
854 let endpoint = self
855 .inner
856 .transport
857 .inner
858 .config
859 .native_endpoints
860 .get(endpoint_index)
861 .ok_or(ClientError::InvalidEndpoint)?;
862 let stream = tokio::time::timeout(
863 self.pool_config().connect_timeout,
864 TcpStream::connect((endpoint.host(), endpoint.port())),
865 )
866 .await
867 .map_err(|_| ClientError::Transport {
868 kind: TransportErrorKind::Connect,
869 disposition: SubmissionDisposition::NotSubmitted,
870 })?
871 .map_err(|_| ClientError::Transport {
872 kind: TransportErrorKind::Connect,
873 disposition: SubmissionDisposition::NotSubmitted,
874 })?;
875 let server_name = ServerName::try_from(self.inner.transport.inner.config.identity.tls_server_name().to_owned())
876 .map_err(|_| ClientError::Config(ClientConfigError::InvalidServerName))?;
877 let connector = TlsConnector::from(Arc::clone(&self.inner.transport.inner.tls));
878 let stream = tokio::time::timeout(
879 self.pool_config().connect_timeout,
880 connector.connect(server_name, stream),
881 )
882 .await
883 .map_err(|_| ClientError::Transport {
884 kind: TransportErrorKind::Tls,
885 disposition: SubmissionDisposition::NotSubmitted,
886 })?
887 .map_err(|_| ClientError::Transport {
888 kind: TransportErrorKind::Tls,
889 disposition: SubmissionDisposition::NotSubmitted,
890 })?;
891 if stream.get_ref().1.alpn_protocol() != Some(LODESTONE_API_ALPN.as_bytes()) {
892 return Err(ClientError::Transport {
893 kind: TransportErrorKind::Tls,
894 disposition: SubmissionDisposition::NotSubmitted,
895 });
896 }
897 self.inner
898 .transport
899 .inner
900 .metrics
901 .live_connections
902 .fetch_add(1, Ordering::Relaxed);
903 Ok(NativeConnection {
904 stream,
905 metrics: Arc::clone(&self.inner.transport.inner.metrics),
906 idle_since: tokio::time::Instant::now(),
907 })
908 }
909
910 pub(crate) async fn credential(&self) -> Result<CredentialMaterial, ClientError> {
911 let profile = self.inner.config.profile.as_wire();
912 self.inner
913 .transport
914 .inner
915 .metrics
916 .credential_fetches
917 .fetch_add(1, Ordering::Relaxed);
918 let material = self
919 .inner
920 .config
921 .credentials
922 .provide(&profile.secret_reference)
923 .await
924 .map_err(ClientError::Credential)?;
925 if material.api_key_id() != &profile.credential_id || material.generation() != &profile.credential_generation {
926 return Err(ClientError::Credential(CredentialProviderError::InvalidMaterial));
927 }
928 Ok(material)
929 }
930
931 pub(crate) fn transport_config(&self) -> &CellTransportConfig {
932 &self.inner.transport.inner.config
933 }
934
935 pub(crate) fn pool_config(&self) -> PoolConfig {
936 self.inner.transport.pool_config()
937 }
938}
939
940fn response_keeps_connection(result: &Result<ResultBody, ClientError>) -> bool {
941 result.is_ok()
942}
943
944const MAX_REUSABLE_CONNECTION_IDLE: Duration = Duration::from_secs(1);
945
946fn connection_idle_is_reusable(idle_since: tokio::time::Instant, now: tokio::time::Instant) -> bool {
947 now.checked_duration_since(idle_since)
948 .is_some_and(|idle| idle <= MAX_REUSABLE_CONNECTION_IDLE)
949}
950
951const fn default_workload_class(action: Action) -> WorkloadClass {
952 match action {
953 Action::DataRead | Action::SubscriptionRead => WorkloadClass::InteractiveSearch,
954 Action::DataWrite | Action::Append | Action::ResolveAppend => WorkloadClass::StreamingIngest,
955 Action::IdentityDiscovery
956 | Action::IndexAdmin
957 | Action::CatalogRead
958 | Action::TaskRead
959 | Action::DiagnosticsRead
960 | Action::DiagnosticsCreate
961 | Action::SecurityRead
962 | Action::SecurityAdmin
963 | Action::AuditRead
964 | Action::QuotaRead
965 | Action::QuotaAdmin
966 | Action::BindingRead
967 | Action::LifecycleAdmin
968 | Action::AdmissionRead
969 | Action::AdmissionAdmin
970 | Action::UsageRead
971 | Action::OperationRead
972 | Action::OperationCancel
973 | Action::CollectionExport
974 | Action::CollectionRestore
975 | Action::IndexExport
976 | Action::IndexImport => WorkloadClass::BatchAnalytics,
977 }
978}
979
980#[derive(serde::Serialize)]
981struct PublicSession<'a> {
982 format_version: u16,
983 api_key_credential: &'a [u8],
984 collection_id: &'a str,
985 request_id: &'a str,
986 action: &'static str,
987 workload_class: WorkloadClass,
988 #[serde(skip_serializing_if = "Option::is_none")]
989 index_name: Option<&'a str>,
990 request_correlation_id: [u8; 16],
991}
992
993fn canonical_request(envelope: &RequestEnvelope) -> Result<Vec<u8>, ClientError> {
994 if envelope.meta.request_id.is_empty()
995 || envelope.meta.request_id.len() > MAX_REQUEST_ID_BYTES
996 || envelope.meta.request_id.bytes().any(|byte| byte.is_ascii_control())
997 {
998 return Err(ClientError::InvalidRequestIdentity);
999 }
1000 let payload = serde_json::to_vec(envelope).map_err(|_| ClientError::RequestCodec)?;
1001 if payload.is_empty() || payload.len() > MAX_NATIVE_REQUEST_BYTES {
1002 return Err(ClientError::RequestTooLarge);
1003 }
1004 let decoded: RequestEnvelope = serde_json::from_slice(&payload).map_err(|_| ClientError::RequestCodec)?;
1005 if decoded != *envelope {
1006 return Err(ClientError::RequestCodec);
1007 }
1008 Ok(payload)
1009}
1010
1011fn build_tls_config(config: &CellTransportConfig) -> Result<rustls::ClientConfig, ClientError> {
1012 let mut roots = rustls::RootCertStore::empty();
1013 for certificate in config.trust.certificates_der() {
1014 roots
1015 .add(CertificateDer::from(certificate.clone()))
1016 .map_err(|_| ClientError::Config(ClientConfigError::InvalidTrustMaterial))?;
1017 }
1018 let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
1019 let mut tls = rustls::ClientConfig::builder_with_provider(provider)
1020 .with_protocol_versions(&[&rustls::version::TLS13])
1021 .map_err(|_| ClientError::Config(ClientConfigError::InvalidTrustMaterial))?
1022 .with_root_certificates(roots)
1023 .with_no_client_auth();
1024 tls.alpn_protocols = vec![LODESTONE_API_ALPN.as_bytes().to_vec()];
1025 Ok(tls)
1026}
1027
1028fn validate_discovery(config: &CollectionSessionConfig, discovered: &DiscoverResult) -> Result<(), ClientError> {
1029 let profile = config.profile.as_wire();
1030 if discovered.collection_id != profile.expected_collection_id
1031 || discovered.principal_id != profile.expected_principal_id
1032 || discovered.schema_id != LODESTONE_API_SCHEMA_ID
1033 || discovered.schema_digest.value != LODESTONE_API_SPEC_SHA256
1034 {
1035 return Err(ClientError::ProfileIdentityMismatch);
1036 }
1037 let expected = LODESTONE_API_STREAM_CONTRACTS
1038 .first()
1039 .ok_or(ClientError::MissingCommandContract)?;
1040 if discovered.streams.len() != 1
1041 || discovered.streams.first().is_none_or(|stream| {
1042 stream.name != expected.name
1043 || stream.transport != expected.transport
1044 || stream.service != expected.service
1045 || stream.method != expected.method
1046 || stream.endpoint != profile.selected_stream_endpoint
1047 || stream.auth_metadata != expected.auth_metadata
1048 || stream.auth_scheme != expected.auth_scheme
1049 || stream.semantic_format_version != u32::from(expected.semantic_format_version)
1050 || stream.schema_digest.value != LODESTONE_API_SPEC_SHA256
1051 })
1052 {
1053 return Err(ClientError::ProfileStreamMismatch);
1054 }
1055 Ok(())
1056}
1057
1058fn validate_binding(
1059 config: &CollectionSessionConfig,
1060 binding: &CoreBinding,
1061 derived: bool,
1062 revision: &U64,
1063) -> Result<(), ClientError> {
1064 let profile = config.profile.as_wire();
1065 if !derived
1066 || binding.cluster_id != profile.expected_cluster_id
1067 || binding.collection_id != profile.expected_collection_id
1068 || binding.native_endpoints != profile.native_endpoints
1069 || binding.selected_stream_endpoint != profile.selected_stream_endpoint
1070 || binding.schema_id != profile.schema_id
1071 || binding.schema_digest != profile.schema_digest
1072 || parse_u64(&binding.configuration_generation.value).is_none_or(|generation| generation == 0)
1073 || parse_u64(&revision.value).is_none_or(|revision| revision == 0)
1074 {
1075 return Err(ClientError::ProfileIdentityMismatch);
1076 }
1077 validate_storage_profile(profile, binding, revision)?;
1078 Ok(())
1079}
1080
1081fn validate_storage_profile(
1082 profile: &lodestone_wire::ConnectionProfile,
1083 binding: &CoreBinding,
1084 binding_revision: &U64,
1085) -> Result<(), ClientError> {
1086 let attestation = &binding.storage_profile;
1087 let expected_roles = [
1088 StorageProfileRole::Control,
1089 StorageProfileRole::DefaultWal,
1090 StorageProfileRole::DefaultData,
1091 ];
1092 let roles_are_canonical = attestation.roles.len() == expected_roles.len()
1093 && attestation
1094 .roles
1095 .iter()
1096 .zip(expected_roles)
1097 .all(|(binding, expected)| binding.role == expected)
1098 && canonical_authority_groups(&attestation.roles);
1099 if attestation.format_version != u32::from(LODESTONE_API_STORAGE_PROFILE_ATTESTATION_FORMAT_VERSION)
1100 || parse_u64(&attestation.configuration_generation.value) != parse_u64(&binding.configuration_generation.value)
1101 || parse_u64(&binding_revision.value).is_none_or(|revision| revision == 0)
1102 || !roles_are_canonical
1103 || attestation.default_policy_digest.validate().is_err()
1104 || attestation.storage_profile_digest.validate().is_err()
1105 || attestation.attestation_digest.validate().is_err()
1106 {
1107 return Err(ClientError::ProfileIdentityMismatch);
1108 }
1109 if profile.storage_profile_expectation.as_ref().is_some_and(|expectation| {
1110 expectation
1111 .required_posture
1112 .is_some_and(|required| required != attestation.overall_posture)
1113 || expectation
1114 .expected_storage_profile_digest
1115 .as_ref()
1116 .is_some_and(|expected| expected != &attestation.storage_profile_digest)
1117 }) {
1118 return Err(ClientError::StorageProfileMismatch);
1119 }
1120 Ok(())
1121}
1122
1123fn canonical_authority_groups(roles: &[lodestone_wire::StorageProfileRoleBinding]) -> bool {
1124 let Some(first) = roles.first() else {
1125 return false;
1126 };
1127 if first.authority_group != 0 {
1128 return false;
1129 }
1130 let mut group_count = 1_usize;
1131 for role in roles.iter().skip(1) {
1132 let Ok(group) = usize::try_from(role.authority_group) else {
1133 return false;
1134 };
1135 if group < group_count {
1136 continue;
1137 }
1138 if group != group_count {
1139 return false;
1140 }
1141 group_count += 1;
1142 }
1143 true
1144}
1145
1146fn validate_cached_verification(
1147 config: &CollectionSessionConfig,
1148 verification: &ProfileVerification,
1149) -> Result<(), ClientError> {
1150 let profile = config.profile.as_wire();
1151 if verification.collection_id != profile.expected_collection_id
1152 || verification.principal_id != profile.expected_principal_id
1153 || verification.cluster_id != profile.expected_cluster_id
1154 || verification.schema_id != profile.schema_id
1155 || verification.schema_digest != profile.schema_digest
1156 || verification.native_endpoints != profile.native_endpoints
1157 || verification.selected_stream_endpoint != profile.selected_stream_endpoint
1158 || verification
1159 .configuration_generation
1160 .value
1161 .parse::<u64>()
1162 .ok()
1163 .is_none_or(|generation| generation == 0)
1164 || verification
1165 .binding_revision
1166 .value
1167 .parse::<u64>()
1168 .ok()
1169 .is_none_or(|revision| revision == 0)
1170 {
1171 return Err(ClientError::ProfileIdentityMismatch);
1172 }
1173 let cached_binding = CoreBinding {
1174 cluster_id: verification.cluster_id.clone(),
1175 collection_id: verification.collection_id.clone(),
1176 configuration_generation: verification.configuration_generation.clone(),
1177 native_endpoints: verification.native_endpoints.clone(),
1178 selected_stream_endpoint: verification.selected_stream_endpoint.clone(),
1179 schema_id: verification.schema_id.clone(),
1180 schema_digest: verification.schema_digest.clone(),
1181 storage_profile: verification.storage_profile.clone(),
1182 };
1183 validate_storage_profile(profile, &cached_binding, &verification.binding_revision)?;
1184 Ok(())
1185}
1186
1187fn resolve_index_name(request: &RequestBody, supplied: Option<&str>) -> Result<Option<String>, ClientError> {
1188 let scope = request_collection_index_ref(request)?;
1189 let inferred = match &scope {
1190 GeneratedCollectionIndexRef::Exact(name) => Some(name.clone()),
1191 GeneratedCollectionIndexRef::None | GeneratedCollectionIndexRef::Caller => None,
1192 };
1193 if let Some(supplied) = supplied {
1194 validate_index_name(supplied)?;
1195 if inferred.as_deref().is_some_and(|inferred| inferred != supplied) {
1196 return Err(ClientError::CollectionIndexRefMismatch);
1197 }
1198 }
1199 let resolved = supplied.map(str::to_owned).or(inferred);
1200 if let Some(name) = &resolved {
1201 validate_index_name(name)?;
1202 }
1203 match scope {
1204 GeneratedCollectionIndexRef::Caller | GeneratedCollectionIndexRef::Exact(_) if resolved.is_none() => {
1205 return Err(ClientError::CollectionIndexRefRequired);
1206 }
1207 GeneratedCollectionIndexRef::None if resolved.is_some() => return Err(ClientError::CollectionIndexRefMismatch),
1208 GeneratedCollectionIndexRef::None
1209 | GeneratedCollectionIndexRef::Caller
1210 | GeneratedCollectionIndexRef::Exact(_) => {}
1211 }
1212 Ok(resolved)
1213}
1214
1215const fn action_name(action: Action) -> &'static str {
1216 match action {
1217 Action::IdentityDiscovery => "identity_discovery",
1218 Action::DataRead => "data_read",
1219 Action::DataWrite => "data_write",
1220 Action::Append => "append",
1221 Action::ResolveAppend => "resolve_append",
1222 Action::IndexAdmin => "index_admin",
1223 Action::CatalogRead => "catalog_read",
1224 Action::TaskRead => "task_read",
1225 Action::SubscriptionRead => "subscription_read",
1226 Action::DiagnosticsRead => "diagnostics_read",
1227 Action::DiagnosticsCreate => "diagnostics_create",
1228 Action::SecurityRead => "security_read",
1229 Action::SecurityAdmin => "security_admin",
1230 Action::AuditRead => "audit_read",
1231 Action::QuotaRead => "quota_read",
1232 Action::QuotaAdmin => "quota_admin",
1233 Action::BindingRead => "binding_read",
1234 Action::LifecycleAdmin => "lifecycle_admin",
1235 Action::AdmissionRead => "admission_read",
1236 Action::AdmissionAdmin => "admission_admin",
1237 Action::UsageRead => "usage_read",
1238 Action::OperationRead => "operation_read",
1239 Action::OperationCancel => "operation_cancel",
1240 Action::CollectionExport => "collection_export",
1241 Action::CollectionRestore => "collection_restore",
1242 Action::IndexExport => "index_export",
1243 Action::IndexImport => "index_import",
1244 }
1245}
1246
1247fn native_credential(material: &CredentialMaterial) -> Result<SensitiveBuffer, ClientError> {
1248 let identifier = parse_identity(&material.api_key_id().value)
1249 .ok_or(ClientError::Credential(CredentialProviderError::InvalidMaterial))?;
1250 let mut output = SensitiveBuffer::new();
1251 output.extend(identifier.to_string().as_bytes());
1252 output.extend(b":");
1253 output.extend(&base64_url_no_pad(material.secret().expose()));
1254 Ok(output)
1255}
1256
1257pub(crate) fn authorization_value(material: &CredentialMaterial) -> Result<SensitiveBuffer, ClientError> {
1258 let native = native_credential(material)?;
1259 let mut output = SensitiveBuffer::new();
1260 output.extend(b"ApiKey ");
1261 output.extend(&base64_standard(native.as_slice()));
1262 Ok(output)
1263}
1264
1265fn base64_url_no_pad(bytes: &[u8]) -> Vec<u8> {
1266 base64_encode(
1267 bytes,
1268 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",
1269 false,
1270 )
1271}
1272
1273fn base64_standard(bytes: &[u8]) -> Vec<u8> {
1274 base64_encode(
1275 bytes,
1276 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
1277 true,
1278 )
1279}
1280
1281pub fn raw_json_text(text: &str) -> Result<RawJson, ClientError> {
1284 let bytes_base64 = String::from_utf8(base64_standard(text.as_bytes())).map_err(|_| ClientError::RequestCodec)?;
1285 Ok(RawJson { bytes_base64 })
1286}
1287
1288fn base64_encode(bytes: &[u8], alphabet: &[u8; 64], padded: bool) -> Vec<u8> {
1289 let mut output = Vec::with_capacity(bytes.len().div_ceil(3).saturating_mul(4));
1290 for chunk in bytes.chunks(3) {
1291 let first = chunk[0];
1292 let second = chunk.get(1).copied().unwrap_or(0);
1293 let third = chunk.get(2).copied().unwrap_or(0);
1294 output.push(alphabet[usize::from(first >> 2)]);
1295 output.push(alphabet[usize::from(((first & 0x03) << 4) | (second >> 4))]);
1296 if chunk.len() > 1 {
1297 output.push(alphabet[usize::from(((second & 0x0f) << 2) | (third >> 6))]);
1298 } else if padded {
1299 output.push(b'=');
1300 }
1301 if chunk.len() > 2 {
1302 output.push(alphabet[usize::from(third & 0x3f)]);
1303 } else if padded {
1304 output.push(b'=');
1305 }
1306 }
1307 output
1308}
1309
1310fn random_identity() -> Result<[u8; 16], ClientError> {
1311 let mut output = [0; 16];
1312 getrandom::fill(&mut output).map_err(|_| ClientError::RandomUnavailable)?;
1313 if output == [0; 16] {
1314 return Err(ClientError::RandomUnavailable);
1315 }
1316 Ok(output)
1317}
1318
1319fn normalized_timeout(timeout: Duration) -> Result<Duration, ClientError> {
1320 if timeout.is_zero() || timeout > MAX_CALL_TIMEOUT {
1321 return Err(ClientError::InvalidCallOptions);
1322 }
1323 let mut millis = timeout.as_millis();
1324 if !timeout.subsec_nanos().is_multiple_of(1_000_000) {
1325 millis = millis.checked_add(1).ok_or(ClientError::InvalidCallOptions)?;
1326 }
1327 let millis = u64::try_from(millis).map_err(|_| ClientError::InvalidCallOptions)?;
1328 let normalized = Duration::from_millis(millis);
1329 if normalized > MAX_CALL_TIMEOUT {
1330 return Err(ClientError::InvalidCallOptions);
1331 }
1332 Ok(normalized)
1333}
1334
1335fn duration_millis_ceil(duration: Duration) -> Result<u64, ClientError> {
1336 let mut millis = duration.as_millis();
1337 if !duration.subsec_nanos().is_multiple_of(1_000_000) {
1338 millis = millis.checked_add(1).ok_or(ClientError::ClockUnavailable)?;
1339 }
1340 u64::try_from(millis).map_err(|_| ClientError::ClockUnavailable)
1341}
1342
1343pub(crate) fn validate_index_name(value: &str) -> Result<(), ClientError> {
1344 if value.is_empty()
1345 || value.len() > 255
1346 || value
1347 .bytes()
1348 .any(|byte| byte.is_ascii_control() || byte.is_ascii_whitespace())
1349 {
1350 return Err(ClientError::InvalidCollectionIndexRef);
1351 }
1352 Ok(())
1353}
1354
1355fn lower_hex(bytes: &[u8]) -> String {
1356 const HEX: &[u8; 16] = b"0123456789abcdef";
1357 let mut output = String::with_capacity(bytes.len().saturating_mul(2));
1358 for byte in bytes {
1359 output.push(char::from(HEX[usize::from(byte >> 4)]));
1360 output.push(char::from(HEX[usize::from(byte & 0x0f)]));
1361 }
1362 output
1363}
1364
1365pub(crate) struct SensitiveBuffer(Vec<u8>);
1366
1367impl SensitiveBuffer {
1368 fn new() -> Self {
1369 Self(Vec::new())
1370 }
1371
1372 fn extend(&mut self, bytes: &[u8]) {
1373 self.0.extend_from_slice(bytes);
1374 }
1375
1376 fn extend_u32(&mut self, value: usize) -> Result<(), ClientError> {
1377 let value = u32::try_from(value).map_err(|_| ClientError::RequestTooLarge)?;
1378 self.extend(&value.to_be_bytes());
1379 Ok(())
1380 }
1381
1382 fn is_empty(&self) -> bool {
1383 self.0.is_empty()
1384 }
1385
1386 fn len(&self) -> usize {
1387 self.0.len()
1388 }
1389
1390 pub(crate) fn as_slice(&self) -> &[u8] {
1391 &self.0
1392 }
1393}
1394
1395impl Drop for SensitiveBuffer {
1396 fn drop(&mut self) {
1397 self.0.fill(0);
1398 std::hint::black_box(&mut self.0);
1399 }
1400}
1401
1402fn started_disposition(started: &AtomicBool) -> SubmissionDisposition {
1403 if started.load(Ordering::Acquire) {
1404 SubmissionDisposition::Indeterminate
1405 } else {
1406 SubmissionDisposition::NotSubmitted
1407 }
1408}
1409
1410#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1411pub enum SubmissionDisposition {
1412 NotSubmitted,
1413 Submitted,
1414 Resolved,
1415 KnownRetired,
1416 Indeterminate,
1417}
1418
1419#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1420pub enum TransportErrorKind {
1421 PoolClosed,
1422 Connect,
1423 Tls,
1424 Write,
1425 Read,
1426 Deadline,
1427 Cancelled,
1428}
1429
1430#[derive(Debug)]
1431pub enum ClientError {
1432 Config(ClientConfigError),
1433 Credential(CredentialProviderError),
1434 InvalidCallOptions,
1435 InvalidEndpoint,
1436 InvalidCollectionIndexRef,
1437 InvalidStateIndex,
1438 InvalidStateActionId,
1439 InvalidStateKey,
1440 InvalidStateBatch,
1441 UnexpectedStateResult,
1442 CollectionIndexRefRequired,
1443 CollectionIndexRefMismatch,
1444 ProfileNotVerified,
1445 ProfileIdentityMismatch,
1446 ProfileStreamMismatch,
1447 StorageProfileMismatch,
1448 MissingCommandContract,
1449 RequestCodec,
1450 RequestTooLarge,
1451 InvalidRequestIdentity,
1452 ResponseCodec,
1453 ResponseTooLarge,
1454 NonCanonicalResponse,
1455 ResponseIdentityMismatch,
1456 ResponseCommandMismatch,
1457 InvalidErrorShape,
1458 RandomUnavailable,
1459 ClockUnavailable,
1460 InternalStateUnavailable,
1461 Transport {
1462 kind: TransportErrorKind,
1463 disposition: SubmissionDisposition,
1464 },
1465 Server(ErrorBody),
1466}
1467
1468impl ClientError {
1469 #[must_use]
1470 pub fn submission_disposition(&self) -> SubmissionDisposition {
1471 match self {
1472 Self::Transport { disposition, .. } => *disposition,
1473 Self::Server(error) => server_error_disposition(error),
1474 Self::UnexpectedStateResult
1475 | Self::ResponseCodec
1476 | Self::ResponseTooLarge
1477 | Self::NonCanonicalResponse
1478 | Self::ResponseIdentityMismatch
1479 | Self::ResponseCommandMismatch
1480 | Self::InvalidErrorShape => SubmissionDisposition::Indeterminate,
1481 _ => SubmissionDisposition::NotSubmitted,
1482 }
1483 }
1484
1485 #[must_use]
1489 pub fn automatic_retry_admitted(&self) -> bool {
1490 matches!(
1491 self.retry_directive(),
1492 Some((
1493 ErrorDisposition::NotSubmitted,
1494 RecoveryAction::Retry | RecoveryAction::RetryAfter
1495 ))
1496 )
1497 }
1498
1499 fn retry_directive(&self) -> Option<(ErrorDisposition, RecoveryAction)> {
1500 match self {
1501 Self::Server(error) => Some((error.disposition, error.recovery)),
1502 Self::Transport {
1503 kind: TransportErrorKind::Deadline | TransportErrorKind::Cancelled,
1504 ..
1505 } => Some((ErrorDisposition::NotSubmitted, RecoveryAction::Stop)),
1506 Self::Transport {
1507 disposition: SubmissionDisposition::NotSubmitted,
1508 ..
1509 } => Some((ErrorDisposition::NotSubmitted, RecoveryAction::Retry)),
1510 Self::Transport { .. } => Some((ErrorDisposition::Indeterminate, RecoveryAction::Resolve)),
1511 _ => None,
1512 }
1513 }
1514
1515 pub(crate) fn is_deadline_exceeded(&self) -> bool {
1516 matches!(
1517 self,
1518 Self::Transport {
1519 kind: TransportErrorKind::Deadline,
1520 ..
1521 } | Self::Server(ErrorBody {
1522 code: lodestone_wire::ErrorCode::DeadlineExceeded,
1523 ..
1524 })
1525 )
1526 }
1527}
1528
1529fn server_error_disposition(error: &ErrorBody) -> SubmissionDisposition {
1530 match error.disposition {
1531 lodestone_wire::ErrorDisposition::NotSubmitted => SubmissionDisposition::NotSubmitted,
1532 lodestone_wire::ErrorDisposition::Submitted => SubmissionDisposition::Submitted,
1533 lodestone_wire::ErrorDisposition::Resolved => SubmissionDisposition::Resolved,
1534 lodestone_wire::ErrorDisposition::KnownRetired => SubmissionDisposition::KnownRetired,
1535 lodestone_wire::ErrorDisposition::Indeterminate => SubmissionDisposition::Indeterminate,
1536 }
1537}
1538
1539fn response_meta_matches(meta: &lodestone_wire::ResponseMeta, request_id: &str, correlation: &[u8; 16]) -> bool {
1540 meta.format_version == u32::from(LODESTONE_API_RESPONSE_META_FORMAT_VERSION)
1541 && meta.request_id == request_id
1542 && meta.correlation_id.value == lower_hex(correlation)
1543 && canonical_identity(&meta.diagnostic_id.value)
1544}
1545
1546fn canonical_identity(value: &str) -> bool {
1547 value.len() == 32
1548 && value
1549 .bytes()
1550 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
1551 && value.bytes().any(|byte| byte != b'0')
1552}
1553
1554impl From<ClientConfigError> for ClientError {
1555 fn from(error: ClientConfigError) -> Self {
1556 Self::Config(error)
1557 }
1558}
1559
1560impl Display for ClientError {
1561 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
1562 formatter.write_str(match self {
1563 Self::Config(_) => "Core client configuration is invalid",
1564 Self::Credential(_) => "Core credential material is unavailable or mismatched",
1565 Self::InvalidCallOptions => "Core call options are invalid",
1566 Self::InvalidEndpoint => "Core endpoint is invalid",
1567 Self::InvalidCollectionIndexRef => "Core request index scope is invalid",
1568 Self::InvalidStateIndex => "Lodestone state index identity is invalid",
1569 Self::InvalidStateActionId => "Lodestone state action identity is invalid",
1570 Self::InvalidStateKey => "Lodestone state key must contain 1 to 254 bytes",
1571 Self::InvalidStateBatch => "Lodestone state key batch is empty or exceeds its bound",
1572 Self::UnexpectedStateResult => "Lodestone state result differs from the requested operation",
1573 Self::CollectionIndexRefRequired => "Core request requires an exact index scope",
1574 Self::CollectionIndexRefMismatch => "Core request and admission index scopes differ",
1575 Self::ProfileNotVerified => "Core mutation is refused until discovery and binding are verified",
1576 Self::ProfileIdentityMismatch => "Core discovery or binding differs from the connection profile",
1577 Self::ProfileStreamMismatch => "Core selected stream differs from the connection profile",
1578 Self::StorageProfileMismatch => "Core storage profile differs from the connection profile expectation",
1579 Self::MissingCommandContract => "generated Core command metadata is incomplete",
1580 Self::RequestCodec => "Core request is not a canonical current Lodestone API value",
1581 Self::RequestTooLarge => "Core request exceeds its generated byte bound",
1582 Self::InvalidRequestIdentity => "Core request identity is invalid",
1583 Self::ResponseCodec => "Core response is malformed or unsupported",
1584 Self::ResponseTooLarge => "Core response exceeds its generated byte bound",
1585 Self::NonCanonicalResponse => "Core response encoding is noncanonical",
1586 Self::ResponseIdentityMismatch => "Core response changed the request identity or version",
1587 Self::ResponseCommandMismatch => "Core response returned another command result",
1588 Self::InvalidErrorShape => "Core error detail does not match its generated code",
1589 Self::RandomUnavailable => "secure request identity generation is unavailable",
1590 Self::ClockUnavailable => "absolute Core request deadline cannot be calculated",
1591 Self::InternalStateUnavailable => "Core client verification state is unavailable",
1592 Self::Transport { .. } => "Core transport failed",
1593 Self::Server(_) => "Core returned a typed request error",
1594 })
1595 }
1596}
1597
1598impl Error for ClientError {}
1599
1600#[cfg(test)]
1601mod tests {
1602 use std::error::Error;
1603
1604 use lodestone_wire::{GetIndexRequest, Identity, OpenPitRequest, Resource, U64};
1605
1606 use super::*;
1607 use crate::SecretBytes;
1608
1609 #[test]
1610 fn response_failures_preserve_indeterminate_submission() {
1611 for error in [
1612 ClientError::UnexpectedStateResult,
1613 ClientError::ResponseCodec,
1614 ClientError::ResponseTooLarge,
1615 ClientError::NonCanonicalResponse,
1616 ClientError::ResponseIdentityMismatch,
1617 ClientError::ResponseCommandMismatch,
1618 ClientError::InvalidErrorShape,
1619 ] {
1620 assert_eq!(error.submission_disposition(), SubmissionDisposition::Indeterminate);
1621 }
1622 assert_eq!(
1623 ClientError::RequestCodec.submission_disposition(),
1624 SubmissionDisposition::NotSubmitted
1625 );
1626 }
1627
1628 #[test]
1629 fn response_metadata_requires_exact_echo_and_nonzero_canonical_diagnostic() {
1630 let correlation = [0x12; 16];
1631 let mut meta = lodestone_wire::ResponseMeta {
1632 format_version: u32::from(LODESTONE_API_RESPONSE_META_FORMAT_VERSION),
1633 request_id: "request-1".to_owned(),
1634 correlation_id: Identity {
1635 value: lower_hex(&correlation),
1636 },
1637 diagnostic_id: Identity { value: "34".repeat(16) },
1638 };
1639 assert!(response_meta_matches(&meta, "request-1", &correlation));
1640
1641 meta.correlation_id.value = "56".repeat(16);
1642 assert!(!response_meta_matches(&meta, "request-1", &correlation));
1643 meta.correlation_id.value = lower_hex(&correlation);
1644
1645 meta.diagnostic_id.value = "00".repeat(16);
1646 assert!(!response_meta_matches(&meta, "request-1", &correlation));
1647 meta.diagnostic_id.value = "AB".repeat(16);
1648 assert!(!response_meta_matches(&meta, "request-1", &correlation));
1649 meta.diagnostic_id.value = "34".repeat(16);
1650
1651 meta.request_id = "request-2".to_owned();
1652 assert!(!response_meta_matches(&meta, "request-1", &correlation));
1653 meta.request_id = "request-1".to_owned();
1654 meta.format_version = meta.format_version.saturating_add(1);
1655 assert!(!response_meta_matches(&meta, "request-1", &correlation));
1656 }
1657
1658 fn cache_key(collection: u128, principal: u128, credential: u128) -> VerifiedSessionCacheKey {
1659 VerifiedSessionCacheKey {
1660 collection_id: format!("{collection:032x}"),
1661 principal_id: format!("{principal:032x}"),
1662 profile_generation: "1".to_owned(),
1663 credential_id: format!("{credential:032x}"),
1664 credential_generation: "1".to_owned(),
1665 secret_reference: format!("memory://session/{collection}/{principal}/{credential}"),
1666 }
1667 }
1668
1669 fn verification(collection: u128, principal: u128) -> ProfileVerification {
1670 ProfileVerification {
1671 artifact_version: "development".to_owned(),
1672 principal_id: Identity {
1673 value: format!("{principal:032x}"),
1674 },
1675 collection_id: Identity {
1676 value: format!("{collection:032x}"),
1677 },
1678 policy_revision: U64 { value: "1".to_owned() },
1679 cluster_id: Identity {
1680 value: "00000000000000000000000000000001".to_owned(),
1681 },
1682 configuration_generation: U64 { value: "1".to_owned() },
1683 binding_revision: U64 { value: "1".to_owned() },
1684 schema_id: LODESTONE_API_SCHEMA_ID.to_owned(),
1685 schema_digest: lodestone_wire::Digest {
1686 value: LODESTONE_API_SPEC_SHA256.to_owned(),
1687 },
1688 native_endpoints: vec!["lodestone+tls://core.example:8443".to_owned()],
1689 selected_stream_endpoint: "https://core.example:9443".to_owned(),
1690 storage_profile: test_storage_profile(),
1691 }
1692 }
1693
1694 fn test_storage_profile() -> StorageProfileAttestation {
1695 StorageProfileAttestation {
1696 format_version: u32::from(LODESTONE_API_STORAGE_PROFILE_ATTESTATION_FORMAT_VERSION),
1697 configuration_generation: U64 { value: "1".to_owned() },
1698 overall_posture: lodestone_wire::StorageOverallPosture::FilesystemDevelopment,
1699 roles: vec![
1700 lodestone_wire::StorageProfileRoleBinding {
1701 role: StorageProfileRole::Control,
1702 authority_group: 0,
1703 provider: lodestone_wire::StorageProviderPosture::FilesystemDevelopment,
1704 service_class: lodestone_wire::StorageBackendServiceClass::RegionalObject,
1705 },
1706 lodestone_wire::StorageProfileRoleBinding {
1707 role: StorageProfileRole::DefaultWal,
1708 authority_group: 0,
1709 provider: lodestone_wire::StorageProviderPosture::FilesystemDevelopment,
1710 service_class: lodestone_wire::StorageBackendServiceClass::RegionalObject,
1711 },
1712 lodestone_wire::StorageProfileRoleBinding {
1713 role: StorageProfileRole::DefaultData,
1714 authority_group: 0,
1715 provider: lodestone_wire::StorageProviderPosture::FilesystemDevelopment,
1716 service_class: lodestone_wire::StorageBackendServiceClass::RegionalObject,
1717 },
1718 ],
1719 default_durability: lodestone_wire::ResourceDurabilityClass::RegionalObject,
1720 default_storage: lodestone_wire::ResourceStorageClass::Standard,
1721 default_policy_digest: lodestone_wire::Digest { value: "11".repeat(32) },
1722 storage_profile_digest: lodestone_wire::Digest { value: "22".repeat(32) },
1723 attestation_digest: lodestone_wire::Digest { value: "33".repeat(32) },
1724 }
1725 }
1726
1727 fn test_session_with_storage_expectation(
1728 posture: Option<lodestone_wire::StorageOverallPosture>,
1729 digest: Option<&str>,
1730 ) -> Result<CollectionSessionConfig, Box<dyn Error>> {
1731 let profile = crate::ValidatedConnectionProfile::new(lodestone_wire::ConnectionProfile {
1732 format_version: u32::from(lodestone_wire::LODESTONE_API_CONNECTION_PROFILE_FORMAT_VERSION),
1733 profile_generation: U64 { value: "1".to_owned() },
1734 native_endpoints: vec!["lodestone+tls://core.example:8443".to_owned()],
1735 selected_stream_endpoint: "https://core.example:9443".to_owned(),
1736 tls_server_name: "core.example".to_owned(),
1737 tls_trust_sha256: lodestone_wire::Digest { value: "44".repeat(32) },
1738 expected_cluster_id: Identity {
1739 value: "00000000000000000000000000000001".to_owned(),
1740 },
1741 expected_collection_id: Identity {
1742 value: "00000000000000000000000000000002".to_owned(),
1743 },
1744 expected_principal_id: Identity {
1745 value: "00000000000000000000000000000003".to_owned(),
1746 },
1747 schema_id: LODESTONE_API_SCHEMA_ID.to_owned(),
1748 schema_digest: lodestone_wire::Digest {
1749 value: LODESTONE_API_SPEC_SHA256.to_owned(),
1750 },
1751 credential_id: Identity {
1752 value: "00000000000000000000000000000004".to_owned(),
1753 },
1754 credential_generation: U64 { value: "1".to_owned() },
1755 credential_expires_at: None,
1756 secret_reference: "memory://storage-profile-test".to_owned(),
1757 storage_profile_expectation: Some(lodestone_wire::StorageProfileExpectation {
1758 format_version: u32::from(lodestone_wire::LODESTONE_API_STORAGE_PROFILE_EXPECTATION_FORMAT_VERSION),
1759 required_posture: posture,
1760 expected_storage_profile_digest: digest.map(|value| lodestone_wire::Digest {
1761 value: value.to_owned(),
1762 }),
1763 }),
1764 })?;
1765 Ok(CollectionSessionConfig {
1766 profile,
1767 credentials: Arc::new(crate::FileCredentialProvider),
1768 })
1769 }
1770
1771 fn test_binding(storage_profile: StorageProfileAttestation) -> CoreBinding {
1772 CoreBinding {
1773 cluster_id: Identity {
1774 value: "00000000000000000000000000000001".to_owned(),
1775 },
1776 collection_id: Identity {
1777 value: "00000000000000000000000000000002".to_owned(),
1778 },
1779 configuration_generation: U64 { value: "1".to_owned() },
1780 native_endpoints: vec!["lodestone+tls://core.example:8443".to_owned()],
1781 selected_stream_endpoint: "https://core.example:9443".to_owned(),
1782 schema_id: LODESTONE_API_SCHEMA_ID.to_owned(),
1783 schema_digest: lodestone_wire::Digest {
1784 value: LODESTONE_API_SPEC_SHA256.to_owned(),
1785 },
1786 storage_profile,
1787 }
1788 }
1789
1790 #[test]
1791 fn storage_profile_expectations_fail_before_session_installation() -> Result<(), Box<dyn Error>> {
1792 let correct = test_session_with_storage_expectation(
1793 Some(lodestone_wire::StorageOverallPosture::FilesystemDevelopment),
1794 Some(&"22".repeat(32)),
1795 )?;
1796 let binding = test_binding(test_storage_profile());
1797 let revision = U64 { value: "1".to_owned() };
1798 validate_binding(&correct, &binding, true, &revision)?;
1799
1800 let wrong_posture = test_session_with_storage_expectation(
1801 Some(lodestone_wire::StorageOverallPosture::ProviderObjectStorage),
1802 None,
1803 )?;
1804 assert!(matches!(
1805 validate_binding(&wrong_posture, &binding, true, &revision),
1806 Err(ClientError::StorageProfileMismatch)
1807 ));
1808 assert_eq!(
1809 ClientError::StorageProfileMismatch.submission_disposition(),
1810 SubmissionDisposition::NotSubmitted
1811 );
1812
1813 let wrong_digest = test_session_with_storage_expectation(None, Some(&"55".repeat(32)))?;
1814 assert!(matches!(
1815 validate_binding(&wrong_digest, &binding, true, &revision),
1816 Err(ClientError::StorageProfileMismatch)
1817 ));
1818 Ok(())
1819 }
1820
1821 #[test]
1822 fn malformed_storage_profile_shape_is_an_identity_mismatch() -> Result<(), Box<dyn Error>> {
1823 let config = test_session_with_storage_expectation(
1824 Some(lodestone_wire::StorageOverallPosture::FilesystemDevelopment),
1825 None,
1826 )?;
1827 let revision = U64 { value: "1".to_owned() };
1828 let mut malformed = test_storage_profile();
1829 malformed.roles.swap(0, 1);
1830 assert!(matches!(
1831 validate_binding(&config, &test_binding(malformed), true, &revision),
1832 Err(ClientError::ProfileIdentityMismatch)
1833 ));
1834
1835 let mut malformed = test_storage_profile();
1836 malformed.roles[1].authority_group = 2;
1837 assert!(matches!(
1838 validate_binding(&config, &test_binding(malformed), true, &revision),
1839 Err(ClientError::ProfileIdentityMismatch)
1840 ));
1841
1842 let mut malformed = test_storage_profile();
1843 malformed.configuration_generation = U64 { value: "2".to_owned() };
1844 assert!(matches!(
1845 validate_binding(&config, &test_binding(malformed), true, &revision),
1846 Err(ClientError::ProfileIdentityMismatch)
1847 ));
1848 Ok(())
1849 }
1850
1851 #[test]
1852 fn automatic_retry_uses_only_typed_disposition_and_recovery() {
1853 let transport = |disposition| ClientError::Transport {
1854 kind: TransportErrorKind::Read,
1855 disposition,
1856 };
1857 assert!(transport(SubmissionDisposition::NotSubmitted).automatic_retry_admitted());
1858 assert!(!transport(SubmissionDisposition::Submitted).automatic_retry_admitted());
1859 assert!(!transport(SubmissionDisposition::Indeterminate).automatic_retry_admitted());
1860
1861 let server = |disposition, recovery| {
1862 ClientError::Server(ErrorBody {
1863 code: lodestone_wire::ErrorCode::TemporarilyUnavailable,
1864 message: "typed retry probe".to_owned(),
1865 disposition,
1866 recovery,
1867 details: lodestone_wire::ErrorDetail::TemporarilyUnavailable {
1868 retry_after_millis: None,
1869 },
1870 })
1871 };
1872 assert!(server(ErrorDisposition::NotSubmitted, RecoveryAction::Retry).automatic_retry_admitted());
1873 assert!(server(ErrorDisposition::NotSubmitted, RecoveryAction::RetryAfter).automatic_retry_admitted());
1874 assert!(!server(ErrorDisposition::Indeterminate, RecoveryAction::Resolve).automatic_retry_admitted());
1875 assert!(!server(ErrorDisposition::NotSubmitted, RecoveryAction::Stop).automatic_retry_admitted());
1876 }
1877
1878 #[test]
1879 fn one_deadline_survives_connect_state_and_resolution_clones() -> Result<(), Box<dyn Error>> {
1880 let options = CallOptions::new(Duration::from_secs(5))?;
1881 let absolute = options.absolute_deadline_unix_millis()?;
1882 let local = options.local_deadline()?;
1883
1884 for stage in [
1887 options.clone(),
1888 options.clone(),
1889 options.clone(),
1890 options.clone(),
1891 options.for_resolution(),
1892 ] {
1893 assert_eq!(stage.absolute_deadline_unix_millis()?, absolute);
1894 assert_eq!(stage.local_deadline()?, local);
1895 }
1896
1897 let resolution = options.for_resolution();
1898 options.cancellation().cancel();
1899 assert!(options.cancellation().is_cancelled());
1900 assert!(!resolution.cancellation().is_cancelled());
1901 assert_eq!(resolution.absolute_deadline_unix_millis()?, absolute);
1902 Ok(())
1903 }
1904
1905 #[test]
1906 fn caller_supplied_absolute_deadline_is_preserved_exactly() -> Result<(), Box<dyn Error>> {
1907 let now = SystemTime::now().duration_since(UNIX_EPOCH)?;
1908 let now_millis = u64::try_from(now.as_millis())?;
1909 let absolute = now_millis.checked_add(5_000).ok_or("test deadline overflow")?;
1910 let options = CallOptions::from_absolute_deadline_unix_millis(absolute)?;
1911 assert_eq!(options.absolute_deadline_unix_millis()?, absolute);
1912
1913 let too_far = now_millis
1914 .checked_add(u64::try_from(MAX_CALL_TIMEOUT.as_millis())?)
1915 .and_then(|value| value.checked_add(1_000))
1916 .ok_or("test deadline overflow")?;
1917 assert!(matches!(
1918 CallOptions::from_absolute_deadline_unix_millis(too_far),
1919 Err(ClientError::InvalidCallOptions)
1920 ));
1921 assert_eq!(normalized_timeout(Duration::from_nanos(1))?, Duration::from_millis(1));
1922 Ok(())
1923 }
1924
1925 #[test]
1926 fn private_api_key_encodings_match_the_core_admission_contract() -> Result<(), Box<dyn Error>> {
1927 let material = CredentialMaterial::new(
1928 Identity {
1929 value: "00000000000000000000000000000001".to_owned(),
1930 },
1931 U64 { value: "1".to_owned() },
1932 SecretBytes::from_array([0; 32]),
1933 )?;
1934 assert_eq!(
1935 native_credential(&material)?.as_slice(),
1936 b"1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
1937 );
1938 assert_eq!(
1939 authorization_value(&material)?.as_slice(),
1940 b"ApiKey MTpBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB"
1941 );
1942 Ok(())
1943 }
1944
1945 #[test]
1946 fn typed_refusal_discards_the_connection_before_a_dependent_follow_up() {
1947 let refusal = Err(ClientError::Server(ErrorBody {
1948 code: lodestone_wire::ErrorCode::PermissionDenied,
1949 message: "denied".to_owned(),
1950 disposition: lodestone_wire::ErrorDisposition::NotSubmitted,
1951 recovery: lodestone_wire::RecoveryAction::Stop,
1952 details: lodestone_wire::ErrorDetail::PermissionDenied {
1953 action: None,
1954 workload_class: None,
1955 },
1956 }));
1957 assert!(!response_keeps_connection(&refusal));
1958 let failure = Err(ClientError::Transport {
1959 kind: TransportErrorKind::Read,
1960 disposition: SubmissionDisposition::Indeterminate,
1961 });
1962 assert!(!response_keeps_connection(&failure));
1963 }
1964
1965 #[test]
1966 fn connection_idle_age_is_fenced_below_the_gateway_prelude_timeout() {
1967 let now = tokio::time::Instant::now();
1968 assert!(connection_idle_is_reusable(now - MAX_REUSABLE_CONNECTION_IDLE, now));
1969 assert!(!connection_idle_is_reusable(
1970 now - MAX_REUSABLE_CONNECTION_IDLE - Duration::from_millis(1),
1971 now
1972 ));
1973 }
1974
1975 #[test]
1976 fn verified_session_cache_is_bounded_and_never_crosses_principals() {
1977 let metrics = CellTransportMetrics::default();
1978 let mut cache = VerifiedSessionCache::default();
1979 let first = cache_key(1, 10, 100);
1980 let other_principal = cache_key(1, 11, 100);
1981 assert_ne!(first, other_principal);
1982 cache.insert(first.clone(), verification(1, 10), &metrics);
1983 assert!(cache.get(&other_principal, &metrics).is_none());
1984 assert_eq!(cache.get(&first, &metrics), Some(verification(1, 10)));
1985
1986 for offset in 0..=MAX_VERIFIED_SESSION_METADATA {
1987 let identity = u128::try_from(offset).unwrap_or_default().saturating_add(1_000);
1988 cache.insert(
1989 cache_key(identity, identity, identity),
1990 verification(identity, identity),
1991 &metrics,
1992 );
1993 }
1994 assert_eq!(cache.entries.len(), MAX_VERIFIED_SESSION_METADATA);
1995 assert!(metrics.session_cache_evictions.load(Ordering::Relaxed) > 0);
1996 assert_eq!(
1997 metrics.session_cache_entries.load(Ordering::Relaxed),
1998 MAX_VERIFIED_SESSION_METADATA
1999 );
2000 assert_eq!(metrics.session_cache_hits.load(Ordering::Relaxed), 1);
2001 assert_eq!(metrics.session_cache_misses.load(Ordering::Relaxed), 1);
2002 }
2003
2004 #[test]
2005 fn generated_multi_index_scope_requires_a_nonempty_distinct_set() {
2006 fn index(name: &str, incarnation: &str) -> Resource {
2007 Resource {
2008 kind: "index".to_owned(),
2009 name: name.to_owned(),
2010 incarnation: Identity {
2011 value: incarnation.to_owned(),
2012 },
2013 }
2014 }
2015
2016 fn request(indexes: Vec<Resource>) -> RequestBody {
2017 RequestBody::OpenPit(OpenPitRequest {
2018 indexes,
2019 keep_alive_millis: U64 {
2020 value: "1000".to_owned(),
2021 },
2022 })
2023 }
2024
2025 let first = index("orders", "00000000000000000000000000000001");
2026 let second = index("events", "00000000000000000000000000000002");
2027 assert!(matches!(
2028 resolve_index_name(&request(Vec::new()), None),
2029 Err(ClientError::CollectionIndexRefRequired)
2030 ));
2031 assert!(matches!(
2032 resolve_index_name(&request(vec![first.clone(), second.clone()]), None),
2033 Ok(None)
2034 ));
2035 assert!(matches!(
2036 resolve_index_name(&request(vec![first.clone(), first.clone()]), None),
2037 Err(ClientError::CollectionIndexRefMismatch)
2038 ));
2039 assert!(matches!(
2040 resolve_index_name(&request(vec![second.clone(), first.clone(), first]), None),
2041 Err(ClientError::CollectionIndexRefMismatch)
2042 ));
2043 }
2044
2045 #[test]
2046 fn generated_get_index_is_collection_scoped() {
2047 let request = RequestBody::GetIndex(GetIndexRequest {
2048 name: "events".to_owned(),
2049 expected_incarnation: None,
2050 });
2051
2052 assert!(matches!(resolve_index_name(&request, None), Ok(None)));
2053 assert!(matches!(
2054 resolve_index_name(&request, Some("events")),
2055 Err(ClientError::CollectionIndexRefMismatch)
2056 ));
2057 }
2058}