kernel/event/record/credential.rs
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! How a credential reached this city: the line a vault write leaves,
7//! and the app a toolkit link was asked for.
8//!
9//! **No line here carries a credential.** A capture names the vault
10//! place and never the value; a toolkit link records which app was
11//! asked for and not the consent URL, which is a capability and would be
12//! handed to every reader of a replayable ledger.
13//!
14//! Older builds also wrote `login_started` for a subscription login and
15//! an `expires_at` key on a capture. This build signs in to no
16//! subscription (`crates/gateway/Spec.lean` §8-5): the kind stays in the vocabulary
17//! so such a ledger still reads, nothing reads its payload, and a
18//! capture's `expires_at` is ignored on the way in.
19
20use serde::{Deserialize, Serialize};
21
22use crate::secret::SecretRef;
23
24/// `secret_captured`: a credential entered the vault at this place.
25///
26/// The place is a [`SecretRef`], so a line whose `ref` is outside the
27/// grammar does not read as a capture at all: every reader meets that
28/// as one refusal from [`Payload::read`](super::super::Payload::read)
29/// rather than each deciding for itself what such a line names.
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
32pub struct SecretCaptured {
33 /// The vault place the credential is stored under.
34 #[serde(rename = "ref")]
35 #[cfg_attr(feature = "schema", schemars(with = "String"))]
36 pub reference: SecretRef,
37 /// How it arrived, in the writer's words: `enrolment` or `pasted`.
38 /// An older build also wrote `<provider>-subscription` and
39 /// `<provider>-renewal`. Free text, because those spellings carry a
40 /// provider name.
41 #[serde(default)]
42 pub origin: String,
43}
44
45/// `toolkit_link_opened`: the person asked to connect this app.
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
48pub struct ToolkitLinkOpened {
49 /// The toolkit's slug.
50 pub toolkit: String,
51}
52
53#[cfg(test)]
54#[allow(clippy::unwrap_used, clippy::expect_used, reason = "test code")]
55mod tests {
56 use super::*;
57 use crate::event::Payload;
58
59 /// The golden bytes: each line as the hand-written writers spelled
60 /// it before these structs existed, so a ledger already on disk
61 /// reads back and a new line is byte-identical to an old one.
62 #[test]
63 fn each_credential_line_keeps_the_bytes_its_ledger_already_holds() {
64 let place = SecretRef::parse("secret:anthropic/api-key").unwrap();
65 let cases: [(Payload, &str); 2] = [
66 (
67 Payload::of(&SecretCaptured {
68 reference: place,
69 origin: "enrolment".to_owned(),
70 })
71 .unwrap(),
72 "{\"origin\":\"enrolment\",\"ref\":\"secret:anthropic/api-key\"}",
73 ),
74 (
75 Payload::of(&ToolkitLinkOpened {
76 toolkit: "gmail".to_owned(),
77 })
78 .unwrap(),
79 "{\"toolkit\":\"gmail\"}",
80 ),
81 ];
82 for (payload, wire) in cases {
83 assert_eq!(serde_json::to_string(&payload).unwrap(), wire);
84 let back: Payload = serde_json::from_str(wire).unwrap();
85 assert_eq!(back, payload);
86 }
87 }
88
89 /// A capture an older build wrote for a subscription token, with its
90 /// `expires_at`, still reads: the key is ignored rather than refused,
91 /// so a ledger that holds one replays (`crates/gateway/Spec.lean` §8-5).
92 #[test]
93 fn a_capture_with_an_expiry_an_older_build_wrote_still_reads() {
94 let payload: Payload = serde_json::from_str(
95 "{\"expires_at\":1700000000000,\"origin\":\"anthropic-subscription\",\"ref\":\"secret:anthropic/oauth\"}",
96 )
97 .unwrap();
98 assert_eq!(
99 payload.read::<SecretCaptured>().unwrap(),
100 SecretCaptured {
101 reference: SecretRef::parse("secret:anthropic/oauth").unwrap(),
102 origin: "anthropic-subscription".to_owned(),
103 }
104 );
105 }
106
107 /// A reference outside the grammar is not a capture, so no reader
108 /// can fold it as one.
109 #[test]
110 fn a_capture_outside_the_reference_grammar_does_not_read() {
111 let payload: Payload =
112 serde_json::from_str("{\"origin\":\"pasted\",\"ref\":\"anthropic/oauth\"}").unwrap();
113 assert!(payload.read::<SecretCaptured>().is_err());
114 }
115}