feat: add RememberMe blueprint illustration

First capability blueprint in the gallery. Transcribed from the ontology
remember-me README + .als: the persistence lifecycle
(NotPersisted -> Persisting -> Persisted -> Revoked), its 8 functions,
and a specimen annotating the opt-in sign-in and cold-start revive
screens plus the secure-storage vault it owns.
This commit is contained in:
Julien Calixte
2026-07-02 23:18:23 +02:00
parent f6b184406d
commit 062826e126
3 changed files with 774 additions and 0 deletions

View File

@@ -7,9 +7,11 @@ import type { Blueprint } from "./blueprint"
import { list } from "./listBlueprint"
import { grid } from "./gridBlueprint"
import { calendar } from "./calendarBlueprint"
import { rememberMe } from "./rememberMeBlueprint"
import ListSpecimen from "@/specimens/ListSpecimen.vue"
import GridSpecimen from "@/specimens/GridSpecimen.vue"
import CalendarSpecimen from "@/specimens/CalendarSpecimen.vue"
import RememberMeSpecimen from "@/specimens/RememberMeSpecimen.vue"
export interface RegistryEntry {
blueprint: Blueprint
@@ -20,6 +22,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
list: { blueprint: list, specimen: ListSpecimen },
grid: { blueprint: grid, specimen: GridSpecimen },
calendar: { blueprint: calendar, specimen: CalendarSpecimen },
"remember-me": { blueprint: rememberMe, specimen: RememberMeSpecimen },
}
export const BLUEPRINTS: Blueprint[] = Object.values(REGISTRY).map((e) => e.blueprint)

View File

@@ -0,0 +1,305 @@
// 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,
}