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:
@@ -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)
|
||||
|
||||
305
src/data/rememberMeBlueprint.ts
Normal file
305
src/data/rememberMeBlueprint.ts
Normal 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,
|
||||
}
|
||||
Reference in New Issue
Block a user