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