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