vta_keyspaces/lib.rs
1//! Central registry of the VTA's keyspace names.
2//!
3//! Every `store.keyspace(..)` call in the VTA (`vta-service` server, offline
4//! CLIs, backup, tests) names its keyspace through a `const` here rather than a
5//! bare string literal. This is the single source of truth that killed the
6//! `"imported"` / `"imported_secrets"` test-vs-production divergence (a test
7//! opened a *different*, empty keyspace than the one production writes). The
8//! `no_bare_keyspace_literals` guard in `vta-service` keeps it that way by
9//! scanning that crate's source for bare `.keyspace("…")` literals.
10//!
11//! Keyspace *names* live here; per-keyspace *key formats* (the `key:`, `seed:`,
12//! `path_counter:` … record families inside a keyspace) are a separate concern
13//! and are not yet centralised.
14//!
15//! A near-leaf crate: it holds the shared keyspace vocabulary (the name
16//! constants) plus the [`Keyspaces`] handle bundle, so that every VTA subsystem
17//! crate can name and pass keyspaces without depending on `vta-service`. Its
18//! only dependency is `vti-common` (for `KeyspaceHandle`).
19
20use vti_common::store::KeyspaceHandle;
21
22/// Shared bundle of borrowed keyspace handles passed to operations that need
23/// several keyspaces at once.
24///
25/// The struct is a pure field bundle — the constructors that borrow it from a
26/// concrete `AppState` / `VtaState` live in `vta-service` (they know those
27/// types), so this stays free of any `vta-service` dependency.
28pub struct Keyspaces<'a> {
29 pub keys: &'a KeyspaceHandle,
30 pub acl: &'a KeyspaceHandle,
31 pub contexts: &'a KeyspaceHandle,
32 pub did_templates: &'a KeyspaceHandle,
33 pub audit: &'a KeyspaceHandle,
34 pub imported: &'a KeyspaceHandle,
35 #[cfg(feature = "webvh")]
36 pub webvh: &'a KeyspaceHandle,
37}
38
39/// Master seed + key records (`key:`, `seed:`, `path_counter:`,
40/// `active_seed_id`, `imported_kek_salt`, …) and the backup import sentinel.
41pub const KEYS: &str = "keys";
42/// Auth sessions + challenges.
43pub const SESSIONS: &str = "sessions";
44/// ACL entries + the seal record + the integrity-anchor root.
45pub const ACL: &str = "acl";
46/// Trust contexts (the BIP-32 key hierarchy roots).
47pub const CONTEXTS: &str = "contexts";
48/// Stored DID templates (global + context-scoped).
49pub const DID_TEMPLATES: &str = "did_templates";
50/// Audit log.
51pub const AUDIT: &str = "audit";
52/// Imported secret material (KEK-wrapped). Named `imported_secrets`, **not**
53/// `imported` — the latter was a long-standing test-only typo that operated on
54/// an empty keyspace disjoint from production. Always reference this const.
55pub const IMPORTED_SECRETS: &str = "imported_secrets";
56/// Non-extractable internal signing keys.
57///
58/// Deliberately **not** [`IMPORTED_SECRETS`]: that keyspace wraps its contents
59/// under a KEK derived from the BIP-39 master seed, so anything stored there is
60/// reconstructible by whoever holds the mnemonic. Internal keys exist precisely
61/// to have no such path — their material is generated from the system CSPRNG,
62/// never derived, and lives here instead.
63///
64/// In [`EXCLUDED_FROM_BACKUP`] by design, not by omission. A backup containing
65/// this keyspace would be an export of keys the VTA promises never to export.
66pub const INTERNAL_KEYS: &str = "internal_keys";
67/// Ephemeral cache (resolver/auth caches).
68pub const CACHE: &str = "cache";
69/// Holder credential vault (third-party secrets stored on this VTA).
70pub const VAULT: &str = "vault";
71/// Persistent runtime service-enable state (`operations::protocol::runtime_state`).
72pub const SERVICE_STATE: &str = "service_state";
73/// Sealed-bootstrap anti-replay nonce log.
74pub const SEALED_NONCES: &str = "sealed_nonces";
75/// In-flight backup-bundle control-plane records.
76pub const BACKUP_BUNDLES: &str = "backup_bundles";
77/// WebVH DID records + `did.jsonl` state.
78pub const WEBVH: &str = "webvh";
79/// In-flight passkey-as-verificationMethod enrolment state.
80pub const PASSKEY_VMS: &str = "passkey_vms";
81/// Persisted protocol-management drain set.
82pub const DRAINS: &str = "drains";
83/// Per-kind previous-config snapshots for fail-forward rollback.
84/// (Historically `operations::protocol::snapshot::KEYSPACE_NAME`.)
85pub const SNAPSHOT: &str = "service_prev_config";
86/// KMS-protected, unencrypted boot keyspace (TEE integrity manifest, etc.).
87pub const BOOTSTRAP: &str = "bootstrap";
88/// Inbound-messaging consent: durable grants + TTL'd pending requests
89/// (`vti_common::consent`). The VTA is the first gate for bridged conversations.
90pub const CONSENT: &str = "consent";
91/// Per-(platform, context) approver bindings — who decides consent and how the
92/// prompt routes (`vti_common::consent::ApproverBinding`).
93pub const CONSENT_APPROVERS: &str = "consent_approvers";
94/// VTA-issued credentials (minted by `vta/credentials/issue/0.1`, revoked by
95/// `vta/credentials/revoke/0.1`). One record per credential keyed `cred:<id>`;
96/// revocation is a tombstone (`revokedAt` set in place), not a delete. Distinct
97/// from [`VAULT`] (which stores credentials the holder *holds*).
98pub const ISSUED_CREDENTIALS: &str = "issued_credentials";
99
100/// Per-context key/value store for AI-agent memory (`vta/memory/{put,list,
101/// delete}/0.1`). One record per `(contextId, key)` pair, keyed
102/// `mem:<contextId>:<key>`; `list` is a `mem:<contextId>:` prefix scan. Durable
103/// user data → in [`BACKED_UP`].
104pub const MEMORY: &str = "memory";
105
106/// A member's MLS group state for each data room they belong to
107/// (`rooms/keys/{welcome,commit,open}`), keyed `room-group:<roomId>`.
108///
109/// **This holds group secrets.** Whoever reads a row can decrypt every record
110/// the group could, up to its epoch — the same class of material as [`KEYS`],
111/// and it inherits the same protection: the KMS storage key in a TEE
112/// deployment, and a trusted data directory outside one.
113///
114/// In [`BACKED_UP`], and that is a decision rather than a default. A member who
115/// restores a VTA without their room groups has lost the ability to read every
116/// sealed room they belong to, with no way to recover it — the group cannot be
117/// re-derived, and rejoining means a fresh invitation from every owner. The
118/// backup already carries the credential vault and the master seed; group state
119/// belongs with them.
120pub const ROOM_GROUPS: &str = "room_groups";
121
122/// Invitation credentials this VTA has consumed by joining a room
123/// (`rooms/keys/welcome`), keyed `room-vic:<credentialId>`.
124///
125/// Separate from [`ROOM_GROUPS`] because it outlives them: a member who leaves a
126/// room discards the group, and the consumed-invitation record must survive that
127/// or the same invitation would let them be re-added without a fresh one. Single
128/// use means single use.
129pub const ROOM_INVITATIONS: &str = "room_invitations";
130
131/// The holder's identity attributes, profiles, bindings and contacts
132/// (`persona/*`) — the fourth store, beside [`VAULT`] (secrets and
133/// credentials), [`MEMORY`] (agent memory) and [`APP_STATE`] (uninterpreted
134/// application JSON).
135///
136/// It is a distinct store because disclosure control is its point, and a
137/// maintainer that cannot read a record cannot decide which of its members may
138/// leave, cannot audit which ones did, and cannot answer "what have I shared
139/// with whom". [`APP_STATE`] promises never to interpret its records, so it
140/// cannot host this; and a namespace there is collision avoidance rather than a
141/// trust boundary, so a compromised application sharing a context could remove
142/// the holder's identity data.
143///
144/// **Two scopes share the keyspace, and the split is a security control rather
145/// than a filing decision.** The pool and profiles are *agent-scoped* — above
146/// every context — so that the correlation index can see the risk it most needs
147/// to report: the same value presented by two personas in two different
148/// contexts, which a per-context index cannot see by construction. Bindings,
149/// contacts and disclosure records are context-scoped, because a persona lives
150/// in a context and so do its counterparties.
151///
152/// Nothing inside a context may read the agent-scoped prefixes. The holder
153/// pushes a materialised projection down; a context never pulls. That is
154/// enforced at dispatch, not here, but the prefix split is what makes the
155/// enforcement expressible.
156///
157/// Agent-scoped:
158///
159/// - `pa:<attributeId>` — one attribute of the pool.
160/// - `pp:<profileId>` — one profile.
161/// - `pxi:<hmac>` — correlation index, keyed by a keyed hash of the value so
162/// exact-match lookup works with no plaintext index over personal data.
163/// - `pxr:<attributeId>:<profileId>` — attribute → profile reverse index, so a
164/// delete can name its referring profiles without scanning every profile.
165///
166/// Context-scoped:
167///
168/// - `pb:<contextId>:<personaDid>` — binding of a profile to a persona DID.
169/// - `pc:<contextId>:<contactId>` — a contact's current revision.
170/// - `pcr:<contextId>:<contactId>:<rev>` — superseded contact revisions,
171/// reference-counted rather than reaped on a flat TTL: a revision behind a
172/// disclosure record is evidence the holder can still be asked to account for.
173/// - `pd:<contextId>:<seq:020>` — append-only disclosure record.
174/// - `plp:<contextId>:<profileId>` — context-local profile (inline entries
175/// only). A *separate prefix*, not a flag on `pp:`, so a context-scoped list
176/// scans a space that structurally cannot contain a pool profile — a filter
177/// bug there would be the same one-line leak the authorization rule exists to
178/// remove.
179/// - `plb:<contextId>:<personaDid>` — binding of a local profile.
180///
181/// The holder's identity is the account → in [`BACKED_UP`]. A restored agent
182/// that came back without it would be an agent that no longer knows who its
183/// holder is.
184pub const PERSONA: &str = "persona";
185
186/// Versioned, namespaced application state (`vta/app-state/{get,put,list,
187/// delete,get-many,put-many}/1.0`) — the third store, beside [`VAULT`] (secrets
188/// and credentials) and [`MEMORY`] (agent memory), for JSON an application owns
189/// and the VTA does not interpret.
190///
191/// Four record shapes share the keyspace, distinguished by prefix:
192///
193/// - `app:<contextId>:<namespace>:<key>` — the record itself. `list` in
194/// snapshot mode is an `app:<contextId>:<namespace>:` prefix scan.
195/// - `appv:<contextId>:<namespace>:<version:020}>` — version index, mapping a
196/// zero-padded counter value to its record key. Change-feed `list` scans this
197/// so it can return changes in version order and paginate over a stable
198/// storage key; a scan-and-sort over the records could do neither.
199/// - `appc:<contextId>:<namespace>` — the namespace's monotonic write counter.
200/// - `appt:<contextId>:<namespace>` — the oldest version still covered by a
201/// retained tombstone, which is what `sinceVersion` is checked against.
202///
203/// Deliberately **not** [`MEMORY`]: clearing an agent's memory has to stay a
204/// safe thing for a user to ask, which it cannot be if account state lives
205/// there. Durable user data — an account's recoverability depends on it — so it
206/// is in [`BACKED_UP`], and a restore that came back without it would defeat
207/// the point of the feature.
208pub const APP_STATE: &str = "app_state";
209
210/// Rego policy modules for the Policy Decision Point (`policy/{upsert,list,
211/// delete,evaluate}`). One `policy::PolicyModule` per id, keyed `policy:<id>`;
212/// the active set is every enabled row, priority-ordered. Durable operator
213/// security config → in [`BACKED_UP`] (a lost policy set would silently drop
214/// enforcement on restore).
215pub const POLICY: &str = "policy";
216
217/// Task-execution consent for the PDP's `requireConsent` disposition: pending
218/// approvals keyed by payload digest, and granted consents a re-submitted task
219/// consumes. Distinct from [`CONSENT`] (messaging-bridge conversation consent).
220/// One `policy::consent::PendingTaskConsent` per `pending:<digest>` and
221/// `policy::consent::TaskConsentGrant` per `grant:<digest>:<requester>`.
222/// Durable operator-facing security state → [`BACKED_UP`].
223pub const TASK_CONSENT: &str = "task_consent";
224
225/// Durable reliable-messaging outbox backing `vti_common::outbox_store::`
226/// `VtiOutboxStore` for the delivery-layer `MessagingService` (D2 P2a
227/// cut-over). Holds `Guaranteed`-delivery outbox entries; dormant in P2a (all
228/// current sends are `BestEffort`) but wired so the drain/confirmation loops
229/// persist across restarts once P2b adds guaranteed VTA pushes. Runtime state,
230/// not backed up.
231pub const OUTBOX: &str = "outbox";
232
233/// Idempotency records for keyed Trust Tasks — one row per
234/// `(actor, idempotency-key)`, holding the request digest and, for tasks whose
235/// response may be replayed, the original response. Lets a client's retry of a
236/// lost reply converge on the first execution instead of producing a second
237/// durable effect.
238///
239/// Persistent rather than in-memory (unlike the `(actor, envelope-id)` replay
240/// cache it sits beside) because the window that matters is exactly the one a
241/// restart falls inside: the VTA processed the request, the reply was lost, and
242/// the client is still retrying. Swept on TTL by
243/// `vta_sweepers::idempotency_sweeper`. Runtime state, not backed up.
244pub const IDEMPOTENCY: &str = "idempotency";
245
246/// Every production keyspace. Partitioned by [`BACKED_UP`] +
247/// [`EXCLUDED_FROM_BACKUP`]; the [`tests::backup_partition_is_total`] guard
248/// asserts the partition stays exhaustive so a newly-added keyspace can't be
249/// silently omitted from the backup decision.
250pub const ALL: &[&str] = &[
251 INTERNAL_KEYS,
252 KEYS,
253 SESSIONS,
254 ACL,
255 CONTEXTS,
256 DID_TEMPLATES,
257 AUDIT,
258 IMPORTED_SECRETS,
259 CACHE,
260 VAULT,
261 SERVICE_STATE,
262 SEALED_NONCES,
263 BACKUP_BUNDLES,
264 WEBVH,
265 PASSKEY_VMS,
266 DRAINS,
267 SNAPSHOT,
268 BOOTSTRAP,
269 CONSENT,
270 CONSENT_APPROVERS,
271 ISSUED_CREDENTIALS,
272 MEMORY,
273 ROOM_GROUPS,
274 ROOM_INVITATIONS,
275 APP_STATE,
276 PERSONA,
277 POLICY,
278 TASK_CONSENT,
279 OUTBOX,
280 IDEMPOTENCY,
281];
282
283/// Keyspaces whose contents a full `export_backup` captures (as typed
284/// collections — see `operations::backup`).
285pub const BACKED_UP: &[&str] = &[
286 KEYS,
287 ACL,
288 CONTEXTS,
289 AUDIT,
290 IMPORTED_SECRETS,
291 WEBVH,
292 CONSENT,
293 CONSENT_APPROVERS,
294 // Durable agent memory is user data and must survive a restore.
295 MEMORY,
296 ROOM_GROUPS,
297 ROOM_INVITATIONS,
298 // Application state IS the user's account for a consumer built on it —
299 // labels, relationships, contacts, join history. A restore that came back
300 // without it would return a VTA whose applications no longer recognise
301 // their own data, which is the failure the store exists to prevent.
302 APP_STATE,
303 // The holder's own identity — attributes, profiles, bindings, contacts.
304 // A restore that came back without it would return an agent that no longer
305 // knows who its holder is, which is most of what the restore was for.
306 PERSONA,
307 // Operator security policy — must survive a restore, else enforcement
308 // silently reverts to whatever defaults boot-install provides.
309 POLICY,
310 // Task-consent grants are durable authorizations a re-submitted task
311 // consumes; losing them on restore would strand in-flight approvals.
312 TASK_CONSENT,
313];
314
315/// Keyspaces deliberately **not** in a backup.
316///
317/// Most are ephemeral / runtime / re-derivable: [`SESSIONS`], [`CACHE`],
318/// [`SEALED_NONCES`], [`SERVICE_STATE`], [`BACKUP_BUNDLES`], [`PASSKEY_VMS`],
319/// [`DRAINS`], [`SNAPSHOT`], [`BOOTSTRAP`]. [`DID_TEMPLATES`] and [`VAULT`]
320/// hold durable operator/holder state and are **known backup gaps** — a
321/// backup-fidelity follow-up should move them into [`BACKED_UP`], not leave
322/// them silently dropped.
323pub const EXCLUDED_FROM_BACKUP: &[&str] = &[
324 // Non-extractable internal signing keys. Excluding them is the feature:
325 // a backup that carried them would export keys the VTA guarantees never
326 // to export, and restoring one elsewhere would silently clone a signer.
327 INTERNAL_KEYS,
328 SESSIONS,
329 DID_TEMPLATES,
330 CACHE,
331 VAULT,
332 SERVICE_STATE,
333 SEALED_NONCES,
334 BACKUP_BUNDLES,
335 PASSKEY_VMS,
336 DRAINS,
337 SNAPSHOT,
338 BOOTSTRAP,
339 // Durable VTA-issued holder credentials. Like [`VAULT`], a known backup
340 // gap — a backup-fidelity follow-up should move it into [`BACKED_UP`].
341 ISSUED_CREDENTIALS,
342 // Reliable-messaging outbox: runtime delivery state, re-driven from live
343 // sends, not part of a state backup.
344 OUTBOX,
345 // Trust-Task idempotency records. Short-lived by construction (a retry
346 // window, not durable state) and scoped to the VTA that served the original
347 // request — restoring one elsewhere would claim to have already performed
348 // operations that instance never did.
349 IDEMPOTENCY,
350];
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355 use std::collections::BTreeSet;
356
357 /// The backup partition must be total and disjoint: every production
358 /// keyspace is either backed up or explicitly excluded. Adding a keyspace
359 /// to [`ALL`] without classifying it fails here — that's the point.
360 #[test]
361 fn backup_partition_is_total() {
362 let all: BTreeSet<&str> = ALL.iter().copied().collect();
363 let backed: BTreeSet<&str> = BACKED_UP.iter().copied().collect();
364 let excluded: BTreeSet<&str> = EXCLUDED_FROM_BACKUP.iter().copied().collect();
365
366 assert_eq!(all.len(), ALL.len(), "ALL has a duplicate");
367 assert!(
368 backed.is_disjoint(&excluded),
369 "a keyspace is both backed up and excluded: {:?}",
370 backed.intersection(&excluded).collect::<Vec<_>>()
371 );
372 let union: BTreeSet<&str> = backed.union(&excluded).copied().collect();
373 assert_eq!(
374 union, all,
375 "backup partition is not exhaustive — every keyspace in ALL must be in \
376 exactly one of BACKED_UP / EXCLUDED_FROM_BACKUP"
377 );
378 }
379}
380
381// ---------------------------------------------------------------------------
382// What a DID deletion means for each keyspace
383// ---------------------------------------------------------------------------
384
385/// What happens to a keyspace's DID-keyed contents when that DID is deleted.
386///
387/// Deleting a DID is not one cleanup. It is four different relationships, and
388/// treating them alike gets one of them wrong in a way nobody notices until it
389/// matters:
390///
391/// * things the DID **owns** go with it;
392/// * things that **name it as a subject of authorization** must go with it, or
393/// they become authority for an identity that no longer resolves;
394/// * things that **depend on it to function** must *stop* the deletion, because
395/// cascading would silently break them;
396/// * credentials the VTA **issued** cannot be deleted at all — copies exist
397/// elsewhere — so the only honest action is revocation.
398///
399/// # Why this is an enum and not a list in a function
400///
401/// The failure mode is not getting today's answers wrong. It is a keyspace
402/// added next quarter that nobody classifies, whose rows then quietly outlive
403/// the DID they belong to. [`ALL`] is already pinned by a census test for the
404/// backup partition, for exactly the same reason; this rides the same rail, so
405/// "we forgot" is a red test rather than an orphan found months later in a log.
406///
407/// The classifications below are judgements and several are arguable. That is
408/// fine — the point of the census is to force the question to be asked, not to
409/// claim these answers are the last word.
410#[derive(Debug, Clone, Copy, PartialEq, Eq)]
411pub enum DidDeleteEffect {
412 /// Rows belonging to the DID are removed with it.
413 Cascade,
414 /// A row referencing the DID **blocks** the deletion: something still in
415 /// use would break. Refused, never forced — the operator is told what to
416 /// unpick first.
417 Blocks,
418 /// Rows cannot be removed, because the VTA is not the only holder. They
419 /// are revoked instead.
420 Revoke,
421 /// Nothing here is keyed to a DID.
422 Unrelated,
423}
424
425/// The effect a DID deletion has on `keyspace`, or `None` if the name is not a
426/// keyspace this build knows.
427///
428/// Every entry in [`ALL`] is classified — see `did_delete_census` in this
429/// module's tests.
430#[must_use]
431pub const fn did_delete_effect(keyspace: &str) -> Option<DidDeleteEffect> {
432 use DidDeleteEffect::*;
433 // `const fn` cannot match on `&str`, so this is a byte-slice match.
434 Some(match keyspace.as_bytes() {
435 // ---- Owned by the DID -------------------------------------------
436 // Key material derived under it, its own log, its advertised name.
437 b"keys" | b"internal_keys" | b"imported_secrets" | b"webvh" => Cascade,
438 // Resolution + protocol caches keyed by DID: stale the moment it goes.
439 b"cache" | b"outbox" => Cascade,
440
441 // ---- Names the DID as a subject of authorization -----------------
442 // An ACL entry outliving its DID is the worst of the orphans: live
443 // authority for an identity that can no longer be resolved or rotated.
444 // The VTC learned this the expensive way (#1194, #1196).
445 b"acl" | b"sessions" | b"passkey_vms" => Cascade,
446 // Consent state and the vault are held *for* a holder; with the holder
447 // gone they are unreachable by anyone.
448 b"consent" | b"task_consent" | b"vault" => Cascade,
449 // Per-DID application state the VTA stores on a holder's behalf.
450 b"app_state" | b"memory" => Cascade,
451 // A member's room groups, and the invitations they were joined under.
452 // Held for the holder in the same sense the vault is: the group state
453 // decrypts a room *that member* belongs to, and with the member gone
454 // nobody can use it or ever will again. Keeping it would leave group
455 // secrets on disk outliving the only party they were for.
456 //
457 // The consumed-invitation records go with them and not before: while
458 // the member exists, a consumed invitation MUST outlive the group it
459 // let them join, or leaving a room would make the same invitation work
460 // twice. Single use means single use, and the record is the only thing
461 // that remembers.
462 b"room_groups" | b"room_invitations" => Cascade,
463
464 // Persona is two scopes in one keyspace and only one half is DID-keyed,
465 // which the per-keyspace enum cannot say — so it is said here. The
466 // context-scoped rows DO belong to a DID: a binding is keyed by the
467 // persona DID, and a contact records the persona that knows it. Those
468 // cascade, because a binding for a DID that can no longer be resolved is
469 // the ACL orphan again in a different keyspace. The agent-scoped rows —
470 // the attribute pool, the profiles, and their indexes — are keyed to no
471 // DID and survive, which is correct: a profile may be bound to several
472 // personas, and deleting one persona must not destroy facts the holder
473 // still presents through another.
474 b"persona" => Cascade,
475
476 // ---- Depends on the DID to function ------------------------------
477 // A context whose `did` is this one, a DID named in an advertised
478 // service entry (or its rollback snapshot), a policy or approver set
479 // that names it. Cascading any of these breaks something that is still
480 // in use; refusing tells the operator what to unpick.
481 b"contexts" | b"service_state" | b"service_prev_config" => Blocks,
482 b"policy" | b"consent_approvers" => Blocks,
483
484 // ---- Cannot be deleted, only revoked -----------------------------
485 // Third parties hold copies. Deleting our record achieves nothing but
486 // losing our ability to revoke it.
487 b"issued_credentials" => Revoke,
488
489 // ---- Not keyed to a DID ------------------------------------------
490 // The audit log is deliberately here: it is append-only, and the record
491 // that a DID was deleted is the one thing that must survive deleting it.
492 b"audit" => Unrelated,
493 b"did_templates" | b"sealed_nonces" | b"backup_bundles" => Unrelated,
494 b"drains" | b"bootstrap" | b"idempotency" => Unrelated,
495
496 _ => return None,
497 })
498}
499
500#[cfg(test)]
501mod did_delete_tests {
502 use super::*;
503
504 /// Every keyspace must have an answer to "what happens to this when a DID
505 /// is deleted".
506 ///
507 /// This is the whole point of the classification. Adding a keyspace is
508 /// easy; remembering that its rows might outlive the DID they belong to is
509 /// not, and nothing about adding one prompts the question. This test asks
510 /// it, once, at the only moment anyone is looking.
511 ///
512 /// A new keyspace fails here until it is classified. `Unrelated` is a
513 /// perfectly good answer — but it has to be a chosen one.
514 #[test]
515 fn every_keyspace_is_classified_for_did_deletion() {
516 let unclassified: Vec<&str> = ALL
517 .iter()
518 .copied()
519 .filter(|ks| did_delete_effect(ks).is_none())
520 .collect();
521 assert!(
522 unclassified.is_empty(),
523 "these keyspaces have no DID-deletion effect declared: {unclassified:?}\n\
524 Add them to `did_delete_effect`. `Unrelated` is a fine answer if \
525 nothing in the keyspace is keyed to a DID — but it must be chosen, \
526 not defaulted."
527 );
528 }
529
530 /// An unknown name is not silently `Unrelated`. The distinction matters:
531 /// `None` means "this build does not know that keyspace", and answering
532 /// `Unrelated` to it would let a typo read as "nothing to clean up".
533 #[test]
534 fn an_unknown_keyspace_has_no_effect_rather_than_a_harmless_one() {
535 assert_eq!(did_delete_effect("not_a_keyspace"), None);
536 assert_eq!(did_delete_effect(""), None);
537 }
538
539 /// The credential keyspace must never be classified `Cascade`.
540 ///
541 /// Pinned explicitly because it is the one that looks most like a cascade
542 /// and is not: the VTA is not the only holder of what it issued, so
543 /// deleting our record destroys the ability to revoke it while leaving
544 /// every copy in the wild valid forever. That is the exact residue an ACL
545 /// revoke left behind on the VTC.
546 #[test]
547 fn issued_credentials_are_revoked_never_deleted() {
548 assert_eq!(
549 did_delete_effect(ISSUED_CREDENTIALS),
550 Some(DidDeleteEffect::Revoke)
551 );
552 }
553
554 /// The audit log must survive the deletion it records.
555 #[test]
556 fn the_audit_log_is_never_cascaded() {
557 assert_eq!(did_delete_effect(AUDIT), Some(DidDeleteEffect::Unrelated));
558 }
559}