// The RememberMe blueprint, as data. Transcribed by hand from the ontology // `remember-me` README + remember-me.als. Unlike List/Grid/Calendar (visual // *feature* layouts), RememberMe is a *capability* that composes onto a // PasswordAuth host: a consent-gated persistent token that survives browser // close and revives the host into `Refreshing` — never directly `Authenticated`. // It owns its own lifecycle (NotPersisted → Persisting → Persisted → Revoked), // so it defines its own state machine rather than sharing LOADSTATE_MACHINE. import type { Blueprint, StateMachine } from "./blueprint" // NotPersisted (initial) → Persisting → Persisted, with the three "return to // NotPersisted" edges (storeFailed / expired / clear) arcing below the row and // revoke() branching Persisted → Revoked. Node coordinates are hand-placed. const REMEMBER_ME_MACHINE: StateMachine = { initId: "notpersisted", caption: "Persistence lifecycle", field: "state : RememberMeState", nodes: [ { id: "notpersisted", x: 40, y: 150, w: 150, h: 44, label: "NotPersisted" }, { id: "persisting", x: 300, y: 150, w: 120, h: 44, label: "Persisting" }, { id: "persisted", x: 560, y: 150, w: 120, h: 44, label: "Persisted" }, { id: "revoked", x: 760, y: 150, w: 120, h: 44, label: "Revoked" }, ], edges: [ // forward path (arcs up, labels above the row) { from: "notpersisted", to: "persisting", label: "startPersist()", kind: "persist", off: -90 }, { from: "persisting", to: "persisted", label: "tokenStored(t)", kind: "store", off: -90 }, { from: "persisted", to: "revoked", label: "revoke()", kind: "revoke", off: -90 }, // returns to NotPersisted (arc below the row; deeper = wider span) { from: "persisting", to: "notpersisted", label: "storeFailed()", kind: "storeFail", off: -70 }, { from: "persisted", to: "notpersisted", label: "expired()", kind: "expire", off: -120 }, { from: "revoked", to: "notpersisted", label: "clear()", kind: "clear", off: -172 }, ], } export const rememberMe: Blueprint = { slug: "remember-me", name: "RememberMe", tagline: "A consent-gated persistent token that survives browser close and revives a signed-out host into Refreshing — never directly into Authenticated.", role: "capability", extendsName: "PasswordAuth", sig: { header: "RememberMeBinding ◁ PasswordAuth", fields: [ { name: "host", type: "one PasswordAuth", note: "the host session being persisted" }, { name: "state", type: "RememberMeState", note: "persistence lifecycle position" }, { name: "optIn", type: "one Bool", note: "captured at sign-in; gates persistence" }, { name: "token", type: "lone PersistentToken", note: "present iff state = Persisted" }, ], types: [ "RememberMeState = NotPersisted | Persisting | Persisted | Revoked", "PersistentToken { subject : Subject, issuedAt, expiresAt : Time } -- absolute TTL", "token ≠ ⊥ ⟺ state = Persisted", "state ∈ {Persisting, Persisted} ⟹ optIn = True (consent-gated)", "token.subject = host.identity.subject at the moment of tokenStored", ], }, functions: [ { id: "capture", name: "Capture", fig: "01", caps: ["RememberMe"], verb: "Read the user's opt-in choice at sign-in (checkbox / toggle) and record optIn.", state: [["optIn", "one Bool"]], behaviors: [], invariants: ["optIn defaults to False — persistence is opt-in, never opt-out"], failures: [], perf: [], notes: ["optIn is captured once at sign-in; it is not re-prompted on every launch."], }, { id: "trigger", name: "Trigger", fig: "02", caps: ["RememberMe", "PasswordAuth"], verb: "Observe host.state transitions; fire startPersist() only when the host reaches Authenticated with opt-in.", state: [ ["state", "RememberMeState"], ["host.state", "AuthState"], ], behaviors: [ { sig: "startPersist()", pre: "host.state = Authenticated, optIn = True, state = NotPersisted", eff: "state ← Persisting", }, ], invariants: ["state ∈ {Persisting, Persisted} ⟹ optIn = True — persistence is consent-gated"], failures: [ "Lost opt-in — startPersist() fires without optIn = True; the user gets surprise persistence when they wanted a one-shot session", ], perf: [], notes: [ "startPersist() fires on the host's transition into Authenticated — not on Idle, not on Refreshing.", ], }, { id: "issue", name: "Issue", fig: "03", caps: ["PersistentToken", "PasswordAuth"], verb: "Backend mints a long-lived, subject-bound PersistentToken with an absolute expiresAt.", state: [["token", "lone PersistentToken"]], behaviors: [], invariants: [ "token.subject = host.identity.subject at issuance — no cross-account binding", "expiresAt is absolute — set once at issuance, never extended by use", ], failures: [ "Cross-account binding — token.subject differs from the verified host identity; the token later impersonates a different user on revival", "Sliding expiry — expiresAt deferred whenever the token is used; the TTL is defeated and the token effectively never expires", ], perf: [ "Token issuance — Persisting → Persisted completes within 200 ms after the host reaches Authenticated", "Token TTL — persistent tokens expire within 30 days of issuance to bound the theft window", ], notes: [], }, { id: "store", name: "Store", fig: "04", caps: ["SecureStorage", "PersistentToken"], verb: "Persist the token bytes to platform-secure storage; surface storeFailed() if storage is unavailable.", state: [ ["state", "RememberMeState"], ["token", "lone PersistentToken"], ], behaviors: [ { sig: "tokenStored(t)", pre: "state = Persisting, t.subject = host.identity.subject", eff: "state ← Persisted, token ← t", }, { sig: "storeFailed()", pre: "state = Persisting, secure storage unavailable", eff: "state ← NotPersisted, token ← ⊥", }, ], invariants: [ "token ≠ ⊥ ⟺ state = Persisted — bytes exist only after tokenStored; Persisting is the in-flight window", "the token lives in platform-secure storage (Keychain, Credential Manager, HttpOnly cookie) — never in plain localStorage", ], failures: [ "Insecure storage — token kept in localStorage / plain disk; recoverable by any script or filesystem reader", "Secret exfil on log — logging middleware captures the token bytes in flight; a long-lived credential leaks to log retention", ], perf: [ "Storage failure — storeFailed() surfaces to the user within 1 s so they know persistence didn't take", ], notes: [ "During Persisting the request is in flight and no token has been minted — the token appears only on tokenStored.", ], }, { id: "revive", name: "Revive", fig: "05", caps: ["RememberMe", "PasswordAuth"], verb: "At cold start with a Persisted token, hand it to host.refresh() — driving the host Idle → Refreshing, never writing Authenticated.", state: [["host.state", "AuthState"]], behaviors: [ { sig: "revive()", pre: "cold start, state = Persisted, host.state = Idle", eff: "host.refresh() — host transitions Idle → Refreshing", }, ], invariants: [ "the persistent token cannot grant host.state = Authenticated directly — only host.refreshed(id) (backend-confirmed) reaches Authenticated", "revive() is a side-effect, not a binding transition — the host's refreshed(id) re-establishes the session", ], failures: [ "Direct-to-Authenticated — revive bypasses host.refresh() and writes Authenticated; the backend never re-validates and revoked tokens still work", "Stale token at launch — revive() uses a token past its expiresAt; the backend rejects it and the user gets a confusing failure", ], perf: [ "Cold-start revival — revive() drives host.state = Refreshing within 100 ms of launch (no flash of Idle UI)", "Refresh round-trip — host refresh → refreshed(id) completes within 1 s on a stable network", ], notes: [ "The consumer never sees Authenticated until the backend confirms — the Refreshing → Authenticated transition is what proves the user is still allowed access.", ], }, { id: "revoke", name: "Revoke", fig: "06", caps: ["RememberMe", "PasswordAuth"], verb: "On host sign-out, a backend revocation signal, or suspicious activity, clear the token and move to Revoked.", state: [ ["state", "RememberMeState"], ["token", "lone PersistentToken"], ], behaviors: [ { sig: "revoke()", pre: "state = Persisted; host signed out / token revoked / suspicious activity", eff: "state ← Revoked, token ← ⊥", }, ], invariants: [ "revoke() clears the token atomically — no window where state = Revoked ∧ token ≠ ⊥", "host.signOut() cascades to revoke() — local sign-out always invalidates the persistent token", ], failures: [ "No revocation cascade — signOut does not fire revoke(); a stolen device still authenticates after the user 'signed out'", "Token theft — a leaked token (XSS, malware, shared device) silently signs in as the user until expiry or revocation", "Multi-device confusion — one device's revoke() doesn't propagate to others; the user signs out on their phone but the browser stays authenticated for hours", ], perf: [ "Revocation propagation — revoke() clears local token bytes within 50 ms of the trigger", ], notes: [], }, { id: "expire", name: "Expire", fig: "07", caps: ["RememberMe", "PersistentToken"], verb: "Drop the token when the wall clock crosses expiresAt; return to NotPersisted with no revocation noise.", state: [ ["state", "RememberMeState"], ["token", "lone PersistentToken"], ], behaviors: [ { sig: "expired()", pre: "state = Persisted, now ≥ token.expiresAt", eff: "state ← NotPersisted, token ← ⊥", }, ], invariants: [ "expired() is reachable only from Persisted and only when the wall clock crosses expiresAt — TTL is absolute, not extended by use", ], failures: [ "Sliding expiry — expired() deferred whenever the token is used; the token effectively never expires and defeats the TTL", ], perf: [ "Expiry visibility — expired() is silent; the UI shows the host's normal Idle state, no false alarm to the user", ], notes: ["Expiry differs from revocation: it is silent and routine, not a security event."], }, { id: "clear", name: "Clear", fig: "08", caps: ["RememberMe"], verb: "Move from Revoked to NotPersisted once the consumer has acknowledged the revocation.", state: [["state", "RememberMeState"]], behaviors: [{ sig: "clear()", pre: "state = Revoked", eff: "state ← NotPersisted" }], invariants: [ "no token exists in Revoked or NotPersisted — clear() only advances the lifecycle position", ], failures: [], perf: [], notes: [ "clear() closes the loop: Revoked → NotPersisted, ready to persist again on the next opted-in sign-in.", ], }, ], coreInvariants: [ "token ≠ ⊥ ⟺ state = Persisted — token bytes exist only after tokenStored", "state ∈ {Persisting, Persisted} ⟹ optIn = True — persistence is opt-in only", "token.subject = host.identity.subject at issuance — no cross-account binding", "a persistent token cannot reach Authenticated directly — it only feeds host.refresh(); only host.refreshed(id) (backend-confirmed) authenticates", "revoke() clears the token atomically — no window where state = Revoked ∧ token ≠ ⊥", "the token lives in platform-secure storage — never in plain localStorage", "expired() is reachable only from Persisted, only when the wall clock crosses expiresAt — TTL is absolute", ], composition: { caps: [ { id: "core", label: "RememberMe : Binding", sub: "state, optIn, token", core: true }, { id: "host", label: "PasswordAuth", sub: "host.state, identity" }, { id: "token", label: "PersistentToken", sub: "subject, expiresAt" }, { id: "storage", label: "SecureStorage", sub: "Keychain · HttpOnly" }, ], funcs: ["Capture", "Trigger", "Issue", "Store", "Revive", "Revoke", "Expire", "Clear"], links: [ ["core", "Capture"], ["core", "Trigger"], ["host", "Trigger"], ["token", "Issue"], ["host", "Issue"], ["storage", "Store"], ["token", "Store"], ["core", "Revive"], ["host", "Revive"], ["core", "Revoke"], ["host", "Revoke"], ["token", "Expire"], ["core", "Expire"], ["core", "Clear"], ], }, stateMachine: REMEMBER_ME_MACHINE, }