@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,606 @@
1
+ import { digest } from "@yielded/auth-crypto";
2
+ import {
3
+ PasskeyCredentials,
4
+ PasskeyCredential,
5
+ PasskeyEnrollmentContext,
6
+ PasskeyManagementPersistence,
7
+ PasskeyManagementPolicy,
8
+ PasskeyMethodPolicy,
9
+ PasskeyPersistence,
10
+ PasskeyProfile,
11
+ PasskeyRequirement,
12
+ PasskeyUnavailable,
13
+ } from "@yielded/auth/Passkey";
14
+ import { Context, Effect, Schema } from "effect";
15
+ import { SqlClient } from "effect/unstable/sql";
16
+
17
+ import {
18
+ PersistenceConfigurationError,
19
+ type MappingInput,
20
+ type PasskeyFeature,
21
+ } from "./configuration";
22
+ import { PersistenceMappingError } from "./mapping-error";
23
+ import {
24
+ requiredPasskeyCredentialConstraints,
25
+ requiredPasskeyPersistenceConstraints,
26
+ type PasskeyCredentialMapping,
27
+ type PasskeyPersistenceMapping,
28
+ } from "./models/passkey-model";
29
+ import {
30
+ requiredPasskeyManagementConstraints,
31
+ type PasskeyManagementMapping,
32
+ } from "./models/passkey-write-model";
33
+ import { makePasskeyKernel } from "./passkey-kernel";
34
+ import type { PasskeyTargetConfiguration } from "./passkey/target";
35
+ import type { Backend } from "./persistence";
36
+ import type { ProofSqlDatabase } from "./proof-kernel";
37
+ import type { SqlExpression as SQL, QueryOperations, TableModel } from "./query-operations";
38
+ import { requireStandalone } from "./standalone";
39
+ import { storageTables, type StorageRole } from "./storage-tables";
40
+
41
+ const profileJson = Schema.fromJsonString(PasskeyProfile);
42
+ const policyJson = Schema.fromJsonString(PasskeyMethodPolicy);
43
+
44
+ const seedJson = Schema.fromJsonString(
45
+ Schema.Struct({
46
+ policy: PasskeyMethodPolicy,
47
+ management: Schema.optionalKey(PasskeyManagementPolicy),
48
+ }),
49
+ );
50
+
51
+ const ModuleRow = Schema.Struct({
52
+ active: Schema.Boolean,
53
+ policy: Schema.String,
54
+ policyRevision: Schema.String,
55
+ });
56
+
57
+ const credentialData = PasskeyCredential.mapFields(
58
+ ({ revision: _revision, active: _active, ...fields }) => fields,
59
+ );
60
+
61
+ const unavailable = () => PasskeyUnavailable.make({});
62
+ const configurationError = (reason: string) => PersistenceConfigurationError.make({ reason });
63
+
64
+ type ReadMapping = PasskeyCredentialMapping<
65
+ TableModel,
66
+ TableModel,
67
+ TableModel,
68
+ TableModel,
69
+ TableModel,
70
+ unknown
71
+ >;
72
+ type BaseMapping = PasskeyPersistenceMapping<
73
+ ReadMapping,
74
+ TableModel,
75
+ TableModel,
76
+ TableModel,
77
+ TableModel,
78
+ unknown
79
+ >;
80
+ type ManagementMapping = PasskeyManagementMapping<
81
+ TableModel,
82
+ TableModel,
83
+ TableModel,
84
+ TableModel,
85
+ TableModel,
86
+ TableModel,
87
+ TableModel,
88
+ TableModel,
89
+ TableModel,
90
+ TableModel,
91
+ unknown
92
+ >;
93
+
94
+ /** Share the same passkey algorithms across raw SQL and Drizzle compilers. */
95
+ export const makeComposedPasskeys = (
96
+ operations: QueryOperations,
97
+ ): Backend<object, never>["passkeys"] => {
98
+ const { and, eq, getTableColumns, inArray, or, sql } = operations;
99
+ const { target, writeTarget } = makePasskeyKernel(operations);
100
+
101
+ const {
102
+ makeTargetPasskeyCredentials,
103
+ makeTargetPasskeyEnrollmentContext,
104
+ makeTargetPasskeyPersistence,
105
+ } = target;
106
+
107
+ const { makeTargetPasskeyManagement } = writeTarget;
108
+
109
+ const mappings = (
110
+ storage: MappingInput,
111
+ namespace: string,
112
+ dialect: "pg" | "sqlite",
113
+ passwordModules: ReadonlyArray<string>,
114
+ signInProfiles: ReadonlyArray<PasskeyProfile>,
115
+ ) => {
116
+ const table = (role: StorageRole) => {
117
+ const value = storage.tables[role];
118
+
119
+ if (value === undefined) throw configurationError(`Missing ${role} mapping`);
120
+
121
+ // Backend validation owns the foreign table shape. Row values are decoded below.
122
+ return value;
123
+ };
124
+
125
+ const mapped = <Role extends StorageRole>(role: Role) => ({
126
+ table: table(role),
127
+ ...(Object.fromEntries(Object.keys(storageTables[role].columns).map((key) => [key, key])) as {
128
+ readonly [Key in keyof (typeof storageTables)[Role]["columns"]]: Key;
129
+ }),
130
+ });
131
+
132
+ const column = (role: StorageRole, key: string) => getTableColumns(table(role))[key];
133
+ const s = storage.subjects;
134
+ const subject = s.table;
135
+ const sc = getTableColumns(subject);
136
+ const active = (value: unknown) => value === true;
137
+ const owned = (value: unknown) => value === "owned";
138
+
139
+ const read: ReadMapping = {
140
+ subject: {
141
+ table: subject,
142
+ id: s.id,
143
+ status: s.status,
144
+ securityRevision: s.securityRevision,
145
+ decodeId: (row) => row[s.id],
146
+ isActiveStatus: (value) => Object.is(value, s.activeValue),
147
+ activeCondition: eq(sc[s.status], s.activeValue),
148
+ },
149
+ credential: {
150
+ ...mapped("passkeyCredentials"),
151
+ status: "active",
152
+ decode: (row) =>
153
+ Schema.decodeUnknownSync(credentialData)({
154
+ ...row,
155
+ profile: Schema.decodeUnknownSync(profileJson)(row.profile),
156
+ }),
157
+ decodeSubjectId: (row) => row.subjectId,
158
+ isActiveStatus: active,
159
+ activeCondition: eq(column("passkeyCredentials", "active"), true),
160
+ },
161
+ authority: {
162
+ ...mapped("credentials"),
163
+ status: "active",
164
+ isActiveStatus: active,
165
+ activeCondition: eq(column("credentials", "active"), true),
166
+ },
167
+ credentialOwnership: {
168
+ ...mapped("passkeyOwnership"),
169
+ isOwnedState: owned,
170
+ ownedCondition: eq(column("passkeyOwnership", "state"), "owned"),
171
+ decodeSubjectId: (row) => row.subjectId,
172
+ },
173
+ handleOwnership: {
174
+ ...mapped("passkeyHandles"),
175
+ isOwnedState: owned,
176
+ ownedCondition: eq(column("passkeyHandles", "state"), "owned"),
177
+ decodeSubjectId: (row) => row.subjectId,
178
+ },
179
+ subjectIds: { toNative: s.toNativeSync, toSubject: s.toSubjectSync, equals: Object.is },
180
+ constraints: requiredPasskeyCredentialConstraints,
181
+ };
182
+
183
+ const base = (feature: PasskeyFeature): BaseMapping => ({
184
+ moduleId: feature.moduleId,
185
+ authorityScope: namespace,
186
+ read,
187
+ module: {
188
+ ...mapped("passkeyModules"),
189
+ status: "active",
190
+ policyColumns: ["policy", "policyRevision"],
191
+ isActiveStatus: active,
192
+ activeCondition: eq(column("passkeyModules", "active"), true),
193
+ decodeMethodPolicy: (row) => Schema.decodeUnknownSync(policyJson)(row.policy),
194
+ },
195
+ flow: {
196
+ ...mapped("passkeyFlows"),
197
+ states: {
198
+ Pending: "Pending",
199
+ Claimed: "Claimed",
200
+ Verified: "Verified",
201
+ Rejected: "Rejected",
202
+ Ambiguous: "Ambiguous",
203
+ RegistrationAccepted: "RegistrationAccepted",
204
+ ProvisioningPending: "ProvisioningPending",
205
+ },
206
+ encodeInsert: () => ({}),
207
+ },
208
+ admission: mapped("passkeyAdmissions"),
209
+ charge: { ...mapped("passkeyCharges"), encodeInsert: () => ({}) },
210
+ clock: {
211
+ encodeInstant: storage.encodeInstant,
212
+ decodeInstant: storage.decodeInstantSync,
213
+ engineNowMillis:
214
+ dialect === "pg"
215
+ ? sql`cast(extract(epoch from clock_timestamp()) * 1000 as bigint)`
216
+ : sql`cast(round((julianday('now') - 2440587.5) * 86400000) as integer)`,
217
+ toMillis: (expression) => expression,
218
+ fromMillis: (expression) => expression,
219
+ },
220
+ telemetry: { lastUsedAt: "lastUsedAt", encodeBackupState: (value) => value },
221
+ constraints: requiredPasskeyPersistenceConstraints,
222
+ });
223
+
224
+ const management = (feature: PasskeyFeature): ManagementMapping => {
225
+ const policy = feature.managementPolicy;
226
+
227
+ if (policy === undefined)
228
+ throw configurationError(`Missing passkey management policy for ${feature.moduleId}`);
229
+ const c = getTableColumns(table("passkeyCredentials"));
230
+ const f = getTableColumns(table("credentials"));
231
+
232
+ const metadata = (subjectId: unknown) =>
233
+ sql`exists(select 1 from ${subject} where ${sc[s.id]} = ${subjectId} and ${sc[s.status]} = ${s.activeValue})`;
234
+
235
+ return {
236
+ ...base(feature),
237
+ write: {
238
+ credential: {
239
+ name: "name",
240
+ createdAt: "createdAt",
241
+ encodeInsert: ({ credential, subjectId, summary, marker }) => ({
242
+ credentialId: credential.credentialId,
243
+ subjectId,
244
+ rpId: credential.rpId,
245
+ protocolCredentialId: credential.protocolCredentialId,
246
+ userHandle: credential.userHandle,
247
+ credentialKey: "",
248
+ handleKey: "",
249
+ publicKey: credential.publicKey,
250
+ algorithm: credential.algorithm,
251
+ profile: Schema.encodeSync(profileJson)(credential.profile),
252
+ credentialRevision: marker,
253
+ active: true,
254
+ primarySignIn: credential.primarySignIn,
255
+ enrollmentUserVerified: credential.enrollmentUserVerified,
256
+ backupEligible: credential.backupEligible,
257
+ backupState: credential.backupState,
258
+ counter: credential.counter,
259
+ maximumCounter: credential.maximumCounter,
260
+ name: summary.name,
261
+ createdAt: storage.encodeInstant(summary.createdAtMillis),
262
+ }),
263
+ encodePrimarySignIn: (value) => value,
264
+ encodeEnrollmentUserVerified: (value) => value,
265
+ encodeBackupEligible: (value) => value,
266
+ activeStatus: true,
267
+ removedStatus: false,
268
+ },
269
+ authority: {
270
+ encodeInsert: ({ subjectId, credential, marker }) => ({
271
+ subjectId,
272
+ credentialId: credential.credentialId,
273
+ revision: marker,
274
+ active: true,
275
+ }),
276
+ activeStatus: true,
277
+ removedStatus: false,
278
+ },
279
+ credentialOwnership: {
280
+ encodeInsert: ({ credential, marker }) => ({
281
+ credentialKey: "",
282
+ rpId: credential.rpId,
283
+ protocolCredentialId: credential.protocolCredentialId,
284
+ state: "owned",
285
+ version: marker,
286
+ }),
287
+ ownedState: "owned",
288
+ removedState: "removed",
289
+ },
290
+ handleOwnership: {
291
+ encodeInsert: ({ subjectId, rpId, userHandle, marker }) => ({
292
+ subjectId,
293
+ rpId,
294
+ userHandle,
295
+ handleKey: "",
296
+ state: "owned",
297
+ version: marker,
298
+ }),
299
+ ownedState: "owned",
300
+ },
301
+ policy: {
302
+ subjectColumns: Object.keys(sc),
303
+ management: () => policy,
304
+ requirement: s.actionRequirements,
305
+ metadata,
306
+ action: metadata,
307
+ remainingSignIn: (subjectId, excluded, row) =>
308
+ Effect.gen(function* () {
309
+ const requirement = yield* Schema.decodeUnknownEffect(PasskeyRequirement)(
310
+ yield* s.requirements(row),
311
+ );
312
+
313
+ const alternatives: SQL[] = [];
314
+
315
+ // Only retain complete sign-in paths enabled by this Auth definition.
316
+ // More involved combinations require an explicit application's predicate.
317
+ if (
318
+ requirement.alternatives.some(
319
+ (alternative) =>
320
+ alternative.minimumCredentials === 1 &&
321
+ alternative.factors.every((factor) => factor === "possession"),
322
+ )
323
+ ) {
324
+ const profiles = signInProfiles.map((profile) =>
325
+ Schema.encodeSync(profileJson)(profile),
326
+ );
327
+
328
+ alternatives.push(sql`exists(
329
+ select 1 from ${table("passkeyCredentials")} join ${table("credentials")}
330
+ on ${f.credentialId} = ${c.credentialId} and ${f.subjectId} = ${c.subjectId}
331
+ and ${f.revision} = ${c.credentialRevision} and ${f.active} = ${true}
332
+ where ${c.subjectId} = ${subjectId} and ${c.credentialId} <> ${excluded}
333
+ and ${c.active} = ${true} and ${c.primarySignIn} = ${true}
334
+ and ${c.enrollmentUserVerified} = ${true} and ${inArray(c.profile, profiles)}
335
+ )`);
336
+ }
337
+ if (
338
+ passwordModules.length > 0 &&
339
+ requirement.alternatives.some(
340
+ (alternative) =>
341
+ alternative.minimumCredentials === 1 &&
342
+ !alternative.userVerified &&
343
+ !alternative.phishingResistant &&
344
+ alternative.factors.every((factor) => factor === "knowledge"),
345
+ )
346
+ ) {
347
+ const p = getTableColumns(table("passwords"));
348
+ const i = getTableColumns(table("identifiers"));
349
+
350
+ alternatives.push(sql`exists(
351
+ select 1 from ${table("passwords")} join ${table("credentials")}
352
+ on ${f.credentialId} = ${p.credentialId} and ${f.subjectId} = ${p.subjectId}
353
+ and ${f.revision} = ${p.credentialRevision} and ${f.active} = ${true}
354
+ join ${table("identifiers")} on ${i.subjectId} = ${p.subjectId}
355
+ and ${i.active} = ${true} and ${i.namespace} = 'email'
356
+ where ${p.subjectId} = ${subjectId} and ${p.credentialId} <> ${excluded}
357
+ and ${inArray(p.moduleId, passwordModules)}
358
+ )`);
359
+ }
360
+
361
+ return or(...alternatives) ?? sql`false`;
362
+ }).pipe(
363
+ Effect.mapError((cause) =>
364
+ PersistenceMappingError.make({ operation: "mapping", cause }),
365
+ ),
366
+ ),
367
+ },
368
+ },
369
+ command: { ...mapped("passkeyCommands"), encodeInsert: (input) => ({ ...input }) },
370
+ managementConstraints: requiredPasskeyManagementConstraints,
371
+ invalidation: {
372
+ window: {
373
+ trigger: "credential-change",
374
+ existingSessions: "immediate",
375
+ maximumExposureMillis: 0,
376
+ oldAuthenticationEvidence: "rejected",
377
+ },
378
+ mutations: [],
379
+ postcondition: ({ subjectId, securityRevision }) =>
380
+ sql`exists(select 1 from ${subject} where ${sc[s.id]} = ${subjectId} and ${sc[s.securityRevision]} = ${securityRevision})`,
381
+ },
382
+ };
383
+ };
384
+
385
+ return { read, base, management, table };
386
+ };
387
+
388
+ const make: Backend<object, never>["passkeys"] = Effect.fn("AuthPersistence.passkeys")(
389
+ function* ({ database, storage, namespace, dialect, features, passwordModules }) {
390
+ const client = yield* SqlClient.SqlClient;
391
+
392
+ const policies = yield* Effect.forEach(features, (feature) =>
393
+ Effect.map(feature.policy, (policy) => ({ feature, policy })),
394
+ );
395
+
396
+ const m = yield* Effect.try({
397
+ try: () =>
398
+ mappings(
399
+ storage,
400
+ namespace,
401
+ dialect,
402
+ passwordModules,
403
+ policies.flatMap(({ feature, policy }) => (feature.management ? [] : policy.profiles)),
404
+ ),
405
+ catch: () => configurationError("Invalid passkey table mappings"),
406
+ });
407
+
408
+ const configuration: PasskeyTargetConfiguration = {
409
+ mode: "interactive",
410
+ dialect,
411
+ locking: dialect === "pg",
412
+ standaloneGuard: () => requireStandalone(unavailable, client),
413
+ };
414
+
415
+ // The backend has already checked the native query builder; schemas check stored values.
416
+ const native = database as ProofSqlDatabase;
417
+
418
+ yield* client.withTransaction(
419
+ Effect.gen(function* () {
420
+ if (dialect === "pg")
421
+ yield* client`select pg_advisory_xact_lock(hashtext(${`${namespace}/passkeys`}))`;
422
+ for (const { feature, policy } of policies) {
423
+ const encoded = yield* Schema.encodeEffect(policyJson)(policy);
424
+
425
+ const policyRevision = digest(
426
+ yield* Schema.encodeEffect(seedJson)({
427
+ policy,
428
+ ...(feature.managementPolicy === undefined
429
+ ? {}
430
+ : { management: feature.managementPolicy }),
431
+ }),
432
+ );
433
+
434
+ const module = m.table("passkeyModules");
435
+ const columns = getTableColumns(module);
436
+
437
+ yield* native
438
+ .insert(module)
439
+ .values({ moduleId: feature.moduleId, active: true, policy: encoded, policyRevision })
440
+ .onConflictDoNothing();
441
+
442
+ const rows = yield* native
443
+ .select()
444
+ .from(module)
445
+ .where(eq(columns.moduleId, feature.moduleId));
446
+
447
+ const row = yield* Schema.decodeUnknownEffect(ModuleRow)(rows[0]);
448
+
449
+ if (!row.active)
450
+ return yield* configurationError(`Passkey module ${feature.moduleId} is disabled`);
451
+ if (row.policyRevision !== policyRevision) {
452
+ const previous = yield* Schema.decodeEffect(policyJson)(row.policy);
453
+
454
+ if (policy.generation <= previous.generation)
455
+ return yield* configurationError(
456
+ `Increase the passkey policy generation to change ${feature.moduleId}`,
457
+ );
458
+ yield* native
459
+ .update(module)
460
+ .set({ policy: encoded, policyRevision })
461
+ .where(
462
+ and(
463
+ eq(columns.moduleId, feature.moduleId),
464
+ eq(columns.policyRevision, row.policyRevision),
465
+ eq(columns.active, true),
466
+ ),
467
+ );
468
+
469
+ const updated = yield* native
470
+ .select()
471
+ .from(module)
472
+ .where(eq(columns.moduleId, feature.moduleId));
473
+
474
+ const applied = yield* Schema.decodeUnknownEffect(ModuleRow)(updated[0]);
475
+
476
+ if (!applied.active || applied.policyRevision !== policyRevision)
477
+ return yield* configurationError(
478
+ `Passkey policy changed while configuring ${feature.moduleId}`,
479
+ );
480
+ }
481
+ yield* native
482
+ .insert(m.table("passkeyAdmissions"))
483
+ .values({
484
+ authorityScope: namespace,
485
+ moduleId: feature.moduleId,
486
+ version: policyRevision,
487
+ ownerMarker: policyRevision,
488
+ })
489
+ .onConflictDoNothing();
490
+ }
491
+ }),
492
+ );
493
+
494
+ const credential = yield* makeTargetPasskeyCredentials<ReadMapping, never>(
495
+ database,
496
+ m.read,
497
+ configuration,
498
+ );
499
+
500
+ const persistence = new Map<string, PasskeyPersistence["Service"]>();
501
+ const managers = new Map<string, PasskeyManagementPersistence["Service"]>();
502
+ const enrollment = new Map<string, PasskeyEnrollmentContext["Service"]>();
503
+
504
+ for (const feature of features) {
505
+ if (feature.management) {
506
+ const mapping = yield* Effect.try({
507
+ try: () => m.management(feature),
508
+ catch: () =>
509
+ configurationError(`Invalid passkey management mapping for ${feature.moduleId}`),
510
+ });
511
+
512
+ const services = yield* makeTargetPasskeyManagement<ManagementMapping, never>(
513
+ database,
514
+ mapping,
515
+ configuration,
516
+ );
517
+
518
+ persistence.set(feature.moduleId, services.passkeyPersistence);
519
+ managers.set(feature.moduleId, services.passkeyManagementPersistence);
520
+ enrollment.set(
521
+ feature.moduleId,
522
+ (yield* makeTargetPasskeyEnrollmentContext<ManagementMapping, never>(
523
+ database,
524
+ mapping,
525
+ configuration,
526
+ )).passkeyEnrollmentContext,
527
+ );
528
+ } else {
529
+ persistence.set(
530
+ feature.moduleId,
531
+ (yield* makeTargetPasskeyPersistence<BaseMapping, never>(
532
+ database,
533
+ m.base(feature),
534
+ configuration,
535
+ )).passkeyPersistence,
536
+ );
537
+ }
538
+ }
539
+
540
+ const service = <A>(services: ReadonlyMap<string, A>, moduleId: string) => {
541
+ const value = services.get(moduleId);
542
+
543
+ return value === undefined ? Effect.fail(unavailable()) : Effect.succeed(value);
544
+ };
545
+
546
+ let context: Context.Context<never> = Context.make(
547
+ PasskeyCredentials,
548
+ credential.passkeyCredentials,
549
+ ).pipe(
550
+ Context.add(PasskeyPersistence, {
551
+ issue: (input, prepare) =>
552
+ Effect.flatMap(service(persistence, input.ceremony.moduleId), (s) =>
553
+ s.issue(input, prepare),
554
+ ),
555
+ context: (input) =>
556
+ Effect.flatMap(service(persistence, input.moduleId), (s) => s.context(input)),
557
+ claim: (input, prepare) =>
558
+ Effect.flatMap(service(persistence, input.ceremony.moduleId), (s) =>
559
+ s.claim(input, prepare),
560
+ ),
561
+ settle: (input, prepare) =>
562
+ Effect.flatMap(service(persistence, input.claim.ceremony.moduleId), (s) =>
563
+ s.settle(input, prepare),
564
+ ),
565
+ cleanup: (input, prepare) =>
566
+ Effect.flatMap(service(persistence, input.moduleId), (s) => s.cleanup(input, prepare)),
567
+ }),
568
+ );
569
+
570
+ if (managers.size > 0)
571
+ context = context.pipe(
572
+ Context.add(PasskeyEnrollmentContext, {
573
+ capture: (input) =>
574
+ Effect.flatMap(service(enrollment, input.moduleId), (s) => s.capture(input)),
575
+ }),
576
+ Context.add(PasskeyManagementPersistence, {
577
+ issueEnrollment: (input, prepare) =>
578
+ Effect.flatMap(service(managers, input.ceremony.moduleId), (s) =>
579
+ s.issueEnrollment(input, prepare),
580
+ ),
581
+ completeEnrollment: (input, prepare) =>
582
+ Effect.flatMap(service(managers, input.claim.ceremony.moduleId), (s) =>
583
+ s.completeEnrollment(input, prepare),
584
+ ),
585
+ list: (input) =>
586
+ Effect.flatMap(service(managers, input.moduleId), (s) => s.list(input)),
587
+ inspectRemove: (input) =>
588
+ Effect.flatMap(service(managers, input.moduleId), (s) => s.inspectRemove(input)),
589
+ rename: (input, prepare) =>
590
+ Effect.flatMap(service(managers, input.moduleId), (s) => s.rename(input, prepare)),
591
+ remove: (input, prepare) =>
592
+ Effect.flatMap(service(managers, input.moduleId), (s) => s.remove(input, prepare)),
593
+ }),
594
+ );
595
+
596
+ return context;
597
+ },
598
+ Effect.mapError((error) =>
599
+ Schema.is(PersistenceConfigurationError)(error)
600
+ ? error
601
+ : configurationError("Unable to configure passkey persistence"),
602
+ ),
603
+ );
604
+
605
+ return make;
606
+ };