@asturiu/v0-sdk 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +5 -0
  4. package/README.md +82 -2
  5. package/contract/assertion-vectors.json +90 -0
  6. package/contract/schemas/branding.schema.json +46 -0
  7. package/contract/schemas/definitions.schema.json +376 -0
  8. package/contract/schemas/portal.schema.json +56 -0
  9. package/contract/tenant-api-routes.json +165 -0
  10. package/dist/branding.d.ts +131 -0
  11. package/dist/branding.d.ts.map +1 -0
  12. package/dist/branding.js +206 -0
  13. package/dist/branding.js.map +1 -0
  14. package/dist/changesets.d.ts +58 -0
  15. package/dist/changesets.d.ts.map +1 -0
  16. package/dist/changesets.js +18 -0
  17. package/dist/changesets.js.map +1 -0
  18. package/dist/cli/bin.d.ts +3 -0
  19. package/dist/cli/bin.d.ts.map +1 -0
  20. package/dist/cli/bin.js +12 -0
  21. package/dist/cli/bin.js.map +1 -0
  22. package/dist/cli/changesets.d.ts +14 -0
  23. package/dist/cli/changesets.d.ts.map +1 -0
  24. package/dist/cli/changesets.js +252 -0
  25. package/dist/cli/changesets.js.map +1 -0
  26. package/dist/cli/cli.d.ts +37 -0
  27. package/dist/cli/cli.d.ts.map +1 -0
  28. package/dist/cli/cli.js +415 -0
  29. package/dist/cli/cli.js.map +1 -0
  30. package/dist/cli/context.d.ts +147 -0
  31. package/dist/cli/context.d.ts.map +1 -0
  32. package/dist/cli/context.js +258 -0
  33. package/dist/cli/context.js.map +1 -0
  34. package/dist/cli/engine.d.ts +59 -0
  35. package/dist/cli/engine.d.ts.map +1 -0
  36. package/dist/cli/engine.js +85 -0
  37. package/dist/cli/engine.js.map +1 -0
  38. package/dist/cli/feedback.d.ts +40 -0
  39. package/dist/cli/feedback.d.ts.map +1 -0
  40. package/dist/cli/feedback.js +420 -0
  41. package/dist/cli/feedback.js.map +1 -0
  42. package/dist/cli/files.d.ts +83 -0
  43. package/dist/cli/files.d.ts.map +1 -0
  44. package/dist/cli/files.js +389 -0
  45. package/dist/cli/files.js.map +1 -0
  46. package/dist/cli/index.d.ts +8 -0
  47. package/dist/cli/index.d.ts.map +1 -0
  48. package/dist/cli/index.js +8 -0
  49. package/dist/cli/index.js.map +1 -0
  50. package/dist/cli/overview.d.ts +6 -0
  51. package/dist/cli/overview.d.ts.map +1 -0
  52. package/dist/cli/overview.js +379 -0
  53. package/dist/cli/overview.js.map +1 -0
  54. package/dist/cli/parts.d.ts +5 -0
  55. package/dist/cli/parts.d.ts.map +1 -0
  56. package/dist/cli/parts.js +414 -0
  57. package/dist/cli/parts.js.map +1 -0
  58. package/dist/cli/sources.d.ts +37 -0
  59. package/dist/cli/sources.d.ts.map +1 -0
  60. package/dist/cli/sources.js +91 -0
  61. package/dist/cli/sources.js.map +1 -0
  62. package/dist/cli/terminal.d.ts +135 -0
  63. package/dist/cli/terminal.d.ts.map +1 -0
  64. package/dist/cli/terminal.js +353 -0
  65. package/dist/cli/terminal.js.map +1 -0
  66. package/dist/cli/ui-page.d.ts +8 -0
  67. package/dist/cli/ui-page.d.ts.map +1 -0
  68. package/dist/cli/ui-page.js +216 -0
  69. package/dist/cli/ui-page.js.map +1 -0
  70. package/dist/cli/ui.d.ts +54 -0
  71. package/dist/cli/ui.d.ts.map +1 -0
  72. package/dist/cli/ui.js +251 -0
  73. package/dist/cli/ui.js.map +1 -0
  74. package/dist/client.d.ts +88 -0
  75. package/dist/client.d.ts.map +1 -0
  76. package/dist/client.js +165 -0
  77. package/dist/client.js.map +1 -0
  78. package/dist/contacts.d.ts +43 -0
  79. package/dist/contacts.d.ts.map +1 -0
  80. package/dist/contacts.js +46 -0
  81. package/dist/contacts.js.map +1 -0
  82. package/dist/definition-set.d.ts +731 -0
  83. package/dist/definition-set.d.ts.map +1 -0
  84. package/dist/definition-set.js +659 -0
  85. package/dist/definition-set.js.map +1 -0
  86. package/dist/definitions.d.ts +42 -0
  87. package/dist/definitions.d.ts.map +1 -0
  88. package/dist/definitions.js +154 -0
  89. package/dist/definitions.js.map +1 -0
  90. package/dist/enrolment.d.ts +9 -0
  91. package/dist/enrolment.d.ts.map +1 -0
  92. package/dist/enrolment.js +8 -0
  93. package/dist/enrolment.js.map +1 -0
  94. package/dist/fill.d.ts +73 -0
  95. package/dist/fill.d.ts.map +1 -0
  96. package/dist/fill.js +46 -0
  97. package/dist/fill.js.map +1 -0
  98. package/dist/index.d.ts +39 -0
  99. package/dist/index.d.ts.map +1 -0
  100. package/dist/index.js +39 -0
  101. package/dist/index.js.map +1 -0
  102. package/dist/notifications.d.ts +33 -0
  103. package/dist/notifications.d.ts.map +1 -0
  104. package/dist/notifications.js +36 -0
  105. package/dist/notifications.js.map +1 -0
  106. package/dist/oidc.d.ts +122 -0
  107. package/dist/oidc.d.ts.map +1 -0
  108. package/dist/oidc.js +150 -0
  109. package/dist/oidc.js.map +1 -0
  110. package/dist/overlay.d.ts +109 -0
  111. package/dist/overlay.d.ts.map +1 -0
  112. package/dist/overlay.js +58 -0
  113. package/dist/overlay.js.map +1 -0
  114. package/dist/passes.d.ts +95 -0
  115. package/dist/passes.d.ts.map +1 -0
  116. package/dist/passes.js +140 -0
  117. package/dist/passes.js.map +1 -0
  118. package/dist/portal-options.d.ts +65 -0
  119. package/dist/portal-options.d.ts.map +1 -0
  120. package/dist/portal-options.js +120 -0
  121. package/dist/portal-options.js.map +1 -0
  122. package/dist/reads.d.ts +41 -0
  123. package/dist/reads.d.ts.map +1 -0
  124. package/dist/reads.js +44 -0
  125. package/dist/reads.js.map +1 -0
  126. package/dist/recovery-words.d.ts +8 -0
  127. package/dist/recovery-words.d.ts.map +1 -0
  128. package/dist/recovery-words.js +136 -0
  129. package/dist/recovery-words.js.map +1 -0
  130. package/dist/recovery.d.ts +198 -0
  131. package/dist/recovery.d.ts.map +1 -0
  132. package/dist/recovery.js +112 -0
  133. package/dist/recovery.js.map +1 -0
  134. package/dist/reveal.d.ts +35 -0
  135. package/dist/reveal.d.ts.map +1 -0
  136. package/dist/reveal.js +66 -0
  137. package/dist/reveal.js.map +1 -0
  138. package/dist/seal.d.ts +15 -0
  139. package/dist/seal.d.ts.map +1 -0
  140. package/dist/seal.js +0 -0
  141. package/dist/seal.js.map +1 -0
  142. package/dist/session.d.ts +66 -0
  143. package/dist/session.d.ts.map +1 -0
  144. package/dist/session.js +61 -0
  145. package/dist/session.js.map +1 -0
  146. package/dist/signing-in-mark.d.ts +14 -0
  147. package/dist/signing-in-mark.d.ts.map +1 -0
  148. package/dist/signing-in-mark.js +24 -0
  149. package/dist/signing-in-mark.js.map +1 -0
  150. package/dist/signing-in.d.ts +22 -0
  151. package/dist/signing-in.d.ts.map +1 -0
  152. package/dist/signing-in.js +203 -0
  153. package/dist/signing-in.js.map +1 -0
  154. package/dist/tags.d.ts +96 -0
  155. package/dist/tags.d.ts.map +1 -0
  156. package/dist/tags.js +88 -0
  157. package/dist/tags.js.map +1 -0
  158. package/dist/tenant-subscription.d.ts +47 -0
  159. package/dist/tenant-subscription.d.ts.map +1 -0
  160. package/dist/tenant-subscription.js +7 -0
  161. package/dist/tenant-subscription.js.map +1 -0
  162. package/dist/testing/index.d.ts +125 -0
  163. package/dist/testing/index.d.ts.map +1 -0
  164. package/dist/testing/index.js +320 -0
  165. package/dist/testing/index.js.map +1 -0
  166. package/dist/theme.d.ts +237 -0
  167. package/dist/theme.d.ts.map +1 -0
  168. package/dist/theme.js +504 -0
  169. package/dist/theme.js.map +1 -0
  170. package/dist/tickets.d.ts +152 -0
  171. package/dist/tickets.d.ts.map +1 -0
  172. package/dist/tickets.js +65 -0
  173. package/dist/tickets.js.map +1 -0
  174. package/dist/tiers.d.ts +168 -0
  175. package/dist/tiers.d.ts.map +1 -0
  176. package/dist/tiers.js +341 -0
  177. package/dist/tiers.js.map +1 -0
  178. package/dist/types.d.ts +885 -0
  179. package/dist/types.d.ts.map +1 -0
  180. package/dist/types.js +12 -0
  181. package/dist/types.js.map +1 -0
  182. package/dist/urlid.d.ts +50 -0
  183. package/dist/urlid.d.ts.map +1 -0
  184. package/dist/urlid.js +78 -0
  185. package/dist/urlid.js.map +1 -0
  186. package/dist/version.d.ts +15 -0
  187. package/dist/version.d.ts.map +1 -0
  188. package/dist/version.js +15 -0
  189. package/dist/version.js.map +1 -0
  190. package/make/asturiu.mk +43 -0
  191. package/package.json +63 -4
  192. package/src/branding.ts +221 -0
  193. package/src/changesets.ts +61 -0
  194. package/src/cli/bin.ts +12 -0
  195. package/src/cli/changesets.ts +233 -0
  196. package/src/cli/cli.ts +425 -0
  197. package/src/cli/context.ts +299 -0
  198. package/src/cli/engine.ts +112 -0
  199. package/src/cli/feedback.ts +406 -0
  200. package/src/cli/files.ts +371 -0
  201. package/src/cli/index.ts +7 -0
  202. package/src/cli/overview.ts +343 -0
  203. package/src/cli/parts.ts +371 -0
  204. package/src/cli/sources.ts +111 -0
  205. package/src/cli/terminal.ts +384 -0
  206. package/src/cli/ui-page.ts +215 -0
  207. package/src/cli/ui.ts +266 -0
  208. package/src/client.ts +202 -0
  209. package/src/contacts.ts +61 -0
  210. package/src/definition-set.ts +965 -0
  211. package/src/definitions.ts +156 -0
  212. package/src/enrolment.ts +10 -0
  213. package/src/fill.ts +83 -0
  214. package/src/index.ts +38 -0
  215. package/src/notifications.ts +55 -0
  216. package/src/oidc.ts +199 -0
  217. package/src/overlay.ts +137 -0
  218. package/src/passes.ts +168 -0
  219. package/src/portal-options.ts +125 -0
  220. package/src/reads.ts +71 -0
  221. package/src/recovery-words.ts +137 -0
  222. package/src/recovery.ts +269 -0
  223. package/src/reveal.ts +72 -0
  224. package/src/seal.ts +0 -0
  225. package/src/session.ts +93 -0
  226. package/src/signing-in-mark.ts +23 -0
  227. package/src/signing-in.ts +213 -0
  228. package/src/tags.ts +146 -0
  229. package/src/tenant-subscription.ts +44 -0
  230. package/src/testing/index.ts +337 -0
  231. package/src/theme.ts +628 -0
  232. package/src/tickets.ts +195 -0
  233. package/src/tiers.ts +397 -0
  234. package/src/types.ts +847 -0
  235. package/src/urlid.ts +77 -0
  236. package/src/version.ts +15 -0
  237. package/templates/asturiu/branding.json +4 -0
  238. package/templates/asturiu/definitions.json +23 -0
  239. package/templates/asturiu/portal.json +16 -0
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Recovery contacts and recovery cards (ADR-050): the lists, codes and card
3
+ * format every surface shares, and what the Interface API answers. Pure and
4
+ * dependency-free: the Engine validates with these lists and checks a card's
5
+ * checksum itself; the Customer Portal and a Tenant's app read with them.
6
+ */
7
+ import { PASS_CODE_ALPHABET, normalizePassCode } from "./passes.js";
8
+ import { RECOVERY_WORDS } from "./recovery-words.js";
9
+
10
+ export { RECOVERY_WORDS };
11
+
12
+ /** A vault recovery ends in a Grant on that vault; a sign-in recovery in an Account reset (§1). */
13
+ export const RECOVERY_SUBJECTS = ["vault", "sign_in"] as const;
14
+ export type RecoverySubject = (typeof RECOVERY_SUBJECTS)[number];
15
+
16
+ export const RECOVERY_LIMITS = {
17
+ contactsMin: 2,
18
+ contactsMax: 7,
19
+ quorumMin: 2,
20
+ note: 500,
21
+ name: 120,
22
+ notAfterDaysMax: 3650,
23
+ /** A claim never completes sooner than this after its last card (ADR-050 build note 27), whatever its waiting period. */
24
+ lastCardHours: 24,
25
+ } as const;
26
+
27
+ /** The waiting period in days (§3), counted from the first card (§6), never ending sooner than `lastCardHours` after the last; the owner can cancel during it. */
28
+ export const RECOVERY_WAITING_DAYS = [1, 3, 7, 14, 30] as const;
29
+ export type RecoveryWaitingDays = (typeof RECOVERY_WAITING_DAYS)[number];
30
+ export const RECOVERY_DEFAULT_WAITING_DAYS: RecoveryWaitingDays = 3;
31
+
32
+ /** What a completed vault claim grants unless the owner changes it, and what the owner may add (§3; `transfer` is added, build note 23). */
33
+ export const RECOVERY_DEFAULT_CAPABILITIES = ["read", "reveal", "share"] as const;
34
+ export const RECOVERY_OPTIONAL_CAPABILITIES = ["transfer", "update", "delete"] as const;
35
+ export const RECOVERY_CAPABILITIES = [...RECOVERY_DEFAULT_CAPABILITIES, ...RECOVERY_OPTIONAL_CAPABILITIES] as const;
36
+ export type RecoveryCapability = (typeof RECOVERY_CAPABILITIES)[number];
37
+
38
+ /** What the owner sees per contact (§3). */
39
+ export const RECOVERY_CONTACT_STATES = ["invited", "ready", "declined", "expired", "unreachable"] as const;
40
+ export type RecoveryContactState = (typeof RECOVERY_CONTACT_STATES)[number];
41
+
42
+ /** A claim's life (§6). `collecting` and `approved` are open; the rest are ends. */
43
+ export const RECOVERY_CLAIM_STATES = ["collecting", "approved", "completed", "failed", "cancelled", "expired"] as const;
44
+ export type RecoveryClaimState = (typeof RECOVERY_CLAIM_STATES)[number];
45
+ export const RECOVERY_OPEN_CLAIM_STATES: readonly RecoveryClaimState[] = ["collecting", "approved"];
46
+
47
+ /**
48
+ * Why a recovery call answered `409`: `reason` beside the English `detail`, so a frontend words it itself.
49
+ * `contact_address`: a sign-in claim's new address is one of that recovery's contacts (ADR-050 build notes).
50
+ */
51
+ export const RECOVERY_CONFLICTS = ["not_set_up", "card_saved", "card_declined", "card_state", "claim_closed", "claim_expired", "card_given", "own_recovery", "not_a_claimant", "contact_address", "too_many_failures"] as const;
52
+ export type RecoveryConflict = (typeof RECOVERY_CONFLICTS)[number];
53
+
54
+ /**
55
+ * Why a well-typed card was refused (`card` on `401 recovery-attempt-failed` with `reason: card`; build note 25):
56
+ * its words don't fit together, no card has its card code (mistyped, or the cards were replaced), it serves the other
57
+ * kind of recovery or another Tenant's, the claim is bound to someone else's recovery, or its number was removed or
58
+ * replaced. Each still counts as a wrong try.
59
+ */
60
+ export const RECOVERY_CARD_REFUSALS = ["unreadable", "unknown", "other_kind", "other_product", "other_person", "retired"] as const;
61
+ export type RecoveryCardRefusal = (typeof RECOVERY_CARD_REFUSALS)[number];
62
+
63
+ // ------------------------------------------------------------- codes
64
+
65
+ /** The label printed on every card of one recovery: 8 characters from the pass alphabet, never the ARID (§4). */
66
+ export const RECOVERY_HANDLE_LENGTH = 8;
67
+ /** The code the claimant gives each contact by phone or in person (§6): 6 characters without look-alikes. */
68
+ export const RECOVERY_CLAIM_CODE_LENGTH = 6;
69
+
70
+ const ofAlphabet = (v: string, length: number) => v.length === length && [...v].every((c) => PASS_CODE_ALPHABET.includes(c));
71
+
72
+ /** What a person typed, as the Engine compares it: capitals, with spaces, dashes and dots dropped. */
73
+ export const normalizeRecoveryCode = normalizePassCode;
74
+
75
+ export function isRecoveryHandle(value: string): boolean {
76
+ return ofAlphabet(value, RECOVERY_HANDLE_LENGTH);
77
+ }
78
+
79
+ export function isRecoveryClaimCode(value: string): boolean {
80
+ return ofAlphabet(value, RECOVERY_CLAIM_CODE_LENGTH);
81
+ }
82
+
83
+ /** `ABCDEFGH` → `ABCD-EFGH`; `ABCDEF` → `ABC-DEF`. */
84
+ export function formatRecoveryCode(code: string): string {
85
+ const c = normalizeRecoveryCode(code);
86
+ const half = c.length / 2;
87
+ return c.length === RECOVERY_HANDLE_LENGTH || c.length === RECOVERY_CLAIM_CODE_LENGTH ? `${c.slice(0, half)}-${c.slice(half)}` : c;
88
+ }
89
+
90
+ // ------------------------------------------------------------- words
91
+
92
+ /** 12 words: the 128-bit card and a 4-bit checksum, 11 bits a word, as BIP-39 encodes 128 bits (§4). */
93
+ export const RECOVERY_CARD_WORDS = 12;
94
+
95
+ const WORD_INDEX: ReadonlyMap<string, number> = new Map(RECOVERY_WORDS.map((w, i) => [w, i]));
96
+
97
+ /** A word's 11-bit value, or -1. Every word of the list is fixed by its first four letters, so a word cut there still counts. */
98
+ export function recoveryWordIndex(input: string): number {
99
+ const w = input.trim().toLowerCase();
100
+ const exact = WORD_INDEX.get(w);
101
+ if (exact !== undefined) return exact;
102
+ if (w.length < 4) return -1;
103
+ const matches = RECOVERY_WORDS.filter((x) => x.startsWith(w.slice(0, 4)) && (w.length === 4 || x.startsWith(w)));
104
+ return matches.length === 1 ? (WORD_INDEX.get(matches[0]) ?? -1) : -1;
105
+ }
106
+
107
+ /** The list's spelling of what was typed, or null. */
108
+ export function recoveryWord(input: string): string | null {
109
+ const i = recoveryWordIndex(input);
110
+ return i < 0 ? null : RECOVERY_WORDS[i];
111
+ }
112
+
113
+ // ------------------------------------------------------------- the card
114
+
115
+ /** What a card carries and what the helper page sends back (§4). */
116
+ export interface RecoveryCardFace {
117
+ label: string;
118
+ /** The card's Shamir x, 1 to 7. */
119
+ number: number;
120
+ words: string[];
121
+ }
122
+
123
+ /** The text in a card's QR code. */
124
+ export const RECOVERY_CARD_PREFIX = "ASTURIU-CARD";
125
+
126
+ export function recoveryCardText(card: RecoveryCardFace): string {
127
+ return [RECOVERY_CARD_PREFIX, formatRecoveryCode(card.label), String(card.number), ...card.words].join(" ");
128
+ }
129
+
130
+ /**
131
+ * A card from what was scanned or typed: the QR code's text, or label,
132
+ * number and words in that order, with or without the prefix. Words are
133
+ * written out from the list; whether they hold a valid card is the Engine's
134
+ * check (the checksum). Null when the shape is wrong.
135
+ */
136
+ export function parseRecoveryCard(text: string): RecoveryCardFace | null {
137
+ const parts = text.trim().split(/[\s,;]+/).filter(Boolean);
138
+ if (parts[0]?.toUpperCase() === RECOVERY_CARD_PREFIX) parts.shift();
139
+ if (parts.length !== RECOVERY_CARD_WORDS + 2) return null;
140
+ const label = normalizeRecoveryCode(parts[0]);
141
+ const number = Number.parseInt(parts[1], 10);
142
+ if (!isRecoveryHandle(label) || !Number.isInteger(number) || number < 1 || number > RECOVERY_LIMITS.contactsMax || String(number) !== parts[1]) return null;
143
+ const words = parts.slice(2).map(recoveryWord);
144
+ if (words.some((w) => w === null)) return null;
145
+ return { label, number, words: words as string[] };
146
+ }
147
+
148
+ // ------------------------------------------------------- what the API answers
149
+
150
+ /** What a completed vault claim grants (§3). `claimants` null: any Customer of the Tenant may claim. */
151
+ export interface RecoveryTerms {
152
+ capabilities: RecoveryCapability[];
153
+ /** The Grant ends this many days after completion; null for no end. */
154
+ not_after_days: number | null;
155
+ claimants: string[] | null;
156
+ }
157
+
158
+ export interface RecoveryContact {
159
+ /** The card's number, once cards are issued. */
160
+ number: number | null;
161
+ /** Vault recovery: the contact's Principal, and the owner's Contact for it (ADR-024). */
162
+ principal?: string;
163
+ contact?: string | null;
164
+ /** The name the owner gave (sign-in recovery) or the Principal's display name (vault recovery). */
165
+ name: string | null;
166
+ /** Sign-in recovery: the address the invitation goes to. */
167
+ email?: string;
168
+ state: RecoveryContactState;
169
+ invited_at: string | null;
170
+ collected_at: string | null;
171
+ /** Too many wrong codes on the invitation; Replace all cards sends a new one. */
172
+ locked: boolean;
173
+ }
174
+
175
+ /** A claim as the owner sees it on the recovery (§7). */
176
+ export interface RecoveryClaimSummary {
177
+ id: string;
178
+ state: RecoveryClaimState;
179
+ received: number;
180
+ needed: number;
181
+ opened_at: string;
182
+ bound_at: string | null;
183
+ approved_at: string | null;
184
+ completes_at: string | null;
185
+ }
186
+
187
+ /** `GET /v0/recovery`, `/v0/me/recovery`, `/v0/customer/sign-in/recovery`. No answer carries a card. */
188
+ export interface Recovery {
189
+ configured: boolean;
190
+ arid: string | null;
191
+ subject: RecoverySubject;
192
+ /** Vault recovery: the vault it covers. */
193
+ vault?: string;
194
+ contacts: RecoveryContact[];
195
+ quorum: number;
196
+ /** Contacts whose card is saved; below `quorum` the page warns. */
197
+ ready: number;
198
+ waiting_days: RecoveryWaitingDays;
199
+ /** Vault recovery only. */
200
+ terms?: RecoveryTerms;
201
+ cards_issued_at: string | null;
202
+ /** Open claims. */
203
+ claims: RecoveryClaimSummary[];
204
+ }
205
+
206
+ /** A claim as its claimant or the owner reads it (`GET /v0/recovery/claims/{id}`, `/v0/recovery/sign-in-claims/{id}`). */
207
+ export interface RecoveryClaim {
208
+ id: string;
209
+ kind: RecoverySubject;
210
+ state: RecoveryClaimState;
211
+ role: "claimant" | "owner";
212
+ received: number;
213
+ /** Null until the first card binds the claim to a recovery. */
214
+ needed: number | null;
215
+ /** The display name of the person whose recovery the first card bound the claim to; null before that. */
216
+ owner_name: string | null;
217
+ note: string | null;
218
+ opened_at: string;
219
+ /** Collecting ends here, when the claim expires. */
220
+ expires_at: string;
221
+ bound_at: string | null;
222
+ approved_at: string | null;
223
+ completes_at: string | null;
224
+ closed_at: string | null;
225
+ }
226
+
227
+ /** What opening a claim answers, once: the code is never shown again (§6). */
228
+ export interface OpenedRecoveryClaim {
229
+ claim: string;
230
+ code: string;
231
+ expires_at: string;
232
+ }
233
+
234
+ /** The contact page (`POST /v0/recovery/invites/open`): who asks, and where the card stands. */
235
+ export interface RecoveryInvite {
236
+ subject: RecoverySubject;
237
+ owner_name: string | null;
238
+ /** The Tenant's product name, for a vault recovery and an `isolated` sign-in recovery. */
239
+ product: string | null;
240
+ contact_name: string | null;
241
+ state: RecoveryContactState;
242
+ number: number;
243
+ }
244
+
245
+ /** A card in transit to its contact (`POST /v0/recovery/invites/accept`). Shown until the contact types two words back. */
246
+ export interface RecoveryCard extends RecoveryCardFace {
247
+ /** Which kind of recovery the card serves, printed on it: access to a vault, or a sign-in. */
248
+ subject: RecoverySubject;
249
+ owner_name: string | null;
250
+ product: string | null;
251
+ /** The helper page on the host the recovery belongs to. */
252
+ help_url: string;
253
+ /** The two word positions (1-based) the contact types back to confirm it was saved. */
254
+ confirm: [number, number];
255
+ }
256
+
257
+ /** The helper page after the claim code (`POST /v0/recovery/help/claim`). */
258
+ export interface RecoveryHelpClaim {
259
+ claim: string;
260
+ kind: RecoverySubject;
261
+ /** Whose recovery the claim is bound to, so a contact who keeps several cards picks the right one; null before the first card. */
262
+ owner_name: string | null;
263
+ /** The product a vault claim's recovery lives in, or the platform's name for a sign-in claim on the shared realm, known before any card. */
264
+ product: string | null;
265
+ note: string | null;
266
+ received: number;
267
+ needed: number | null;
268
+ state: RecoveryClaimState;
269
+ }
package/src/reveal.ts ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Reveal levels of a secret Field (ADR-018). Shared by the Engine (Definition
3
+ * schema, the reveal gate) and every frontend (what to ask of the person):
4
+ * pure and dependency-free so the Frontend kit can carry it.
5
+ */
6
+ export const REVEAL_LEVELS = ["visible", "click", "captcha", "password", "2fa"] as const;
7
+ export type RevealLevel = (typeof REVEAL_LEVELS)[number];
8
+
9
+ /** What the reveal gate needs before it returns the value; `null` = nothing beyond the capability. */
10
+ export type RevealStep = "captcha" | "password" | "mfa";
11
+
12
+ export function revealStep(level: RevealLevel | null | undefined): RevealStep | null {
13
+ switch (level) {
14
+ case "captcha":
15
+ return "captcha";
16
+ case "password":
17
+ return "password";
18
+ case "2fa":
19
+ return "mfa";
20
+ default:
21
+ return null;
22
+ }
23
+ }
24
+
25
+ /** The level in effect for a Field Definition: `null` for a plain Field, `click` for a secret one that names none. */
26
+ export function revealLevelOf(def: { secret?: boolean; reveal?: RevealLevel | string | null } | null | undefined): RevealLevel | null {
27
+ if (!def?.secret) return null;
28
+ return (REVEAL_LEVELS as readonly string[]).includes(def.reveal ?? "") ? (def.reveal as RevealLevel) : "click";
29
+ }
30
+
31
+ /** When a session last proved each factor (ISO timestamps). A second factor implies the password. */
32
+ export interface StepUpState {
33
+ password?: string;
34
+ mfa?: string;
35
+ }
36
+
37
+ /**
38
+ * What a fresh OIDC token proves (components/11-authentik.md §Step-up flows).
39
+ * Authentik reports `pwd` for a password stage and `mfa` once an authenticator
40
+ * was validated in the same login; `auth_time` is that login's moment. A
41
+ * token whose login is older than `maxAgeSeconds` proves nothing now.
42
+ */
43
+ export function stepUpFromClaims(claims: { amr?: string[]; auth_time?: number }, now: Date, maxAgeSeconds: number): StepUpState {
44
+ if (typeof claims.auth_time !== "number" || !Number.isFinite(claims.auth_time)) return {};
45
+ const at = new Date(claims.auth_time * 1000);
46
+ if (now.getTime() - at.getTime() > maxAgeSeconds * 1000 || at.getTime() - now.getTime() > 60_000) return {};
47
+ const amr = claims.amr ?? [];
48
+ const out: StepUpState = {};
49
+ if (amr.includes("mfa")) {
50
+ out.mfa = at.toISOString();
51
+ out.password = at.toISOString();
52
+ }
53
+ if (amr.includes("pwd")) out.password = at.toISOString();
54
+ return out;
55
+ }
56
+
57
+ /** Whether a recorded step-up still satisfies `step` at `now`. */
58
+ export function stepUpSatisfies(step: RevealStep | null, state: StepUpState | undefined, now: Date, ttlSeconds: number): boolean {
59
+ if (step === null) return true;
60
+ if (step === "captcha") return false;
61
+ const at = state?.[step];
62
+ if (!at) return false;
63
+ const t = Date.parse(at);
64
+ return Number.isFinite(t) && now.getTime() - t <= ttlSeconds * 1000;
65
+ }
66
+
67
+ /** When a recorded step-up lapses, or `undefined` when there is none. */
68
+ export function stepUpUntil(at: string | undefined, ttlSeconds: number): string | undefined {
69
+ if (!at) return undefined;
70
+ const t = Date.parse(at);
71
+ return Number.isFinite(t) ? new Date(t + ttlSeconds * 1000).toISOString() : undefined;
72
+ }
package/src/seal.ts ADDED
Binary file
package/src/session.ts ADDED
@@ -0,0 +1,93 @@
1
+ /**
2
+ * What every Frontend component holds, and how (ADR-003; components/02 §Security):
3
+ * the opaque session identifier in an `HttpOnly` cookie, forwarded as
4
+ * `X-Asturiu-Session`. Nothing else — no token, no claims, no resource data.
5
+ */
6
+
7
+ import { SIGNING_IN_CSS, signingInMarkup, signingInPhase } from "./signing-in.js";
8
+
9
+ /** The header the Interface API reads on every request except `POST /v0/sessions`. */
10
+ export const SESSION_HEADER = "x-asturiu-session";
11
+
12
+ export interface CookiePolicy {
13
+ httpOnly: true;
14
+ sameSite: "strict" | "lax";
15
+ path: string;
16
+ /** `Secure` whenever the page is served over TLS; local development is the only exception. */
17
+ secure: boolean;
18
+ maxAge?: number;
19
+ }
20
+
21
+ /** The Engine session cookie: `HttpOnly`, `Secure`, `SameSite=Strict`, host-only. */
22
+ export function sessionCookiePolicy(opts: { secure: boolean; maxAge?: number }): CookiePolicy {
23
+ return { httpOnly: true, sameSite: "strict", path: "/", secure: opts.secure, maxAge: opts.maxAge };
24
+ }
25
+
26
+ /**
27
+ * The login-attempt cookie (sealed PKCE verifier + `state`). `Lax`, not
28
+ * `Strict`: the browser arrives at the callback from Authentik's origin and a
29
+ * `Strict` cookie would not be sent on that top-level navigation.
30
+ */
31
+ export function loginCookiePolicy(opts: { secure: boolean; maxAgeSeconds?: number; path?: string }): CookiePolicy {
32
+ return { httpOnly: true, sameSite: "lax", path: opts.path ?? "/auth", secure: opts.secure, maxAge: opts.maxAgeSeconds ?? 600 };
33
+ }
34
+
35
+ /** What one login attempt needs to remember between `/auth/login` and `/auth/callback`. */
36
+ export interface LoginAttempt {
37
+ verifier: string;
38
+ state: string;
39
+ /** Site-relative path to land on afterwards. */
40
+ next: string;
41
+ created_at: string;
42
+ /** Set when the attempt is a step-up (ADR-018): the callback elevates the existing session instead of starting one. */
43
+ step_up?: "password" | "2fa";
44
+ }
45
+
46
+ /** Only site-relative paths may be used as post-login targets (no open redirect). */
47
+ export function safeNextPath(candidate: string | null | undefined, fallback = "/"): string {
48
+ if (!candidate) return fallback;
49
+ if (!candidate.startsWith("/") || candidate.startsWith("//") || candidate.startsWith("/\\")) return fallback;
50
+ if (/[\r\n]/.test(candidate)) return fallback;
51
+ return candidate;
52
+ }
53
+
54
+ /** How `continuePage` writes its page. */
55
+ export interface ContinuePageOptions {
56
+ /** The label and the page title. Default "Signing in". */
57
+ text?: string;
58
+ /** A Content-Security-Policy nonce for the page's one `<style>` element. */
59
+ nonce?: string;
60
+ }
61
+
62
+ /**
63
+ * The page a login callback answers with once the session cookie is set
64
+ * (ADR-032). The callback was reached through a redirect chain the sign-in host
65
+ * started — another site — so a redirect from it is still cross-site and a
66
+ * `SameSite=Strict` session cookie would not go with the next request: the app
67
+ * would see no session and start a login again, in a loop. A page that moves on
68
+ * by itself starts a fresh navigation from the app's own origin, which carries
69
+ * the cookie. `path` must already be a same-origin path (`safeNextPath`).
70
+ *
71
+ * The page is the "Signing in" screen the sign-in host showed while leaving,
72
+ * picked up at the same point of its animation, so the hand-over reads as one
73
+ * screen. It carries no script; its one `<style>` takes `nonce`.
74
+ */
75
+ export function continuePage(path: string, options: string | ContinuePageOptions = {}): string {
76
+ const { text = "Signing in", nonce } = typeof options === "string" ? { text: options } : options;
77
+ const esc = (s: string) => s.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
78
+ const to = esc(safeNextPath(path, "/"));
79
+ const css = `html{background:#f4f4f1}@media (prefers-color-scheme:dark){html{background:#0a0b0d}}body{margin:0}.signing-in{--si-phase:${signingInPhase()}}${SIGNING_IN_CSS}`;
80
+ return (
81
+ `<!doctype html><html><head><meta charset="utf-8"><meta http-equiv="refresh" content="0;url=${to}">` +
82
+ `<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"><meta name="color-scheme" content="light dark"><meta name="robots" content="noindex">` +
83
+ `<title>${esc(text)}</title><style${nonce ? ` nonce="${esc(nonce)}"` : ""}>${css}</style></head>` +
84
+ `<body><div class="signing-in" role="status">${signingInMarkup({ text, href: safeNextPath(path, "/") })}</div></body></html>`
85
+ );
86
+ }
87
+
88
+ /** Seconds until an ISO timestamp, floored at zero. */
89
+ export function secondsUntil(iso: string | undefined, fallbackSeconds = 3600): number {
90
+ if (!iso) return fallbackSeconds;
91
+ const ms = Date.parse(iso) - Date.now();
92
+ return Number.isFinite(ms) ? Math.max(0, Math.floor(ms / 1000)) : fallbackSeconds;
93
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The platform's mark as outlines, in the pixels of the 1015 × 967 brand
3
+ * image: the arch (gold head and arms, teal towards the left drop), the stem
4
+ * (teal head and drop) and the gloss highlights. Generated by
5
+ * `scripts/trace-mark.mjs` — regenerate it, never edit it.
6
+ */
7
+ export const MARK = {
8
+ width: 1015,
9
+ height: 967,
10
+ arch: "M499 1C504 1 519 0 529 1C539 3 550 6 559 10C568 14 577 20 584 26C591 32 597 40 602 47C606 54 610 61 612 68C615 75 617 83 619 90C620 97 621 97 621 110C621 123 619 156 619 170C620 184 622 185 623 192C625 199 627 205 630 212C633 220 637 229 642 238C647 247 652 256 659 265C666 275 675 286 683 295C691 305 697 311 709 322C720 333 742 351 754 360C766 370 773 373 782 379C791 384 798 388 808 392C817 396 823 400 840 405C857 409 892 415 909 419C926 424 932 428 942 434C953 440 963 448 971 456C979 463 986 472 992 481C998 490 1002 500 1006 510C1009 519 1012 527 1013 537C1014 546 1014 557 1013 567C1012 576 1010 584 1007 592C1004 600 1000 608 995 615C989 622 982 630 975 636C967 642 960 646 952 649C944 653 935 655 927 656C918 657 910 657 901 656C892 655 881 652 872 648C863 645 856 642 849 636C842 631 834 624 828 618C821 611 816 604 811 597C807 590 808 597 801 578C793 559 776 505 767 483C759 461 758 461 750 445C741 429 727 405 716 388C706 371 699 361 686 344C672 326 651 299 636 284C621 268 610 258 597 249C585 240 571 233 561 229C551 224 544 222 536 221C528 219 522 218 515 218C508 217 501 218 494 219C487 220 479 222 472 224C465 226 459 229 452 232C446 235 443 236 433 243C423 251 405 265 391 279C377 292 364 308 351 323C338 339 323 357 313 372C303 386 299 391 289 409C279 426 267 448 255 476C244 504 227 554 218 575C208 597 205 596 198 605C192 614 183 621 176 627C169 634 162 638 155 641C147 645 138 649 131 651C123 654 118 655 111 655C104 656 95 656 89 655C82 654 76 653 69 651C62 648 56 645 49 641C43 637 36 632 31 627C26 621 22 618 18 610C13 602 7 587 4 579C1 570 1 566 1 560C0 554 -1 551 1 541C2 532 5 515 10 503C15 491 24 478 30 469C36 461 39 459 44 454C50 449 53 446 61 441C69 436 82 428 92 424C102 420 104 419 121 414C137 410 172 405 192 399C212 393 226 387 242 378C258 369 274 358 289 346C303 335 317 321 328 311C339 300 348 290 355 282C363 273 368 266 373 259C379 251 384 244 388 235C392 227 397 220 400 209C403 199 407 192 408 174C409 156 405 115 405 99C405 83 408 85 410 78C413 70 416 62 420 55C424 47 430 40 435 34C441 28 446 23 453 19C460 14 469 10 476 7C484 4 494 2 498 1C501 0 493 1 499 1Z",
11
+ stem: "M501 336C506 336 522 335 531 337C541 339 552 345 559 349C566 352 567 354 571 358C575 361 577 363 581 369C585 375 592 387 595 396C598 404 599 412 598 421C598 430 595 441 593 448C590 455 591 454 585 464C578 474 562 496 555 509C548 521 546 529 543 540C540 552 539 565 539 575C538 586 538 593 540 604C541 614 543 627 546 637C549 648 554 658 559 668C563 677 566 682 575 694C584 705 602 726 611 739C620 752 623 760 628 770C633 781 636 792 639 801C641 810 642 818 642 826C643 834 642 842 641 850C640 857 639 864 637 871C634 878 632 886 628 893C624 901 619 908 613 915C608 922 602 928 596 933C590 939 584 943 577 947C571 951 563 954 556 957C548 960 541 962 532 964C524 965 512 966 503 966C494 965 487 964 479 962C471 960 462 957 454 953C445 948 436 942 429 936C421 930 416 924 410 917C405 910 399 903 395 894C391 886 387 877 385 868C382 859 380 850 379 839C379 828 379 816 381 804C383 793 387 780 392 769C397 758 401 750 410 737C420 724 439 702 448 690C457 679 458 677 462 669C467 660 472 650 475 640C478 629 481 619 482 608C484 596 485 584 484 571C483 559 481 545 478 534C475 523 473 517 467 506C460 494 446 475 440 465C434 456 434 455 432 449C430 442 427 434 427 426C426 418 427 408 429 399C431 391 435 383 439 376C444 369 449 363 455 358C461 352 471 346 478 343C486 339 496 337 500 336C503 335 495 336 501 336Z",
12
+ gloss: [
13
+ "M492 14C493 14 495 14 497 14C498 14 499 14 500 16C500 17 501 20 500 22C500 23 502 23 499 25C497 26 491 30 486 32C481 34 473 38 469 39C465 39 464 38 463 38C461 37 460 35 459 34C458 33 458 33 459 32C459 31 460 29 461 28C462 26 463 25 465 24C468 22 471 20 475 19C479 17 488 15 491 14C493 13 490 14 492 14Z",
14
+ "M451 43C451 43 453 42 455 43C456 43 458 45 459 46C460 48 461 49 461 50C461 52 463 50 460 54C458 58 449 67 445 74C441 81 436 91 434 95C431 99 430 98 429 98C428 99 426 99 425 98C424 98 424 97 423 96C423 94 422 92 422 90C422 88 420 92 423 85C427 78 439 54 443 47C448 40 449 44 450 43C451 43 450 43 451 43Z",
15
+ "M599 80C599 80 601 80 602 82C603 84 604 85 605 92C606 98 607 117 607 122C607 127 606 123 606 123C605 122 605 127 604 120C603 113 600 87 599 80C598 73 598 79 599 80Z",
16
+ "M421 161C421 161 422 160 423 162C423 163 425 164 424 169C424 174 422 185 419 194C417 202 412 212 409 219C406 227 402 232 399 236C395 241 389 248 389 246C389 245 397 235 400 228C403 222 407 215 409 208C412 201 414 195 416 187C417 179 420 166 421 162C421 158 421 162 421 161Z",
17
+ "M305 343C305 343 306 343 306 344C306 344 308 342 304 346C301 349 291 359 284 366C277 372 265 382 260 386C256 390 257 388 256 389C254 389 255 387 253 388C251 388 246 389 244 390C243 391 245 391 243 393C240 394 234 396 231 398C228 399 229 401 227 401C225 402 217 401 218 400C218 399 224 399 230 395C236 392 243 387 254 380C266 373 289 357 297 351C305 345 303 345 304 344C305 342 304 343 305 343Z",
18
+ "M810 411C810 411 811 410 812 410C813 410 812 410 817 411C822 413 832 418 842 421C853 424 872 428 879 430C886 433 882 432 883 433C884 435 885 438 885 439C885 441 884 442 882 443C881 443 883 444 876 443C869 441 849 436 841 434C833 431 833 431 829 429C825 427 818 423 816 421C813 419 814 419 813 418C812 416 810 413 810 412C809 411 810 412 810 411Z",
19
+ "M76 450C76 450 78 450 79 450C80 450 81 450 81 451C82 452 82 454 82 455C82 456 84 456 81 459C79 461 70 466 66 470C61 474 58 478 54 482C51 486 47 491 44 497C41 502 37 510 35 514C33 518 32 518 31 519C30 520 30 519 29 519C28 519 27 519 27 518C26 516 25 514 26 510C26 506 29 500 31 496C33 491 34 490 37 485C41 480 48 471 53 466C58 461 62 458 66 456C70 453 73 451 75 450C76 449 75 450 76 450Z",
20
+ "M962 473C962 473 963 473 964 473C965 474 966 473 969 477C972 480 979 489 982 495C985 500 987 506 988 509C989 511 988 511 988 512C988 512 987 513 987 513C986 513 984 514 983 514C982 514 984 518 980 513C977 508 965 490 962 485C958 479 959 482 959 480C959 478 961 475 961 473C961 472 961 473 962 473Z",
21
+ "M435 732C436 732 437 732 436 734C436 735 437 734 432 742C427 750 412 776 408 783C403 789 406 782 407 781C407 780 406 779 408 775C409 771 415 759 417 755C420 750 419 751 422 748C425 744 433 735 435 732C437 730 435 732 435 732Z",
22
+ ],
23
+ } as const;