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