Skip to main content

sentinelpass_protocol/
service.rs

1//! Application-service contract (WBS-408, ADR-007).
2//!
3//! This module is the single call shape every daemon client uses for vault
4//! operations: one request enum ([`VaultOp`]), one result enum
5//! ([`VaultOpResult`]), and one typed error ([`ServiceError`]). UI, CLI, and
6//! native-host clients reach vault data ONLY by sending
7//! `IpcMessage::ServiceCall` over IPC and receiving
8//! `IpcMessage::ServiceResult`; the daemon is the sole DEK owner and vault
9//! writer (ADR-007).
10//!
11//! Layering: this crate must not depend on `sentinelpass-core`, so the
12//! service surface uses its own wire DTOs ([`ServiceEntry`],
13//! [`ServiceEntrySummary`], ...). Conversions to/from the core types live in
14//! `sentinelpass_core::daemon::service`.
15//!
16//! Compatibility: every new field must be `#[serde(default)]` so an older
17//! client's frames still parse on a newer daemon and vice versa (same rule
18//! as the rest of the protocol crate).
19
20use serde::{Deserialize, Serialize};
21use zeroize::Zeroizing;
22
23/// Wire DTO for one vault entry. `password` is plaintext on the IPC surface
24/// — the same trust level as the pre-existing `GetCredential` response — and
25/// is `Zeroizing` on both ends. `Debug` redacts the password.
26#[derive(Clone, Serialize, Deserialize)]
27pub struct ServiceEntry {
28    #[serde(default)]
29    pub entry_id: Option<i64>,
30    pub title: String,
31    pub username: String,
32    #[serde(default)]
33    pub password: Zeroizing<String>,
34    #[serde(default)]
35    pub url: Option<String>,
36    #[serde(default)]
37    pub notes: Option<String>,
38    /// `password | api_key | passkey_reference` (core `CredentialType`).
39    #[serde(default = "default_credential_type")]
40    pub credential_type: String,
41    /// Unix epoch seconds.
42    #[serde(default)]
43    pub created_at: i64,
44    /// Unix epoch seconds.
45    #[serde(default)]
46    pub modified_at: i64,
47    #[serde(default)]
48    pub favorite: bool,
49}
50
51impl std::fmt::Debug for ServiceEntry {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        f.debug_struct("ServiceEntry")
54            .field("entry_id", &self.entry_id)
55            .field("title", &self.title)
56            .field("username", &self.username)
57            .field("password", &"[REDACTED]")
58            .field("url", &self.url)
59            .field("notes", &self.notes)
60            .field("credential_type", &self.credential_type)
61            .field("created_at", &self.created_at)
62            .field("modified_at", &self.modified_at)
63            .field("favorite", &self.favorite)
64            .finish()
65    }
66}
67
68fn default_credential_type() -> String {
69    "password".to_string()
70}
71
72/// Wire DTO for one entry summary (no password — bulk listings).
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct ServiceEntrySummary {
75    pub entry_id: i64,
76    pub title: String,
77    pub username: String,
78    pub credential_type: String,
79    pub favorite: bool,
80}
81
82/// Wire DTO for TOTP metadata.
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct ServiceTotpMetadata {
85    pub algorithm: String,
86    pub digits: u8,
87    pub period: u32,
88    pub issuer: Option<String>,
89    pub account_name: Option<String>,
90}
91
92/// Wire DTO for an SSH key (decrypted view; `private_key` present only when
93/// explicitly requested and authorized). `Debug` redacts the private key.
94#[derive(Clone, Serialize, Deserialize)]
95pub struct ServiceSshKey {
96    pub key_id: i64,
97    pub name: String,
98    pub comment: Option<String>,
99    pub key_type: String,
100    pub public_key: String,
101    #[serde(default)]
102    pub private_key: Option<Zeroizing<String>>,
103    pub fingerprint: String,
104    pub created_at: i64,
105}
106
107impl std::fmt::Debug for ServiceSshKey {
108    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109        f.debug_struct("ServiceSshKey")
110            .field("key_id", &self.key_id)
111            .field("name", &self.name)
112            .field("comment", &self.comment)
113            .field("key_type", &self.key_type)
114            .field("public_key", &self.public_key)
115            .field(
116                "private_key",
117                &self.private_key.as_ref().map(|_| "[REDACTED]"),
118            )
119            .field("fingerprint", &self.fingerprint)
120            .field("created_at", &self.created_at)
121            .finish()
122    }
123}
124
125/// Wire DTO for an SSH key listing row.
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct ServiceSshKeySummary {
128    pub key_id: i64,
129    pub name: String,
130    pub comment: Option<String>,
131    pub key_type: String,
132    pub fingerprint: String,
133}
134
135/// Wire DTO for one registry entity.
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct ServiceEntity {
138    pub entity_id: String,
139    pub name: String,
140    /// Entity kind label (core `EntityKind` as_str).
141    pub kind: String,
142    /// Criticality label (core `Criticality` as_str).
143    pub criticality: String,
144    pub notes: Option<String>,
145    pub rotation_interval_days_override: Option<i64>,
146    pub created_at: i64,
147    pub modified_at: i64,
148}
149
150/// Wire DTO for one sync device row.
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct ServiceSyncDeviceInfo {
153    pub device_id: String,
154    pub device_name: String,
155    pub device_type: String,
156    pub revoked: bool,
157}
158
159/// Wire DTO for vault status as seen by the daemon.
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct ServiceVaultStatus {
162    pub unlocked: bool,
163    /// Master-password rotation generation (0 = unknown/locked).
164    pub key_epoch: i64,
165    /// True while the daemon serves only bootstrap/maintenance operations
166    /// (no vault exists yet). serde default keeps older clients parsing.
167    #[serde(default)]
168    pub maintenance: bool,
169}
170
171/// Wire DTO for sync status (`VaultOp::SyncStatus`).
172#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct ServiceSyncStatus {
174    pub enabled: bool,
175    pub device_id: Option<String>,
176    pub device_name: Option<String>,
177    pub relay_url: Option<String>,
178    pub last_sync_at: Option<i64>,
179    pub pending_changes: u64,
180    /// Objects with a stored concurrent-edit alternative awaiting user
181    /// resolution (WBS-611; serde default keeps older clients parsing).
182    #[serde(default)]
183    pub conflicts: u64,
184}
185
186/// Wire DTO for biometric unlock status.
187#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct ServiceBiometricStatus {
189    pub method_name: String,
190    pub available: bool,
191    pub enrolled: bool,
192    /// Valid on a LOCKED vault too (metadata read — the UI asks this before
193    /// offering the biometric unlock button).
194    pub configured: bool,
195}
196
197/// One application-service request.
198///
199/// This is the single call shape for vault operations. Most ops execute
200/// against the live vault on the blocking pool via
201/// `sentinelpass_core::daemon::service::LiveVaultService`; the daemon's
202/// async dispatcher owns `SyncNow` (relay HTTP). `SyncPairStart` /
203/// `SyncPairJoin` are NOT served by the daemon in this release: pairing is
204/// an exclusive OFFLINE maintenance flow run by the CLI under the
205/// maintenance lock (pair-join creates local vaults — creation the daemon's
206/// live surface must not perform); over IPC they fail with the typed
207/// `op_not_served` code.
208///
209/// `Debug` is hand-written and redacts every field: this enum carries
210/// master passwords, TOTP seeds, and private keys, and derived Debug would
211/// print them into logs or panic payloads (core sets the same redaction
212/// convention).
213#[derive(Clone, Serialize, Deserialize)]
214pub enum VaultOp {
215    // --- lifecycle -------------------------------------------------------
216    /// Create a vault. Valid only while the daemon is in maintenance mode
217    /// (no vault exists); the daemon holds the exclusive maintenance lock
218    /// (WBS-501/503).
219    VaultCreate {
220        master_password: Zeroizing<String>,
221    },
222    VaultStatus,
223
224    // --- entries ---------------------------------------------------------
225    EntryAdd {
226        entry: ServiceEntry,
227    },
228    EntryGet {
229        entry_id: i64,
230    },
231    EntryList,
232    EntryUpdate {
233        entry_id: i64,
234        entry: ServiceEntry,
235    },
236    EntryDelete {
237        entry_id: i64,
238    },
239
240    // --- TOTP ------------------------------------------------------------
241    /// The secret is RAW BASE32 only: `otpauth://` URI parsing stays
242    /// client-side (both the CLI and the UI parse the URI themselves and
243    /// send the derived fields).
244    TotpAdd {
245        entry_id: i64,
246        secret: Zeroizing<String>,
247        algorithm: Option<String>,
248        digits: Option<u8>,
249        period: Option<u32>,
250        issuer: Option<String>,
251        account_name: Option<String>,
252    },
253    TotpCode {
254        entry_id: i64,
255    },
256    TotpMetadata {
257        entry_id: i64,
258    },
259    TotpRemove {
260        entry_id: i64,
261    },
262
263    // --- SSH keys ---------------------------------------------------------
264    SshKeyAdd {
265        name: String,
266        comment: Option<String>,
267        key_type: String,
268        public_key: String,
269        private_key: Zeroizing<String>,
270        fingerprint: String,
271    },
272    SshKeyList,
273    SshKeyGet {
274        key_id: i64,
275        include_private: bool,
276    },
277    SshKeyDelete {
278        key_id: i64,
279    },
280
281    // --- credential registry (ADR-001) ------------------------------------
282    /// `include_strength: true` decrypts and scores every eligible secret —
283    /// bounded by the 30s session deadline on very large vaults (stage-6
284    /// review F5: documented cap).
285    RegistryOverview {
286        include_strength: bool,
287    },
288    RegistrySweep,
289    EntityList,
290    EntityAdd {
291        name: String,
292        kind: String,
293        criticality: String,
294        notes: Option<String>,
295        rotation_interval_days: Option<i64>,
296    },
297    EntityDelete {
298        name: String,
299    },
300    EntryAssign {
301        entry_id: i64,
302        entity: String,
303        label: Option<String>,
304    },
305    EntryUnassign {
306        entry_id: i64,
307    },
308    EntryMarkRotated {
309        entry_id: i64,
310    },
311    EntrySetExpiresAt {
312        entry_id: i64,
313        expires_at: Option<i64>,
314    },
315
316    // --- health / audit ----------------------------------------------------
317    /// Vault password health report (summary + per-entry findings), as
318    /// JSON. DECRYPTS EVERY ENTRY server-side — the 30s session deadline
319    /// bounds the response on very large vaults (stage-6 review F5:
320    /// documented cap).
321    HealthReport,
322    /// Verify the audit hash chain (WBS-415), as JSON.
323    AuditVerify,
324
325    // --- biometric ----------------------------------------------------------
326    BiometricStatusGet,
327    BiometricEnable {
328        master_password: Zeroizing<String>,
329    },
330    BiometricDisable,
331
332    // --- import/export -------------------------------------------------------
333    /// Decrypted dump of every EXPORTABLE entry (generic passwords and API
334    /// keys — `passkey_reference` entries are excluded, matching every
335    /// built-in export path). The client renders JSON/CSV/KeePass locally.
336    ExportAll,
337    /// Bulk insert from an import file parse. Returns created ids.
338    ImportEntries {
339        entries: Vec<ServiceEntry>,
340    },
341
342    // --- sync (daemon-executed; local vault writes) --------------------------
343    SyncInit {
344        relay_url: String,
345        device_name: Option<String>,
346    },
347    SyncDisable,
348    SyncDeviceList,
349    SyncDeviceRevoke {
350        device_id: String,
351    },
352    /// Sync status (local metadata read).
353    SyncStatus,
354    /// Run a full push+pull cycle. Daemon-async: the sync engine awaits
355    /// relay HTTP, so the daemon's async dispatcher executes this op.
356    SyncNow,
357    /// List dead-lettered sync mutations (metadata only; WBS-607).
358    SyncDeadLetterList,
359    /// Claim the AUTHORITATIVE migration for the configured origin vault
360    /// (WBS-624): the relay mints a fresh vault and records the claim. A
361    /// second claim for the same origin is refused. Requires the relay
362    /// network (daemon-async like SyncNow).
363    SyncMigrateClaim,
364    /// Re-baseline THIS device as the migration authority for the given
365    /// fresh relay vault: resets every object's sync bookkeeping so the
366    /// full local baseline re-uploads as fresh creates. Local vault write.
367    SyncMigrateAuthoritative {
368        new_relay_vault: String,
369    },
370    /// List stored concurrent-edit conflicts (metadata only; WBS-611 /
371    /// SR-SYNC-005): the payload VALUES require an unlocked vault and are
372    /// not part of this listing.
373    SyncConflictList,
374    /// Resolve a stored concurrent-edit conflict: `take_remote = false`
375    /// keeps the local content (re-versioned above the peer so the next
376    /// push lands); `true` applies the stored alternative. Requires the
377    /// vault unlocked (take-remote decrypts and re-seals).
378    SyncConflictResolve {
379        object_id: String,
380        take_remote: bool,
381    },
382    /// Purge dead-lettered sync mutations: one by server sequence, or all
383    /// when `server_sequence` is None. The fail-closed dead-letter bound
384    /// requires a supported purge path (raw SQL against the daemon-owned
385    /// vault is not one).
386    SyncDeadLetterPurge {
387        server_sequence: Option<i64>,
388    },
389
390    // --- sync pairing (NOT served by the daemon; offline CLI maintenance) ------
391    /// Upload this vault's bootstrap under a fresh pairing code. Not served
392    /// over IPC in this release — the CLI runs pairing as exclusive offline
393    /// maintenance under the vault lock.
394    SyncPairStart,
395    /// Fetch a bootstrap with a pairing code and adopt it (creating the
396    /// local vault when none exists). Not served over IPC in this release —
397    /// the CLI runs pairing as exclusive offline maintenance under the
398    /// vault lock.
399    SyncPairJoin {
400        relay_url: String,
401        code: String,
402        salt: String,
403    },
404}
405
406impl std::fmt::Debug for VaultOp {
407    /// Redacts every field: VaultOp carries master passwords, TOTP seeds,
408    /// and SSH private keys, and derived Debug would print them into any
409    /// future `tracing` call or panic payload.
410    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
411        f.write_str("VaultOp::")
412    }
413}
414
415/// Successful outcome of one [`VaultOp`].
416///
417/// `Debug` prints the VARIANT NAME ONLY: `TotpCode` carries a live
418/// two-factor code and `Entry`/`Entries` carry plaintext secrets, and
419/// derived Debug would render them into any log line or panic payload.
420#[derive(Clone, Serialize, Deserialize)]
421pub enum VaultOpResult {
422    Ok,
423    EntryId(i64),
424    Entry(Box<ServiceEntry>),
425    EntryList(Vec<ServiceEntrySummary>),
426    Entries(Vec<ServiceEntry>),
427    /// Ids created by a bulk import.
428    Imported(Vec<i64>),
429    TotpCode {
430        code: String,
431        seconds_remaining: u32,
432    },
433    TotpMetadata(Option<ServiceTotpMetadata>),
434    SshKey(Box<ServiceSshKey>),
435    SshKeyList(Vec<ServiceSshKeySummary>),
436    Entity(Box<ServiceEntity>),
437    EntityList(Vec<ServiceEntity>),
438    /// Report-shaped payloads (registry overview, sweep, health, audit
439    /// verification) as pre-serialized JSON; clients deserialize into the
440    /// core report types they already link.
441    Report(serde_json::Value),
442    Status(ServiceVaultStatus),
443    Biometric(ServiceBiometricStatus),
444    SyncDevices(Vec<ServiceSyncDeviceInfo>),
445    SyncStatus(ServiceSyncStatus),
446}
447
448impl std::fmt::Debug for VaultOpResult {
449    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
450        f.write_str(self.kind())
451    }
452}
453
454impl VaultOpResult {
455    fn kind(&self) -> &'static str {
456        match self {
457            Self::Ok => "VaultOpResult::Ok",
458            Self::EntryId(_) => "VaultOpResult::EntryId",
459            Self::Entry(_) => "VaultOpResult::Entry",
460            Self::EntryList(_) => "VaultOpResult::EntryList",
461            Self::Entries(_) => "VaultOpResult::Entries",
462            Self::Imported(_) => "VaultOpResult::Imported",
463            Self::TotpCode { .. } => "VaultOpResult::TotpCode",
464            Self::TotpMetadata(_) => "VaultOpResult::TotpMetadata",
465            Self::SshKey(_) => "VaultOpResult::SshKey",
466            Self::SshKeyList(_) => "VaultOpResult::SshKeyList",
467            Self::Entity(_) => "VaultOpResult::Entity",
468            Self::EntityList(_) => "VaultOpResult::EntityList",
469            Self::Report(_) => "VaultOpResult::Report",
470            Self::Status(_) => "VaultOpResult::Status",
471            Self::Biometric(_) => "VaultOpResult::Biometric",
472            Self::SyncDevices(_) => "VaultOpResult::SyncDevices",
473            Self::SyncStatus(_) => "VaultOpResult::SyncStatus",
474        }
475    }
476}
477
478/// Typed service error. `code` is a stable machine-readable label; `message`
479/// is human-facing and MUST NOT contain secret material.
480#[derive(Debug, Clone, Serialize, Deserialize)]
481pub struct ServiceError {
482    pub code: String,
483    pub message: String,
484}
485
486impl ServiceError {
487    pub fn new(code: &str, message: impl Into<String>) -> Self {
488        Self {
489            code: code.to_string(),
490            message: message.into(),
491        }
492    }
493}
494
495impl std::fmt::Display for ServiceError {
496    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
497        write!(f, "{}: {}", self.code, self.message)
498    }
499}
500
501impl std::error::Error for ServiceError {}
502
503/// Outcome envelope for `IpcMessage::ServiceResult`.
504#[derive(Debug, Clone, Serialize, Deserialize)]
505#[serde(tag = "status", rename_all = "snake_case")]
506pub enum ServiceOutcome {
507    Ok { result: VaultOpResult },
508    Err { error: ServiceError },
509}
510
511impl From<VaultOpResult> for ServiceOutcome {
512    fn from(result: VaultOpResult) -> Self {
513        Self::Ok { result }
514    }
515}
516
517impl From<ServiceError> for ServiceOutcome {
518    fn from(error: ServiceError) -> Self {
519        Self::Err { error }
520    }
521}
522
523#[cfg(test)]
524mod tests {
525    use super::*;
526
527    #[test]
528    fn service_entry_round_trips_with_defaults() {
529        let entry = ServiceEntry {
530            entry_id: Some(7),
531            title: "Example".to_string(),
532            username: "user@example.com".to_string(),
533            password: Zeroizing::new("secret".to_string()),
534            url: Some("https://example.com".to_string()),
535            notes: None,
536            credential_type: "api_key".to_string(),
537            created_at: 1_700_000_000,
538            modified_at: 1_700_000_001,
539            favorite: true,
540        };
541        let json = serde_json::to_string(&entry).unwrap();
542        let back: ServiceEntry = serde_json::from_str(&json).unwrap();
543        assert_eq!(back.entry_id, Some(7));
544        assert_eq!(back.password.as_str(), "secret");
545        assert_eq!(back.credential_type, "api_key");
546    }
547
548    /// An old client's entry frame (no credential_type / timestamps) must
549    /// parse on a newer endpoint with serde defaults.
550    #[test]
551    fn legacy_service_entry_parses_with_defaults() {
552        let legacy = r#"{"title":"T","username":"u","password":"p"}"#;
553        let entry: ServiceEntry = serde_json::from_str(legacy).unwrap();
554        assert_eq!(entry.credential_type, "password");
555        assert_eq!(entry.entry_id, None);
556        assert!(!entry.favorite);
557    }
558
559    #[test]
560    fn vault_op_and_result_round_trip() {
561        let op = VaultOp::TotpAdd {
562            entry_id: 3,
563            secret: Zeroizing::new("JBSWY3DPEHPK3PXP".to_string()),
564            algorithm: Some("sha256".to_string()),
565            digits: Some(8),
566            period: Some(60),
567            issuer: Some("Example".to_string()),
568            account_name: None,
569        };
570        let json = serde_json::to_string(&op).unwrap();
571        let back: VaultOp = serde_json::from_str(&json).unwrap();
572        match back {
573            VaultOp::TotpAdd {
574                entry_id, digits, ..
575            } => {
576                assert_eq!(entry_id, 3);
577                assert_eq!(digits, Some(8));
578            }
579            other => panic!("unexpected op: {other:?}"),
580        }
581
582        let result = VaultOpResult::Report(serde_json::json!({ "ok": true }));
583        let json = serde_json::to_string(&result).unwrap();
584        let back: VaultOpResult = serde_json::from_str(&json).unwrap();
585        match back {
586            VaultOpResult::Report(v) => assert_eq!(v["ok"], serde_json::json!(true)),
587            other => panic!("unexpected result: {other:?}"),
588        }
589    }
590
591    #[test]
592    fn service_outcome_is_tagged_and_both_branches_round_trip() {
593        let ok = ServiceOutcome::from(VaultOpResult::EntryId(11));
594        let json = serde_json::to_string(&ok).unwrap();
595        assert!(json.contains("\"status\":\"ok\""), "tagged: {json}");
596        let back: ServiceOutcome = serde_json::from_str(&json).unwrap();
597        match back {
598            ServiceOutcome::Ok {
599                result: VaultOpResult::EntryId(id),
600            } => assert_eq!(id, 11),
601            other => panic!("unexpected outcome: {other:?}"),
602        }
603
604        let err = ServiceOutcome::from(ServiceError::new("vault_locked", "vault is locked"));
605        let json = serde_json::to_string(&err).unwrap();
606        assert!(json.contains("\"status\":\"err\""), "tagged: {json}");
607        let back: ServiceOutcome = serde_json::from_str(&json).unwrap();
608        match back {
609            ServiceOutcome::Err { error } => {
610                assert_eq!(error.code, "vault_locked");
611                assert_eq!(error.message, "vault is locked");
612            }
613            other => panic!("unexpected outcome: {other:?}"),
614        }
615    }
616
617    /// Debug must never leak secret material (review finding: derived Debug
618    /// on a surface carrying master passwords / TOTP seeds / private keys).
619    #[test]
620    fn debug_of_secret_bearing_types_redacts() {
621        let entry = ServiceEntry {
622            entry_id: None,
623            title: "T".to_string(),
624            username: "u".to_string(),
625            password: Zeroizing::new("plain-secret-value".to_string()),
626            url: None,
627            notes: None,
628            credential_type: "password".to_string(),
629            created_at: 0,
630            modified_at: 0,
631            favorite: false,
632        };
633        let rendered = format!("{:?}", entry);
634        assert!(!rendered.contains("plain-secret-value"), "{rendered}");
635        assert!(rendered.contains("[REDACTED]"), "{rendered}");
636
637        let op = VaultOp::VaultCreate {
638            master_password: Zeroizing::new("master-secret-value".to_string()),
639        };
640        let rendered = format!("{op:?}");
641        assert!(!rendered.contains("master-secret-value"), "{rendered}");
642
643        let key = ServiceSshKey {
644            key_id: 1,
645            name: "k".to_string(),
646            comment: None,
647            key_type: "ed25519".to_string(),
648            public_key: "ssh-ed25519 AAA".to_string(),
649            private_key: Some(Zeroizing::new("private-material".to_string())),
650            fingerprint: "SHA256:xyz".to_string(),
651            created_at: 0,
652        };
653        let rendered = format!("{key:?}");
654        assert!(!rendered.contains("private-material"), "{rendered}");
655
656        // The result enum must not leak live TOTP codes or entry secrets.
657        let result = VaultOpResult::TotpCode {
658            code: "123456".to_string(),
659            seconds_remaining: 30,
660        };
661        let rendered = format!("{result:?}");
662        assert!(!rendered.contains("123456"), "{rendered}");
663
664        let result = VaultOpResult::Entry(Box::new(entry));
665        let rendered = format!("{result:?}");
666        assert!(!rendered.contains("plain-secret-value"), "{rendered}");
667    }
668}