Skip to main content

starweaver_session/
environment.rs

1//! Product-neutral durable environment attachment and mount contracts.
2//!
3//! These records deliberately contain only public environment identities and reviewed display
4//! projections. Provider endpoints, credentials, and private resource source references belong to
5//! the product-side resolver and must never be placed in these values.
6
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9
10use crate::{
11    DurableHostEventClass, DurableHostEventScope, MutationReceipt, PendingHostEventPublication,
12    SessionStoreError, SessionStoreResult,
13};
14
15/// Maximum page size accepted by durable attachment queries, matching the host schema.
16pub const MAX_ENVIRONMENT_PAGE_SIZE: u32 = 200;
17/// Maximum active mounts retained for one run, matching the host result bound.
18pub const MAX_ENVIRONMENT_MOUNTS_PER_RUN: u32 = 128;
19/// Stable operation identifier for attachment creation.
20pub const ENVIRONMENT_ATTACH_OPERATION: &str = "environment.attach";
21/// Stable operation identifier for attachment detachment.
22pub const ENVIRONMENT_DETACH_OPERATION: &str = "environment.detach";
23/// Stable operation identifier for mounting an allowlisted resource.
24pub const ENVIRONMENT_MOUNT_OPERATION: &str = "environment.mount";
25/// Stable operation identifier for unmounting a resource.
26pub const ENVIRONMENT_UNMOUNT_OPERATION: &str = "environment.unmount";
27
28/// Product-neutral attachment scope.
29#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
30#[serde(tag = "kind", rename_all = "snake_case")]
31pub enum DurableEnvironmentScope {
32    /// Scope follows one transport-owned connection and is never transferable.
33    Connection {
34        /// Opaque host-generated connection identity.
35        connection_id: String,
36    },
37    /// Scope follows one durable session.
38    Session {
39        /// Session identity.
40        session_id: String,
41    },
42    /// Scope follows one durable run.
43    Run {
44        /// Session identity.
45        session_id: String,
46        /// Run identity.
47        run_id: String,
48    },
49}
50
51impl DurableEnvironmentScope {
52    /// Validate scope identities.
53    ///
54    /// # Errors
55    ///
56    /// Returns an error when a scoped session or run identity is empty.
57    pub fn validate(&self) -> SessionStoreResult<()> {
58        match self {
59            Self::Connection { connection_id } => {
60                require_non_empty("environment scope connection", connection_id)
61            }
62            Self::Session { session_id } => {
63                require_non_empty("environment scope session", session_id)
64            }
65            Self::Run { session_id, run_id } => {
66                require_non_empty("environment scope session", session_id)?;
67                require_non_empty("environment scope run", run_id)
68            }
69        }
70    }
71
72    /// Return whether this scope permits use by the specified run.
73    #[must_use]
74    pub fn permits_run(&self, connection_id: Option<&str>, session_id: &str, run_id: &str) -> bool {
75        match self {
76            Self::Connection {
77                connection_id: owner,
78            } => connection_id == Some(owner.as_str()),
79            Self::Session { session_id: owner } => owner == session_id,
80            Self::Run {
81                session_id: owner_session,
82                run_id: owner_run,
83            } => owner_session == session_id && owner_run == run_id,
84        }
85    }
86}
87
88/// Durable attachment lifecycle state.
89#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
90#[serde(rename_all = "snake_case")]
91pub enum DurableEnvironmentStatus {
92    /// Provider is being prepared.
93    Attaching,
94    /// Provider is ready for use.
95    Ready,
96    /// Provider is available with reduced capability.
97    Degraded,
98    /// Attachment is terminally detached.
99    Detached,
100}
101
102/// Safe durable projection of an environment attachment.
103#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
104pub struct DurableEnvironmentAttachment {
105    /// Trusted authority binding owning the record and its idempotency namespace.
106    pub authority_binding: String,
107    /// Stable attachment identity.
108    pub attachment_id: String,
109    /// Public, configured environment catalog identity.
110    pub environment_id: String,
111    /// Optional reviewed display label; never a provider endpoint or source reference.
112    pub display_name: Option<String>,
113    /// Attachment scope.
114    pub scope: DurableEnvironmentScope,
115    /// Lifecycle state.
116    pub status: DurableEnvironmentStatus,
117    /// Monotonic record revision, beginning at one.
118    pub revision: u64,
119    /// Last committed mutation time.
120    pub updated_at: DateTime<Utc>,
121}
122
123impl starweaver_core::VersionedRecord for DurableEnvironmentAttachment {
124    const SCHEMA: &'static str = "starweaver.session.environment_attachment";
125}
126
127impl DurableEnvironmentAttachment {
128    /// Validate persisted attachment invariants.
129    ///
130    /// # Errors
131    ///
132    /// Returns an error when an identity, display label, scope, or revision is invalid.
133    pub fn validate(&self) -> SessionStoreResult<()> {
134        require_non_empty("environment authority binding", &self.authority_binding)?;
135        require_non_empty("environment attachment id", &self.attachment_id)?;
136        require_non_empty("public environment id", &self.environment_id)?;
137        if self.display_name.as_ref().is_some_and(String::is_empty) {
138            return Err(SessionStoreError::Failed(
139                "environment display name cannot be empty".to_string(),
140            ));
141        }
142        self.scope.validate()?;
143        require_revision(self.revision, "environment attachment")
144    }
145}
146
147/// Durable mount lifecycle state.
148#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
149#[serde(rename_all = "snake_case")]
150pub enum DurableEnvironmentMountStatus {
151    /// Resource is actively mounted.
152    Mounted,
153    /// Resource is terminally unmounted.
154    Unmounted,
155}
156
157/// Safe durable projection of one mounted resource.
158#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
159pub struct DurableEnvironmentMount {
160    /// Trusted authority binding owning the mount.
161    pub authority_binding: String,
162    /// Stable mount identity.
163    pub mount_id: String,
164    /// Owning attachment identity.
165    pub attachment_id: String,
166    /// Owning session identity.
167    pub session_id: String,
168    /// Owning run identity.
169    pub run_id: String,
170    /// Reviewed label resolved from a product-private resource allowlist.
171    pub resource_label: String,
172    /// Mount lifecycle state.
173    pub status: DurableEnvironmentMountStatus,
174    /// Monotonic record revision, beginning at one.
175    pub revision: u64,
176    /// Last committed mutation time.
177    pub updated_at: DateTime<Utc>,
178}
179
180impl starweaver_core::VersionedRecord for DurableEnvironmentMount {
181    const SCHEMA: &'static str = "starweaver.session.environment_mount";
182}
183
184impl DurableEnvironmentMount {
185    /// Validate persisted mount invariants.
186    ///
187    /// # Errors
188    ///
189    /// Returns an error when an identity, safe resource label, or revision is invalid.
190    pub fn validate(&self) -> SessionStoreResult<()> {
191        require_non_empty("environment authority binding", &self.authority_binding)?;
192        require_non_empty("environment mount id", &self.mount_id)?;
193        require_non_empty("environment attachment id", &self.attachment_id)?;
194        require_non_empty("environment mount session", &self.session_id)?;
195        require_non_empty("environment mount run", &self.run_id)?;
196        require_non_empty("environment resource label", &self.resource_label)?;
197        require_revision(self.revision, "environment mount")
198    }
199}
200
201/// Stable identity and visibility scope for an environment event projected at commit time.
202#[derive(Clone, Debug, Eq, PartialEq)]
203pub struct EnvironmentHostEventContext {
204    /// Stable identity of the logical environment transition across retries.
205    pub transition_identity: String,
206    /// Durable visibility scope for the resulting event.
207    pub scope: DurableHostEventScope,
208}
209
210impl EnvironmentHostEventContext {
211    /// Validate stable event identity and scope.
212    ///
213    /// # Errors
214    ///
215    /// Returns an error when the transition or a scoped durable identity is empty.
216    pub fn validate(&self) -> SessionStoreResult<()> {
217        require_non_empty(
218            "environment host event transition identity",
219            &self.transition_identity,
220        )?;
221        match &self.scope {
222            DurableHostEventScope::Global => Ok(()),
223            DurableHostEventScope::Session { session_id } => {
224                require_non_empty("environment host event session", session_id.as_str())
225            }
226            DurableHostEventScope::Run { session_id, run_id } => {
227                require_non_empty("environment host event session", session_id.as_str())?;
228                require_non_empty("environment host event run", run_id.as_str())
229            }
230        }
231    }
232
233    /// Project one transaction-final attachment into its exact durable host event.
234    ///
235    /// # Errors
236    ///
237    /// Returns an error when the context, attachment, or projected publication is invalid.
238    pub fn publication(
239        &self,
240        attachment: &DurableEnvironmentAttachment,
241    ) -> SessionStoreResult<PendingHostEventPublication> {
242        self.validate()?;
243        attachment.validate()?;
244        let attachment_scope = match &attachment.scope {
245            DurableEnvironmentScope::Connection { .. } => {
246                serde_json::json!({"kind": "connection"})
247            }
248            DurableEnvironmentScope::Session { session_id } => {
249                serde_json::json!({"kind": "session", "sessionId": session_id})
250            }
251            DurableEnvironmentScope::Run { session_id, run_id } => serde_json::json!({
252                "kind": "run",
253                "sessionId": session_id,
254                "runId": run_id,
255            }),
256        };
257        let status = match attachment.status {
258            DurableEnvironmentStatus::Attaching => "attaching",
259            DurableEnvironmentStatus::Ready => "ready",
260            DurableEnvironmentStatus::Degraded => "degraded",
261            DurableEnvironmentStatus::Detached => "detached",
262        };
263        let mut projected_attachment = serde_json::Map::from_iter([
264            (
265                "attachmentId".to_string(),
266                serde_json::Value::String(attachment.attachment_id.clone()),
267            ),
268            (
269                "environmentId".to_string(),
270                serde_json::Value::String(attachment.environment_id.clone()),
271            ),
272            (
273                "revision".to_string(),
274                serde_json::Value::String(attachment.revision.to_string()),
275            ),
276            ("scope".to_string(), attachment_scope),
277            (
278                "status".to_string(),
279                serde_json::Value::String(status.to_string()),
280            ),
281        ]);
282        if let Some(display_name) = &attachment.display_name {
283            projected_attachment.insert(
284                "displayName".to_string(),
285                serde_json::Value::String(display_name.clone()),
286            );
287        }
288        PendingHostEventPublication::new(
289            &self.transition_identity,
290            0,
291            self.scope.clone(),
292            DurableHostEventClass::EnvironmentChanged,
293            serde_json::json!({
294                "kind": "environment_changed",
295                "attachment": serde_json::Value::Object(projected_attachment),
296            }),
297            attachment.updated_at,
298        )
299    }
300}
301
302/// Shared authority, idempotency, timestamp, and event evidence for an environment mutation.
303#[derive(Clone, Debug, Eq, PartialEq)]
304pub struct EnvironmentMutationContext {
305    /// Trusted authority binding owning state and the idempotency namespace.
306    pub authority_binding: String,
307    /// Idempotency key scoped to `authority_binding` across environment operations.
308    pub idempotency_key: String,
309    /// Canonical fingerprint of the normalized, authorized command.
310    pub command_fingerprint: String,
311    /// Authoritative commit timestamp.
312    pub occurred_at: DateTime<Utc>,
313    /// Optional stable event context projected from transaction-final attachment state.
314    pub host_event: Option<EnvironmentHostEventContext>,
315}
316
317impl EnvironmentMutationContext {
318    /// Validate shared mutation evidence.
319    ///
320    /// # Errors
321    ///
322    /// Returns an error when authority, idempotency, fingerprint, or event evidence is invalid.
323    pub fn validate(&self) -> SessionStoreResult<()> {
324        require_non_empty("environment authority binding", &self.authority_binding)?;
325        require_non_empty("environment idempotency key", &self.idempotency_key)?;
326        require_non_empty("environment command fingerprint", &self.command_fingerprint)?;
327        if let Some(host_event) = &self.host_event {
328            host_event.validate()?;
329        }
330        Ok(())
331    }
332}
333
334/// Atomic attachment creation command. All provider-private resolution has already occurred.
335#[derive(Clone, Debug, Eq, PartialEq)]
336pub struct AttachEnvironment {
337    /// Shared mutation evidence.
338    pub context: EnvironmentMutationContext,
339    /// Host-generated stable attachment identity.
340    pub attachment_id: String,
341    /// Public configured environment identity.
342    pub environment_id: String,
343    /// Reviewed public display label.
344    pub display_name: Option<String>,
345    /// Authorized attachment scope.
346    pub scope: DurableEnvironmentScope,
347    /// Initial resolved provider status.
348    pub status: DurableEnvironmentStatus,
349}
350
351impl AttachEnvironment {
352    /// Validate command evidence.
353    ///
354    /// # Errors
355    ///
356    /// Returns an error when shared mutation evidence or a command identity is invalid.
357    pub fn validate(&self) -> SessionStoreResult<()> {
358        self.context.validate()?;
359        require_non_empty("environment attachment id", &self.attachment_id)?;
360        require_non_empty("public environment id", &self.environment_id)?;
361        if self.display_name.as_ref().is_some_and(String::is_empty) {
362            return Err(SessionStoreError::Failed(
363                "environment display name cannot be empty".to_string(),
364            ));
365        }
366        if self.status == DurableEnvironmentStatus::Detached {
367            return Err(SessionStoreError::Failed(
368                "new environment attachment cannot be detached".to_string(),
369            ));
370        }
371        self.scope.validate()
372    }
373}
374
375/// Atomic attachment detachment command.
376#[derive(Clone, Debug, Eq, PartialEq)]
377pub struct DetachEnvironment {
378    /// Shared mutation evidence.
379    pub context: EnvironmentMutationContext,
380    /// Attachment identity.
381    pub attachment_id: String,
382}
383
384impl DetachEnvironment {
385    /// Validate command evidence.
386    ///
387    /// # Errors
388    ///
389    /// Returns an error when shared mutation evidence or a command identity is invalid.
390    pub fn validate(&self) -> SessionStoreResult<()> {
391        self.context.validate()?;
392        require_non_empty("environment attachment id", &self.attachment_id)
393    }
394}
395
396/// Atomic allowlisted-resource mount command.
397#[derive(Clone, Debug, Eq, PartialEq)]
398pub struct MountEnvironmentResource {
399    /// Shared mutation evidence.
400    pub context: EnvironmentMutationContext,
401    /// Host-generated mount identity.
402    pub mount_id: String,
403    /// Ready attachment identity.
404    pub attachment_id: String,
405    /// Owning session.
406    pub session_id: String,
407    /// Owning run.
408    pub run_id: String,
409    /// Current trusted connection identity when consuming connection-scoped authority.
410    pub connection_id: Option<String>,
411    /// Reviewed safe label from the product-private resource resolver.
412    pub resource_label: String,
413}
414
415impl MountEnvironmentResource {
416    /// Validate command evidence.
417    ///
418    /// # Errors
419    ///
420    /// Returns an error when shared mutation evidence or a command identity is invalid.
421    pub fn validate(&self) -> SessionStoreResult<()> {
422        self.context.validate()?;
423        require_non_empty("environment mount id", &self.mount_id)?;
424        require_non_empty("environment attachment id", &self.attachment_id)?;
425        require_non_empty("environment mount session", &self.session_id)?;
426        require_non_empty("environment mount run", &self.run_id)?;
427        if self.connection_id.as_ref().is_some_and(String::is_empty) {
428            return Err(SessionStoreError::Failed(
429                "environment mount connection identity cannot be empty".to_string(),
430            ));
431        }
432        require_non_empty("environment resource label", &self.resource_label)
433    }
434}
435
436/// Atomic resource unmount command.
437#[derive(Clone, Debug, Eq, PartialEq)]
438pub struct UnmountEnvironmentResource {
439    /// Shared mutation evidence.
440    pub context: EnvironmentMutationContext,
441    /// Mount identity.
442    pub mount_id: String,
443}
444
445impl UnmountEnvironmentResource {
446    /// Validate command evidence.
447    ///
448    /// # Errors
449    ///
450    /// Returns an error when shared mutation evidence or a command identity is invalid.
451    pub fn validate(&self) -> SessionStoreResult<()> {
452        self.context.validate()?;
453        require_non_empty("environment mount id", &self.mount_id)
454    }
455}
456
457/// Durable result of an attachment mutation.
458#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
459pub struct EnvironmentAttachmentMutationResult {
460    /// Authoritative post-mutation attachment.
461    pub attachment: DurableEnvironmentAttachment,
462    /// Durable mutation receipt.
463    pub receipt: MutationReceipt,
464}
465
466impl starweaver_core::VersionedRecord for EnvironmentAttachmentMutationResult {
467    const SCHEMA: &'static str = "starweaver.session.environment_attachment_mutation_result";
468}
469
470impl EnvironmentAttachmentMutationResult {
471    /// Validate receipt and state linkage.
472    ///
473    /// # Errors
474    ///
475    /// Returns an error when state, receipt, operation, or target evidence disagrees.
476    pub fn validate(&self) -> SessionStoreResult<()> {
477        self.attachment.validate()?;
478        self.receipt.validate()?;
479        if self.receipt.target_ref != self.attachment.attachment_id {
480            return Err(SessionStoreError::Conflict(
481                "environment receipt target does not match attachment".to_string(),
482            ));
483        }
484        Ok(())
485    }
486
487    /// Return an exact-replay projection.
488    #[must_use]
489    pub fn replayed_projection(&self) -> Self {
490        Self {
491            attachment: self.attachment.clone(),
492            receipt: self.receipt.replayed_projection(),
493        }
494    }
495}
496
497/// Durable result of a mount mutation.
498#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
499pub struct EnvironmentMountMutationResult {
500    /// Authoritative post-mutation mount.
501    pub mount: DurableEnvironmentMount,
502    /// Durable mutation receipt.
503    pub receipt: MutationReceipt,
504}
505
506impl starweaver_core::VersionedRecord for EnvironmentMountMutationResult {
507    const SCHEMA: &'static str = "starweaver.session.environment_mount_mutation_result";
508}
509
510impl EnvironmentMountMutationResult {
511    /// Validate receipt and state linkage.
512    ///
513    /// # Errors
514    ///
515    /// Returns an error when state, receipt, operation, or target evidence disagrees.
516    pub fn validate(&self) -> SessionStoreResult<()> {
517        self.mount.validate()?;
518        self.receipt.validate()?;
519        if self.receipt.target_ref != self.mount.mount_id {
520            return Err(SessionStoreError::Conflict(
521                "environment receipt target does not match mount".to_string(),
522            ));
523        }
524        Ok(())
525    }
526
527    /// Return an exact-replay projection.
528    #[must_use]
529    pub fn replayed_projection(&self) -> Self {
530        Self {
531            mount: self.mount.clone(),
532            receipt: self.receipt.replayed_projection(),
533        }
534    }
535}
536
537/// Typed payload stored in the shared environment idempotency namespace.
538#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
539#[serde(tag = "result_kind", rename_all = "snake_case")]
540pub enum EnvironmentMutationResult {
541    /// Attachment attach/detach result.
542    Attachment(EnvironmentAttachmentMutationResult),
543    /// Resource mount/unmount result.
544    Mount(EnvironmentMountMutationResult),
545}
546
547impl starweaver_core::VersionedRecord for EnvironmentMutationResult {
548    const SCHEMA: &'static str = "starweaver.session.environment_mutation_result";
549}
550
551/// Stable keyset position for attachment queries.
552#[derive(Clone, Debug, Eq, PartialEq)]
553pub struct EnvironmentAttachmentPageKey {
554    /// Last seen update time.
555    pub updated_at: DateTime<Utc>,
556    /// Last seen stable identity.
557    pub attachment_id: String,
558}
559
560/// Bounded authority-scoped attachment query.
561#[derive(Clone, Debug, Eq, PartialEq)]
562pub struct EnvironmentAttachmentQuery {
563    /// Trusted authority binding.
564    pub authority_binding: String,
565    /// Optional exact scope filter.
566    pub scope: Option<DurableEnvironmentScope>,
567    /// Trusted viewer connection identity used to hide foreign connection scopes.
568    pub connection_id: Option<String>,
569    /// Requested page size, from one through [`MAX_ENVIRONMENT_PAGE_SIZE`].
570    pub limit: u32,
571    /// Exclusive descending keyset cursor.
572    pub after: Option<EnvironmentAttachmentPageKey>,
573}
574
575impl EnvironmentAttachmentQuery {
576    /// Validate query bounds and identities.
577    ///
578    /// # Errors
579    ///
580    /// Returns an error when authority, scope, cursor, target, or page bounds are invalid.
581    pub fn validate(&self) -> SessionStoreResult<()> {
582        require_non_empty("environment authority binding", &self.authority_binding)?;
583        require_page_limit(self.limit)?;
584        if let Some(scope) = &self.scope {
585            scope.validate()?;
586            if let DurableEnvironmentScope::Connection { connection_id } = scope
587                && self.connection_id.as_deref() != Some(connection_id)
588            {
589                return Err(SessionStoreError::NotFound(
590                    "environment connection scope".to_string(),
591                ));
592            }
593        }
594        if self.connection_id.as_ref().is_some_and(String::is_empty) {
595            return Err(SessionStoreError::Failed(
596                "environment viewer connection identity cannot be empty".to_string(),
597            ));
598        }
599        if let Some(after) = &self.after {
600            require_non_empty("environment attachment page key", &after.attachment_id)?;
601        }
602        Ok(())
603    }
604}
605
606/// Stable bounded attachment page.
607#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
608pub struct EnvironmentAttachmentPage {
609    /// Items ordered by `(updated_at DESC, attachment_id DESC)`.
610    pub items: Vec<DurableEnvironmentAttachment>,
611    /// Exclusive key for the next page.
612    pub next: Option<EnvironmentAttachmentPageKeyProjection>,
613}
614
615/// Serializable form of an attachment page key.
616#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
617pub struct EnvironmentAttachmentPageKeyProjection {
618    /// Last seen update time.
619    pub updated_at: DateTime<Utc>,
620    /// Last seen stable identity.
621    pub attachment_id: String,
622}
623
624impl From<EnvironmentAttachmentPageKeyProjection> for EnvironmentAttachmentPageKey {
625    fn from(value: EnvironmentAttachmentPageKeyProjection) -> Self {
626        Self {
627            updated_at: value.updated_at,
628            attachment_id: value.attachment_id,
629        }
630    }
631}
632
633/// Bounded stable active-mount query for one run.
634#[derive(Clone, Debug, Eq, PartialEq)]
635pub struct EnvironmentMountQuery {
636    /// Trusted authority binding.
637    pub authority_binding: String,
638    /// Owning session.
639    pub session_id: String,
640    /// Owning run.
641    pub run_id: String,
642    /// Requested maximum, from one through [`MAX_ENVIRONMENT_PAGE_SIZE`].
643    pub limit: u32,
644}
645
646impl EnvironmentMountQuery {
647    /// Validate query bounds and identities.
648    ///
649    /// # Errors
650    ///
651    /// Returns an error when authority, scope, cursor, target, or page bounds are invalid.
652    pub fn validate(&self) -> SessionStoreResult<()> {
653        require_non_empty("environment authority binding", &self.authority_binding)?;
654        require_non_empty("environment mount session", &self.session_id)?;
655        require_non_empty("environment mount run", &self.run_id)?;
656        require_page_limit(self.limit)
657    }
658}
659
660fn require_page_limit(limit: u32) -> SessionStoreResult<()> {
661    if !(1..=MAX_ENVIRONMENT_PAGE_SIZE).contains(&limit) {
662        return Err(SessionStoreError::Failed(format!(
663            "environment page limit must be between 1 and {MAX_ENVIRONMENT_PAGE_SIZE}"
664        )));
665    }
666    Ok(())
667}
668
669fn require_revision(revision: u64, label: &str) -> SessionStoreResult<()> {
670    if revision == 0 {
671        return Err(SessionStoreError::Failed(format!(
672            "{label} revision must be greater than zero"
673        )));
674    }
675    Ok(())
676}
677
678fn require_non_empty(label: &str, value: &str) -> SessionStoreResult<()> {
679    if value.is_empty() {
680        return Err(SessionStoreError::Failed(format!(
681            "{label} cannot be empty"
682        )));
683    }
684    Ok(())
685}