@yielded/auth-persistence 0.1.0-beta.10

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 (182) hide show
  1. package/README.md +27 -0
  2. package/dist/Adapter.d.mts +30 -0
  3. package/dist/Adapter.mjs +25 -0
  4. package/dist/index.d.mts +7 -0
  5. package/dist/index.mjs +7 -0
  6. package/dist/internal/PasswordPreparedPostconditions.d.mts +22 -0
  7. package/dist/internal/PasswordPreparedPostconditions.mjs +12 -0
  8. package/dist/internal/PasswordPreparedPostconditions.mjs.map +1 -0
  9. package/dist/internal/configuration.d.mts +174 -0
  10. package/dist/internal/configuration.mjs +7 -0
  11. package/dist/internal/configuration.mjs.map +1 -0
  12. package/dist/internal/email-kernel.d.mts +275 -0
  13. package/dist/internal/email-kernel.mjs +487 -0
  14. package/dist/internal/email-kernel.mjs.map +1 -0
  15. package/dist/internal/mapping-error.d.mts +10 -0
  16. package/dist/internal/mapping-error.mjs +30 -0
  17. package/dist/internal/mapping-error.mjs.map +1 -0
  18. package/dist/internal/models/common.d.mts +12 -0
  19. package/dist/internal/models/email-model.d.mts +353 -0
  20. package/dist/internal/models/email-model.mjs +21 -0
  21. package/dist/internal/models/email-model.mjs.map +1 -0
  22. package/dist/internal/models/passkey-model.d.mts +266 -0
  23. package/dist/internal/models/passkey-model.mjs +34 -0
  24. package/dist/internal/models/passkey-model.mjs.map +1 -0
  25. package/dist/internal/models/passkey-registration-ceremony-model.d.mts +49 -0
  26. package/dist/internal/models/passkey-registration-ceremony-model.mjs +18 -0
  27. package/dist/internal/models/passkey-registration-ceremony-model.mjs.map +1 -0
  28. package/dist/internal/models/passkey-write-model.d.mts +189 -0
  29. package/dist/internal/models/passkey-write-model.mjs +13 -0
  30. package/dist/internal/models/passkey-write-model.mjs.map +1 -0
  31. package/dist/internal/models/password-model.d.mts +328 -0
  32. package/dist/internal/models/password-model.mjs +23 -0
  33. package/dist/internal/models/password-model.mjs.map +1 -0
  34. package/dist/internal/models/proof-model.d.mts +285 -0
  35. package/dist/internal/models/proof-model.mjs +17 -0
  36. package/dist/internal/models/proof-model.mjs.map +1 -0
  37. package/dist/internal/models/session-model.d.mts +199 -0
  38. package/dist/internal/models/session-model.mjs +20 -0
  39. package/dist/internal/models/session-model.mjs.map +1 -0
  40. package/dist/internal/models/step-up-model.d.mts +66 -0
  41. package/dist/internal/models/step-up-model.mjs +9 -0
  42. package/dist/internal/models/step-up-model.mjs.map +1 -0
  43. package/dist/internal/oauth-app.d.mts +11 -0
  44. package/dist/internal/oauth-app.mjs +57 -0
  45. package/dist/internal/oauth-app.mjs.map +1 -0
  46. package/dist/internal/oauth-server.d.mts +10 -0
  47. package/dist/internal/oauth-server.mjs +66 -0
  48. package/dist/internal/oauth-server.mjs.map +1 -0
  49. package/dist/internal/passkey/admission.d.mts +6 -0
  50. package/dist/internal/passkey/admission.mjs +216 -0
  51. package/dist/internal/passkey/admission.mjs.map +1 -0
  52. package/dist/internal/passkey/credentials.d.mts +14 -0
  53. package/dist/internal/passkey/credentials.mjs +249 -0
  54. package/dist/internal/passkey/credentials.mjs.map +1 -0
  55. package/dist/internal/passkey/enrollment.mjs +138 -0
  56. package/dist/internal/passkey/enrollment.mjs.map +1 -0
  57. package/dist/internal/passkey/flow.d.mts +21 -0
  58. package/dist/internal/passkey/flow.mjs +452 -0
  59. package/dist/internal/passkey/flow.mjs.map +1 -0
  60. package/dist/internal/passkey/management.mjs +231 -0
  61. package/dist/internal/passkey/management.mjs.map +1 -0
  62. package/dist/internal/passkey/registration-ceremony.d.mts +9 -0
  63. package/dist/internal/passkey/registration-ceremony.mjs +129 -0
  64. package/dist/internal/passkey/registration-ceremony.mjs.map +1 -0
  65. package/dist/internal/passkey/registration-custody.d.mts +2 -0
  66. package/dist/internal/passkey/registration-custody.mjs +52 -0
  67. package/dist/internal/passkey/registration-custody.mjs.map +1 -0
  68. package/dist/internal/passkey/registration-write.mjs +211 -0
  69. package/dist/internal/passkey/registration-write.mjs.map +1 -0
  70. package/dist/internal/passkey/state.d.mts +10 -0
  71. package/dist/internal/passkey/state.mjs +234 -0
  72. package/dist/internal/passkey/state.mjs.map +1 -0
  73. package/dist/internal/passkey/target.d.mts +19 -0
  74. package/dist/internal/passkey/target.mjs +190 -0
  75. package/dist/internal/passkey/target.mjs.map +1 -0
  76. package/dist/internal/passkey/write-state.d.mts +17 -0
  77. package/dist/internal/passkey/write-state.mjs +345 -0
  78. package/dist/internal/passkey/write-state.mjs.map +1 -0
  79. package/dist/internal/passkey/write-target.mjs +192 -0
  80. package/dist/internal/passkey/write-target.mjs.map +1 -0
  81. package/dist/internal/passkey-kernel.d.mts +5592 -0
  82. package/dist/internal/passkey-kernel.mjs +49 -0
  83. package/dist/internal/passkey-kernel.mjs.map +1 -0
  84. package/dist/internal/passkeys.d.mts +7 -0
  85. package/dist/internal/passkeys.mjs +368 -0
  86. package/dist/internal/passkeys.mjs.map +1 -0
  87. package/dist/internal/password-kernel.d.mts +290 -0
  88. package/dist/internal/password-kernel.mjs +704 -0
  89. package/dist/internal/password-kernel.mjs.map +1 -0
  90. package/dist/internal/persistence.d.mts +25 -0
  91. package/dist/internal/persistence.mjs +291 -0
  92. package/dist/internal/persistence.mjs.map +1 -0
  93. package/dist/internal/phone-kernel.d.mts +162 -0
  94. package/dist/internal/phone-kernel.mjs +434 -0
  95. package/dist/internal/phone-kernel.mjs.map +1 -0
  96. package/dist/internal/proof-kernel.d.mts +93 -0
  97. package/dist/internal/proof-kernel.mjs +619 -0
  98. package/dist/internal/proof-kernel.mjs.map +1 -0
  99. package/dist/internal/query-operations.d.mts +61 -0
  100. package/dist/internal/registration-contract.d.mts +16 -0
  101. package/dist/internal/registration.mjs +66 -0
  102. package/dist/internal/registration.mjs.map +1 -0
  103. package/dist/internal/session-kernel.d.mts +48 -0
  104. package/dist/internal/session-kernel.mjs +737 -0
  105. package/dist/internal/session-kernel.mjs.map +1 -0
  106. package/dist/internal/sql-persistence.d.mts +10 -0
  107. package/dist/internal/sql-persistence.mjs +37 -0
  108. package/dist/internal/sql-persistence.mjs.map +1 -0
  109. package/dist/internal/sql-query.mjs +207 -0
  110. package/dist/internal/sql-query.mjs.map +1 -0
  111. package/dist/internal/sql-table.d.mts +36 -0
  112. package/dist/internal/sql-table.mjs +81 -0
  113. package/dist/internal/sql-table.mjs.map +1 -0
  114. package/dist/internal/standalone.d.mts +9 -0
  115. package/dist/internal/standalone.mjs +13 -0
  116. package/dist/internal/standalone.mjs.map +1 -0
  117. package/dist/internal/step-up-state.d.mts +49 -0
  118. package/dist/internal/step-up-state.mjs +32 -0
  119. package/dist/internal/step-up-state.mjs.map +1 -0
  120. package/dist/internal/storage-mapping.mjs +686 -0
  121. package/dist/internal/storage-mapping.mjs.map +1 -0
  122. package/dist/internal/storage-tables.d.mts +913 -0
  123. package/dist/internal/storage-tables.mjs +387 -0
  124. package/dist/internal/storage-tables.mjs.map +1 -0
  125. package/dist/internal/storage-validation.mjs +58 -0
  126. package/dist/internal/storage-validation.mjs.map +1 -0
  127. package/dist/internal/transaction-execution-kernel.d.mts +38 -0
  128. package/dist/internal/transaction-execution-kernel.mjs +116 -0
  129. package/dist/internal/transaction-execution-kernel.mjs.map +1 -0
  130. package/dist/internal/transaction-kernel.d.mts +77 -0
  131. package/dist/internal/transaction-kernel.mjs +260 -0
  132. package/dist/internal/transaction-kernel.mjs.map +1 -0
  133. package/package.json +1 -0
  134. package/src/Adapter.ts +291 -0
  135. package/src/index.ts +6 -0
  136. package/src/internal/PasswordPreparedPostconditions.ts +30 -0
  137. package/src/internal/configuration.ts +282 -0
  138. package/src/internal/email-kernel.ts +1535 -0
  139. package/src/internal/mapping-error.ts +36 -0
  140. package/src/internal/models/common.ts +11 -0
  141. package/src/internal/models/email-model.ts +559 -0
  142. package/src/internal/models/passkey-model.ts +390 -0
  143. package/src/internal/models/passkey-registration-ceremony-model.ts +72 -0
  144. package/src/internal/models/passkey-write-model.ts +297 -0
  145. package/src/internal/models/password-model.ts +513 -0
  146. package/src/internal/models/proof-model.ts +424 -0
  147. package/src/internal/models/session-model.ts +375 -0
  148. package/src/internal/models/step-up-model.ts +135 -0
  149. package/src/internal/oauth-app.ts +82 -0
  150. package/src/internal/oauth-server.ts +89 -0
  151. package/src/internal/passkey/admission.ts +361 -0
  152. package/src/internal/passkey/credentials.ts +511 -0
  153. package/src/internal/passkey/enrollment.ts +326 -0
  154. package/src/internal/passkey/flow.ts +1062 -0
  155. package/src/internal/passkey/management.ts +421 -0
  156. package/src/internal/passkey/registration-ceremony.ts +283 -0
  157. package/src/internal/passkey/registration-custody.ts +91 -0
  158. package/src/internal/passkey/registration-write.ts +426 -0
  159. package/src/internal/passkey/state.ts +353 -0
  160. package/src/internal/passkey/target.ts +586 -0
  161. package/src/internal/passkey/write-state.ts +681 -0
  162. package/src/internal/passkey/write-target.ts +452 -0
  163. package/src/internal/passkey-kernel.ts +104 -0
  164. package/src/internal/passkeys.ts +606 -0
  165. package/src/internal/password-kernel.ts +2124 -0
  166. package/src/internal/persistence.ts +588 -0
  167. package/src/internal/phone-kernel.ts +846 -0
  168. package/src/internal/proof-kernel.ts +1954 -0
  169. package/src/internal/query-operations.ts +81 -0
  170. package/src/internal/registration-contract.ts +22 -0
  171. package/src/internal/registration.ts +161 -0
  172. package/src/internal/session-kernel.ts +2217 -0
  173. package/src/internal/sql-persistence.ts +45 -0
  174. package/src/internal/sql-query.ts +298 -0
  175. package/src/internal/sql-table.ts +104 -0
  176. package/src/internal/standalone.ts +30 -0
  177. package/src/internal/step-up-state.ts +139 -0
  178. package/src/internal/storage-mapping.ts +708 -0
  179. package/src/internal/storage-tables.ts +408 -0
  180. package/src/internal/storage-validation.ts +117 -0
  181. package/src/internal/transaction-execution-kernel.ts +313 -0
  182. package/src/internal/transaction-kernel.ts +593 -0
@@ -0,0 +1,421 @@
1
+ /* oxlint-disable no-explicit-any -- existing mapped-row bridge; public adapters preserve table, ID, and Effect types. */
2
+
3
+ import {
4
+ PasskeyCredentialSummary,
5
+ PasskeyRemoved,
6
+ type PasskeyManagementPersistence,
7
+ } from "@yielded/auth/Passkey";
8
+ import { Effect, Schema } from "effect";
9
+
10
+ import type { PersistenceMappingError } from "../mapping-error";
11
+ import type { SqlExpression as SQL, QueryOperations } from "../query-operations";
12
+ import type { makeTransactionKernel } from "../transaction-kernel";
13
+ import type { makePasskeyAdmissionKernel } from "./admission";
14
+ import type { makePasskeyCredentialsKernel } from "./credentials";
15
+ import { CurrentPasskeyTransaction } from "./state";
16
+ import type { makePasskeyStateKernel } from "./state";
17
+ import type { WriteSubject, makePasskeyWriteStateKernel } from "./write-state";
18
+
19
+ type Port = PasskeyManagementPersistence["Service"];
20
+
21
+ export const makePasskeyManagementKernel = (
22
+ operations: QueryOperations,
23
+ admission: Pick<ReturnType<typeof makePasskeyAdmissionKernel>, "lockAdmission">,
24
+ credentials: Pick<
25
+ ReturnType<typeof makePasskeyCredentialsKernel>,
26
+ "readModule" | "readPolicyGuards"
27
+ >,
28
+ state: Pick<
29
+ ReturnType<typeof makePasskeyStateKernel>,
30
+ "credentialKey" | "copiedRow" | "equal" | "invariant" | "sameCredential" | "sameRevision"
31
+ >,
32
+ writeState: Pick<
33
+ ReturnType<typeof makePasskeyWriteStateKernel>,
34
+ | "authorizeAction"
35
+ | "credentialRows"
36
+ | "currentSubject"
37
+ | "invalidate"
38
+ | "invalidationMatches"
39
+ | "jsonStorage"
40
+ | "managementPolicy"
41
+ | "metadataAllowed"
42
+ | "ownedCredential"
43
+ | "removeDigest"
44
+ | "summary"
45
+ >,
46
+ transactions: Pick<ReturnType<typeof makeTransactionKernel>, "both">,
47
+ ) => {
48
+ const { sql } = operations;
49
+ const { lockAdmission } = admission;
50
+ const { readModule, readPolicyGuards } = credentials;
51
+ const { credentialKey, copiedRow, equal, sameCredential, sameRevision } = state;
52
+ const invariant: (value: unknown) => asserts value = state.invariant;
53
+
54
+ const {
55
+ authorizeAction,
56
+ credentialRows,
57
+ currentSubject,
58
+ invalidate,
59
+ invalidationMatches,
60
+ jsonStorage,
61
+ managementPolicy,
62
+ metadataAllowed,
63
+ ownedCredential,
64
+ removeDigest,
65
+ summary,
66
+ } = writeState;
67
+
68
+ const { both } = transactions;
69
+
70
+ const decisionCodec = jsonStorage(
71
+ Schema.Union([
72
+ Schema.TaggedStruct("Renamed", { credential: PasskeyCredentialSummary }),
73
+ Schema.TaggedStruct("Removed", { result: PasskeyRemoved }),
74
+ ]),
75
+ );
76
+
77
+ const intentCodec = jsonStorage(
78
+ Schema.Struct({
79
+ operation: Schema.Literals(["rename", "remove"]),
80
+ credentialId: Schema.String,
81
+ name: Schema.optionalKey(Schema.String),
82
+ }),
83
+ );
84
+
85
+ const command = Effect.fn("passkey.managementCommand")(function* (
86
+ mapping: any,
87
+ subject: WriteSubject,
88
+ commandId: string,
89
+ credentialId: string,
90
+ intent: string,
91
+ ) {
92
+ const owner = yield* CurrentPasskeyTransaction;
93
+ const table = mapping.command;
94
+
95
+ const observation = yield* owner.read(
96
+ table.table,
97
+ equal(table.table, { [table.moduleId]: mapping.moduleId, [table.commandId]: commandId }),
98
+ { limit: 1 },
99
+ );
100
+
101
+ const row = observation.rows[0];
102
+
103
+ if (row === undefined) return { _tag: "New", observation } as const;
104
+ if (
105
+ !mapping.read.subjectIds.equals(row[table.subjectId], subject.nativeId) ||
106
+ row[table.credentialId] !== credentialId ||
107
+ row[table.intent] !== intent
108
+ )
109
+ return { _tag: "Rejected" } as const;
110
+
111
+ return { _tag: "Replay", decision: decisionCodec.decode(row[table.decision]) } as const;
112
+ });
113
+
114
+ const record = Effect.fn("passkey.recordManagementCommand")(function* (
115
+ mapping: any,
116
+ subject: WriteSubject,
117
+ commandId: string,
118
+ credentialId: string,
119
+ intent: string,
120
+ decision: Parameters<typeof decisionCodec.encode>[0],
121
+ retentionUntilMillis: number,
122
+ observation: any,
123
+ ) {
124
+ const owner = yield* CurrentPasskeyTransaction;
125
+ const table = mapping.command;
126
+
127
+ const inserted = yield* owner.insert(
128
+ table.table,
129
+ {
130
+ ...table.encodeInsert({
131
+ moduleId: mapping.moduleId,
132
+ subjectId: subject.nativeId,
133
+ commandId,
134
+ credentialId,
135
+ }),
136
+ [table.moduleId]: mapping.moduleId,
137
+ [table.commandId]: commandId,
138
+ [table.subjectId]: subject.nativeId,
139
+ [table.credentialId]: credentialId,
140
+ [table.intent]: intent,
141
+ [table.decision]: decisionCodec.encode(decision),
142
+ [table.retentionUntil]: mapping.clock.encodeInstant(retentionUntilMillis),
143
+ [table.version]: owner.marker,
144
+ },
145
+ { [table.moduleId]: mapping.moduleId, [table.commandId]: commandId },
146
+ );
147
+
148
+ observation.rows = inserted.rows;
149
+ });
150
+
151
+ const access = Effect.fn("passkey.managementAccess")(function* (
152
+ mapping: any,
153
+ moduleId: string,
154
+ subjectId: Parameters<Port["list"]>[0]["subjectId"],
155
+ mutation: boolean,
156
+ ) {
157
+ if (moduleId !== mapping.moduleId || (yield* readModule(mapping)) === undefined)
158
+ return undefined;
159
+ if (mutation) yield* lockAdmission(mapping);
160
+ const subject = yield* currentSubject(mapping, subjectId);
161
+
162
+ if (subject === undefined) return undefined;
163
+ yield* readPolicyGuards(mapping);
164
+
165
+ return (yield* metadataAllowed(mapping, subject)) ? subject : undefined;
166
+ });
167
+
168
+ const listCredentials = Effect.fn("passkey.listCredentials")(function* (
169
+ mapping: any,
170
+ input: Parameters<Port["list"]>[0],
171
+ ) {
172
+ const subject = yield* access(mapping, input.moduleId, input.subjectId, false);
173
+
174
+ invariant(subject !== undefined);
175
+ const rows = [...(yield* credentialRows(mapping, subject))];
176
+
177
+ rows.sort((left, right) => {
178
+ const a = left[mapping.read.credential.credentialId],
179
+ b = right[mapping.read.credential.credentialId];
180
+
181
+ return a < b ? -1 : a > b ? 1 : 0;
182
+ });
183
+
184
+ const eligible = rows.filter(
185
+ (row) =>
186
+ input.cursor === undefined || row[mapping.read.credential.credentialId] > input.cursor,
187
+ );
188
+
189
+ const page = eligible.slice(0, input.limit);
190
+ const credentials = [];
191
+
192
+ for (const row of page) {
193
+ invariant((yield* ownedCredential(mapping, subject, row)) !== undefined);
194
+ credentials.push(summary(mapping, row));
195
+ }
196
+
197
+ return {
198
+ credentials,
199
+ ...(eligible.length > input.limit ? { cursor: credentials.at(-1)!.credentialId } : {}),
200
+ };
201
+ });
202
+
203
+ const renameCredential = Effect.fn("passkey.renameCredential")(function* (
204
+ mapping: any,
205
+ input: Parameters<Port["rename"]>[0],
206
+ ) {
207
+ const subject = yield* access(mapping, input.moduleId, input.subjectId, true);
208
+
209
+ if (subject === undefined) return { _tag: "Rejected" } as const;
210
+
211
+ const intent = intentCodec.encode({
212
+ operation: "rename",
213
+ credentialId: input.credentialId,
214
+ name: input.name,
215
+ });
216
+
217
+ const prior = yield* command(mapping, subject, input.commandId, input.credentialId, intent);
218
+
219
+ if (prior._tag === "Rejected") return prior;
220
+ const rows = yield* credentialRows(mapping, subject);
221
+
222
+ const row = rows.find(
223
+ (item) => item[mapping.read.credential.credentialId] === input.credentialId,
224
+ );
225
+
226
+ if (row === undefined || (yield* ownedCredential(mapping, subject, row)) === undefined)
227
+ return { _tag: "Rejected" } as const;
228
+ if (prior._tag === "Replay")
229
+ return prior.decision._tag === "Renamed"
230
+ ? { ...prior.decision, replayed: true }
231
+ : ({ _tag: "Rejected" } as const);
232
+ const owner = yield* CurrentPasskeyTransaction;
233
+ const table = mapping.read.credential;
234
+
235
+ yield* owner.update(
236
+ table.table,
237
+ { [table.credentialId]: input.credentialId, [table.subjectId]: subject.nativeId },
238
+ { [mapping.write.credential.name]: input.name },
239
+ );
240
+ const credential = summary(mapping, { ...row, [mapping.write.credential.name]: input.name });
241
+
242
+ yield* record(
243
+ mapping,
244
+ subject,
245
+ input.commandId,
246
+ input.credentialId,
247
+ intent,
248
+ { _tag: "Renamed", credential },
249
+ input.retentionUntilMillis,
250
+ prior.observation,
251
+ );
252
+
253
+ return { _tag: "Renamed", credential, replayed: false } as const;
254
+ });
255
+
256
+ const inspectRemove = Effect.fn("passkey.inspectRemove")(function* (
257
+ mapping: any,
258
+ input: Parameters<Port["inspectRemove"]>[0],
259
+ ) {
260
+ const subject = yield* access(mapping, input.moduleId, input.subjectId, false);
261
+
262
+ if (subject === undefined) return { _tag: "Rejected" } as const;
263
+
264
+ const prior = yield* command(
265
+ mapping,
266
+ subject,
267
+ input.commandId,
268
+ input.credentialId,
269
+ intentCodec.encode({ operation: "remove", credentialId: input.credentialId }),
270
+ );
271
+
272
+ if (prior._tag === "Rejected") return prior;
273
+ if (prior._tag === "Replay")
274
+ return prior.decision._tag === "Removed"
275
+ ? ({ _tag: "Replay", result: { ...prior.decision.result, replayed: true } } as const)
276
+ : ({ _tag: "Rejected" } as const);
277
+
278
+ const row = (yield* credentialRows(mapping, subject)).find(
279
+ (item) => item[mapping.read.credential.credentialId] === input.credentialId,
280
+ );
281
+
282
+ const credential =
283
+ row === undefined ? undefined : yield* ownedCredential(mapping, subject, row);
284
+
285
+ return credential === undefined
286
+ ? ({ _tag: "Rejected" } as const)
287
+ : ({ _tag: "Target", credential } as const);
288
+ });
289
+
290
+ const removeCredential = Effect.fn("passkey.removeCredential")(function* (
291
+ mapping: any,
292
+ input: Parameters<Port["remove"]>[0],
293
+ ) {
294
+ const subject = yield* access(
295
+ mapping,
296
+ input.moduleId,
297
+ input.credential.revision.subjectId,
298
+ true,
299
+ );
300
+
301
+ if (subject === undefined) return { _tag: "Rejected" } as const;
302
+ const id = input.credential.credentialId;
303
+ const intent = intentCodec.encode({ operation: "remove", credentialId: id });
304
+ const prior = yield* command(mapping, subject, input.commandId, id, intent);
305
+
306
+ if (prior._tag === "Rejected") return prior;
307
+ if (prior._tag === "Replay")
308
+ return prior.decision._tag === "Removed"
309
+ ? ({ _tag: "Removed", result: { ...prior.decision.result, replayed: true } } as const)
310
+ : ({ _tag: "Rejected" } as const);
311
+
312
+ const row = (yield* credentialRows(mapping, subject)).find(
313
+ (item) => item[mapping.read.credential.credentialId] === id,
314
+ );
315
+
316
+ const credential =
317
+ row === undefined ? undefined : yield* ownedCredential(mapping, subject, row);
318
+
319
+ const policy = managementPolicy(mapping, subject);
320
+
321
+ if (
322
+ credential === undefined ||
323
+ !sameCredential(input.credential, credential) ||
324
+ !sameRevision(input.credential.revision, credential.revision) ||
325
+ !invalidationMatches(mapping, input.invalidation, policy) ||
326
+ !invalidationMatches(mapping, input.invalidation, input.management)
327
+ )
328
+ return { _tag: "Rejected" } as const;
329
+ if (
330
+ !(yield* authorizeAction(
331
+ mapping,
332
+ subject,
333
+ input.authorization,
334
+ {
335
+ action: "remove",
336
+ commandId: input.commandId,
337
+ flowId: input.commandId,
338
+ bindingDigest: removeDigest(mapping.moduleId, input.commandId, input.credential),
339
+ revision: input.credential.revision,
340
+ },
341
+ {
342
+ ...policy,
343
+ maximumEvidenceAgeMillis: Math.min(
344
+ policy.maximumEvidenceAgeMillis,
345
+ input.management.maximumEvidenceAgeMillis,
346
+ ),
347
+ },
348
+ ))
349
+ )
350
+ return { _tag: "Rejected" } as const;
351
+ const owner = yield* CurrentPasskeyTransaction;
352
+
353
+ const remainingEffect: Effect.Effect<SQL, PersistenceMappingError> =
354
+ mapping.write.policy.remainingSignIn(subject.nativeId, id, copiedRow(subject.row));
355
+
356
+ const remaining = yield* remainingEffect;
357
+
358
+ if (!(yield* owner.check(remaining))) return { _tag: "LastSignInMethod" } as const;
359
+ const read = mapping.read;
360
+ const table = read.credential;
361
+
362
+ yield* owner.update(
363
+ table.table,
364
+ { [table.credentialId]: id, [table.subjectId]: subject.nativeId },
365
+ {
366
+ [table.status]: mapping.write.credential.removedStatus,
367
+ [table.credentialRevision]: owner.marker,
368
+ },
369
+ );
370
+ const factor = read.authority;
371
+
372
+ yield* owner.update(
373
+ factor.table,
374
+ { [factor.subjectId]: subject.nativeId, [factor.credentialId]: id },
375
+ { [factor.status]: mapping.write.authority.removedStatus, [factor.revision]: owner.marker },
376
+ );
377
+ for (const observation of owner.observations) {
378
+ if (observation.table === table.table)
379
+ observation.rows = observation.rows.filter((item) => item[table.credentialId] !== id);
380
+ if (observation.table === factor.table)
381
+ observation.rows = observation.rows.filter((item) => item[factor.credentialId] !== id);
382
+ }
383
+ const tuple = read.credentialOwnership;
384
+
385
+ yield* owner.update(
386
+ tuple.table,
387
+ { [tuple.credentialKey]: credentialKey(credential.rpId, credential.protocolCredentialId) },
388
+ {
389
+ [tuple.state]: mapping.write.credentialOwnership.removedState,
390
+ [tuple.version]: owner.marker,
391
+ },
392
+ );
393
+ yield* invalidate(mapping, subject, input.invalidation);
394
+ owner.postconditions.push(
395
+ remaining,
396
+ sql`not exists(select 1 from ${table.table} where ${both(equal(table.table, { [table.credentialId]: id }), table.activeCondition)})`,
397
+ sql`not exists(select 1 from ${factor.table} where ${both(equal(factor.table, { [factor.subjectId]: subject.nativeId, [factor.credentialId]: id }), factor.activeCondition)})`,
398
+ );
399
+
400
+ const result = {
401
+ credentialId: credential.credentialId,
402
+ replayed: false,
403
+ invalidation: input.invalidation,
404
+ };
405
+
406
+ yield* record(
407
+ mapping,
408
+ subject,
409
+ input.commandId,
410
+ id,
411
+ intent,
412
+ { _tag: "Removed", result },
413
+ input.retentionUntilMillis,
414
+ prior.observation,
415
+ );
416
+
417
+ return { _tag: "Removed", result } as const;
418
+ });
419
+
420
+ return { listCredentials, renameCredential, inspectRemove, removeCredential };
421
+ };
@@ -0,0 +1,283 @@
1
+ /* oxlint-disable no-explicit-any -- existing mapped-row bridge; public adapters preserve table, ID, and Effect types. */
2
+
3
+ import {
4
+ PasskeyClaim,
5
+ type PasskeyAccess,
6
+ type PasskeyCeremony,
7
+ type PasskeyMethodPolicy,
8
+ snapshotPasskeySync,
9
+ } from "@yielded/auth/Passkey";
10
+ import { Effect } from "effect";
11
+
12
+ import type { QueryOperations } from "../query-operations";
13
+ import type { makeTransactionKernel } from "../transaction-kernel";
14
+ import type { makePasskeyAdmissionKernel } from "./admission";
15
+ import type { makePasskeyCredentialsKernel } from "./credentials";
16
+ import type { makePasskeyFlowKernel } from "./flow";
17
+ import type { makePasskeyRegistrationCustodyKernel } from "./registration-custody";
18
+ import { CurrentPasskeyTransaction } from "./state";
19
+ import type { makePasskeyStateKernel } from "./state";
20
+
21
+ export const makePasskeyRegistrationCeremonyKernel = (
22
+ operations: QueryOperations,
23
+ admission: Pick<
24
+ ReturnType<typeof makePasskeyAdmissionKernel>,
25
+ "admissionCondition" | "guardAdmission" | "guardChargeSet" | "lockAdmission" | "readCharges"
26
+ >,
27
+ credentials: Pick<
28
+ ReturnType<typeof makePasskeyCredentialsKernel>,
29
+ "readModule" | "readPolicyGuards"
30
+ >,
31
+ flow: Pick<
32
+ ReturnType<typeof makePasskeyFlowKernel>,
33
+ "compatiblePolicy" | "liveCondition" | "matchesAccess" | "readFlow" | "terminalFlow"
34
+ >,
35
+ registrationCustody: Pick<
36
+ ReturnType<typeof makePasskeyRegistrationCustodyKernel>,
37
+ "releaseRegistrationCustody"
38
+ >,
39
+ state: Pick<
40
+ ReturnType<typeof makePasskeyStateKernel>,
41
+ "ceremonyStorage" | "equal" | "handleKey" | "invariant" | "mappedColumns" | "policyStorage"
42
+ >,
43
+ transactions: Pick<ReturnType<typeof makeTransactionKernel>, "both">,
44
+ ) => {
45
+ const { sql } = operations;
46
+
47
+ const { admissionCondition, guardAdmission, guardChargeSet, lockAdmission, readCharges } =
48
+ admission;
49
+
50
+ const { readModule, readPolicyGuards } = credentials;
51
+ const { compatiblePolicy, liveCondition, matchesAccess, readFlow, terminalFlow } = flow;
52
+ const { releaseRegistrationCustody } = registrationCustody;
53
+ const { ceremonyStorage, equal, handleKey, mappedColumns, policyStorage } = state;
54
+ const invariant: (value: unknown) => asserts value = state.invariant;
55
+ const { both } = transactions;
56
+
57
+ /** The issuing registration authority owns the application value and reservation.
58
+ * This facet only compares its opaque custody and cannot issue or provision. */
59
+ const custody = Effect.fn("passkey.registrationCustody")(function* (
60
+ mapping: any,
61
+ ceremony: PasskeyCeremony,
62
+ pending: boolean,
63
+ ) {
64
+ const owner = yield* CurrentPasskeyTransaction;
65
+
66
+ invariant(ceremony.context._tag === "Registration");
67
+
68
+ const context = ceremony.context,
69
+ intent = mapping.intent,
70
+ handle = mapping.handle;
71
+
72
+ const identity = { [intent.moduleId]: mapping.moduleId, [intent.flowId]: ceremony.flowId };
73
+
74
+ const advisory = (yield* owner.read(intent.table, equal(intent.table, identity), {
75
+ limit: 1,
76
+ lock: false,
77
+ observe: false,
78
+ })).rows[0];
79
+
80
+ if (advisory === undefined) return false;
81
+ const hashedHandle = handleKey(ceremony.profile.rpId, context.userHandle);
82
+
83
+ const held = (yield* owner.read(
84
+ handle.table,
85
+ equal(handle.table, { [handle.handleKey]: hashedHandle }),
86
+ { limit: 1, columns: mappedColumns(handle) },
87
+ )).rows[0];
88
+
89
+ if (
90
+ held === undefined ||
91
+ !handle.isReservedState(held[handle.state]) ||
92
+ held[handle.handleKey] !== hashedHandle ||
93
+ held[handle.rpId] !== ceremony.profile.rpId ||
94
+ held[handle.userHandle] !== context.userHandle ||
95
+ held[handle.reservationId] !== advisory[intent.reservationId]
96
+ )
97
+ return false;
98
+ const heldCondition = sql`exists (select 1 from ${handle.table} where ${both(owner.exact(handle.table, held), handle.reservedCondition)})`;
99
+
100
+ if (!(yield* owner.check(heldCondition))) return false;
101
+
102
+ const row = (yield* owner.read(intent.table, equal(intent.table, identity), {
103
+ limit: 1,
104
+ columns: mappedColumns(intent),
105
+ })).rows[0];
106
+
107
+ if (
108
+ row === undefined ||
109
+ row[intent.commandId] !== ceremony.commandId ||
110
+ row[intent.fingerprint] !== context.fingerprint ||
111
+ row[intent.handleKey] !== hashedHandle ||
112
+ row[intent.reservationId] !== held[handle.reservationId] ||
113
+ row[intent.ceremonySnapshot] !== ceremonyStorage.encode(ceremony) ||
114
+ typeof row[intent.applicationSnapshot] !== "string" ||
115
+ new TextEncoder().encode(row[intent.applicationSnapshot]).length > 1048576 ||
116
+ (pending && !intent.isPendingState(row[intent.state]))
117
+ )
118
+ return false;
119
+ const condition = pending ? intent.pendingCondition : intent.custodyCondition;
120
+
121
+ if (
122
+ !(yield* owner.check(
123
+ sql`exists (select 1 from ${intent.table} where ${both(owner.exact(intent.table, row), condition)})`,
124
+ ))
125
+ )
126
+ return false;
127
+
128
+ return [
129
+ heldCondition,
130
+ sql`exists (select 1 from ${intent.table} where ${both(owner.exact(intent.table, row), condition)})`,
131
+ ];
132
+ });
133
+
134
+ const contextRegistration = Effect.fn("passkey.contextRegistration")(function* (
135
+ mapping: any,
136
+ access: PasskeyAccess,
137
+ ) {
138
+ const owner = yield* CurrentPasskeyTransaction;
139
+
140
+ if (access.moduleId !== mapping.moduleId) return undefined;
141
+ const current = yield* readModule(mapping);
142
+
143
+ if (current === undefined) return undefined;
144
+ yield* readPolicyGuards(mapping);
145
+ const table = mapping.flow;
146
+
147
+ const advisory = (yield* owner.read(
148
+ table.table,
149
+ equal(table.table, { [table.moduleId]: mapping.moduleId, [table.flowId]: access.flowId }),
150
+ { limit: 1, lock: false, observe: false },
151
+ )).rows[0];
152
+
153
+ if (advisory === undefined) return undefined;
154
+ const ceremony = ceremonyStorage.decode(advisory[table.snapshot]);
155
+
156
+ if (ceremony.purpose !== "registration" || !matchesAccess(ceremony, access)) return undefined;
157
+ const held = yield* custody(mapping, ceremony, true);
158
+
159
+ if (!held) return undefined;
160
+ const read = yield* readFlow(mapping, access.flowId);
161
+
162
+ if (
163
+ read === undefined ||
164
+ read.state !== "Pending" ||
165
+ ceremonyStorage.encode(read.ceremony) !== ceremonyStorage.encode(ceremony) ||
166
+ !compatiblePolicy(read.ceremony, read.policy, current) ||
167
+ !(yield* owner.check(liveCondition(mapping, ceremony)))
168
+ )
169
+ return undefined;
170
+ owner.postconditions.push(...held, liveCondition(mapping, ceremony));
171
+
172
+ return read.ceremony;
173
+ });
174
+
175
+ const claimRegistration = Effect.fn("passkey.claimRegistration")(function* (
176
+ mapping: any,
177
+ input: {
178
+ readonly access: PasskeyAccess;
179
+ readonly policy: PasskeyMethodPolicy;
180
+ readonly ceremony: PasskeyCeremony;
181
+ readonly claimId: string;
182
+ },
183
+ ) {
184
+ const owner = yield* CurrentPasskeyTransaction;
185
+ const current = yield* readModule(mapping);
186
+
187
+ if (current === undefined || input.access.moduleId !== mapping.moduleId)
188
+ return { _tag: "Rejected" } as const;
189
+ yield* lockAdmission(mapping);
190
+ yield* readPolicyGuards(mapping);
191
+ const held = yield* custody(mapping, input.ceremony, true);
192
+ const read = yield* readFlow(mapping, input.access.flowId);
193
+
194
+ if (
195
+ read === undefined ||
196
+ read.state !== "Pending" ||
197
+ !matchesAccess(read.ceremony, input.access)
198
+ )
199
+ return { _tag: "Rejected" } as const;
200
+ invariant(ceremonyStorage.encode(read.ceremony) === ceremonyStorage.encode(input.ceremony));
201
+ if (
202
+ !held ||
203
+ !compatiblePolicy(read.ceremony, read.policy, current) ||
204
+ policyStorage.encode(current) !== policyStorage.encode(input.policy) ||
205
+ !(yield* owner.check(liveCondition(mapping, read.ceremony))) ||
206
+ !(yield* owner.check(
207
+ admissionCondition(
208
+ mapping,
209
+ [read.policy, current],
210
+ read.ceremony,
211
+ undefined,
212
+ [],
213
+ false,
214
+ false,
215
+ ),
216
+ ))
217
+ ) {
218
+ if (held) yield* releaseRegistrationCustody(mapping, read.ceremony);
219
+ yield* terminalFlow(mapping, read, "Rejected");
220
+
221
+ return { _tag: "Rejected" } as const;
222
+ }
223
+ yield* readCharges(mapping, read.ceremony, read.policy, undefined);
224
+ const claimedAtMillis = yield* owner.now(mapping.clock);
225
+
226
+ const claim = snapshotPasskeySync(PasskeyClaim, {
227
+ ceremony: read.ceremony,
228
+ claimId: input.claimId,
229
+ claimedAtMillis,
230
+ claimExpiresAtMillis: Math.min(
231
+ claimedAtMillis + read.ceremony.claimLifetimeMillis,
232
+ read.ceremony.expiresAtMillis,
233
+ ),
234
+ });
235
+
236
+ const table = mapping.flow;
237
+
238
+ yield* owner.update(
239
+ table.table,
240
+ { [table.moduleId]: mapping.moduleId, [table.flowId]: read.ceremony.flowId },
241
+ {
242
+ [table.state]: table.states.Claimed,
243
+ [table.version]: owner.marker,
244
+ [table.claimId]: claim.claimId,
245
+ [table.claimedAt]: mapping.clock.encodeInstant(claim.claimedAtMillis),
246
+ [table.claimExpiresAt]: mapping.clock.encodeInstant(claim.claimExpiresAtMillis),
247
+ },
248
+ );
249
+ yield* guardChargeSet(mapping, read.ceremony, undefined);
250
+ yield* guardAdmission(mapping, [read.policy, current], read.ceremony, undefined);
251
+ owner.postconditions.push(...held, liveCondition(mapping, read.ceremony, claim));
252
+
253
+ return { _tag: "Claimed", claim } as const;
254
+ });
255
+
256
+ const settleRegistration = Effect.fn("passkey.settleRegistration")(function* (
257
+ mapping: any,
258
+ claim: PasskeyClaim,
259
+ outcome: "Rejected" | "Ambiguous",
260
+ ) {
261
+ yield* readModule(mapping);
262
+ yield* lockAdmission(mapping);
263
+ yield* readPolicyGuards(mapping);
264
+ const read = yield* readFlow(mapping, claim.ceremony.flowId);
265
+
266
+ if (
267
+ read === undefined ||
268
+ read.state !== "Claimed" ||
269
+ read.claim === undefined ||
270
+ read.claim.claimId !== claim.claimId ||
271
+ read.claim.claimedAtMillis !== claim.claimedAtMillis ||
272
+ read.claim.claimExpiresAtMillis !== claim.claimExpiresAtMillis ||
273
+ ceremonyStorage.encode(read.ceremony) !== ceremonyStorage.encode(claim.ceremony)
274
+ )
275
+ return "Rejected" as const;
276
+
277
+ if (outcome === "Rejected") yield* releaseRegistrationCustody(mapping, claim.ceremony);
278
+
279
+ return yield* terminalFlow(mapping, read, outcome);
280
+ });
281
+
282
+ return { contextRegistration, claimRegistration, settleRegistration };
283
+ };