@yielded/auth-persistence 0.1.0-beta.17 → 0.1.0-beta.19

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 (85) hide show
  1. package/README.md +5 -2
  2. package/dist/Adapter.d.mts +2 -2
  3. package/dist/Adapter.mjs +2 -2
  4. package/dist/internal/configuration.d.mts +5 -4
  5. package/dist/internal/configuration.mjs.map +1 -1
  6. package/dist/internal/crypto.mjs +12 -0
  7. package/dist/internal/crypto.mjs.map +1 -0
  8. package/dist/internal/email-kernel.d.mts +7 -7
  9. package/dist/internal/passkey/admission.mjs +19 -15
  10. package/dist/internal/passkey/admission.mjs.map +1 -1
  11. package/dist/internal/passkey/credentials.mjs +4 -4
  12. package/dist/internal/passkey/credentials.mjs.map +1 -1
  13. package/dist/internal/passkey/enrollment.mjs +6 -6
  14. package/dist/internal/passkey/enrollment.mjs.map +1 -1
  15. package/dist/internal/passkey/flow.mjs +7 -7
  16. package/dist/internal/passkey/flow.mjs.map +1 -1
  17. package/dist/internal/passkey/management.mjs +2 -2
  18. package/dist/internal/passkey/management.mjs.map +1 -1
  19. package/dist/internal/passkey/registration-ceremony.mjs +2 -2
  20. package/dist/internal/passkey/registration-ceremony.mjs.map +1 -1
  21. package/dist/internal/passkey/registration-custody.mjs +1 -1
  22. package/dist/internal/passkey/registration-custody.mjs.map +1 -1
  23. package/dist/internal/passkey/registration-write.mjs +12 -12
  24. package/dist/internal/passkey/registration-write.mjs.map +1 -1
  25. package/dist/internal/passkey/state.d.mts +1 -1
  26. package/dist/internal/passkey/state.mjs +8 -9
  27. package/dist/internal/passkey/state.mjs.map +1 -1
  28. package/dist/internal/passkey/target.d.mts +1 -1
  29. package/dist/internal/passkey/target.mjs.map +1 -1
  30. package/dist/internal/passkey/write-state.mjs +13 -13
  31. package/dist/internal/passkey/write-state.mjs.map +1 -1
  32. package/dist/internal/passkey/write-target.mjs.map +1 -1
  33. package/dist/internal/passkey-kernel.d.mts +705 -249
  34. package/dist/internal/passkeys.mjs +32 -14
  35. package/dist/internal/passkeys.mjs.map +1 -1
  36. package/dist/internal/password-kernel.d.mts +6 -6
  37. package/dist/internal/password-kernel.mjs +1 -1
  38. package/dist/internal/password-kernel.mjs.map +1 -1
  39. package/dist/internal/persistence.d.mts +2 -2
  40. package/dist/internal/persistence.mjs +18 -9
  41. package/dist/internal/persistence.mjs.map +1 -1
  42. package/dist/internal/phone-kernel.d.mts +7 -7
  43. package/dist/internal/phone-kernel.mjs +14 -14
  44. package/dist/internal/phone-kernel.mjs.map +1 -1
  45. package/dist/internal/registration.mjs +8 -7
  46. package/dist/internal/registration.mjs.map +1 -1
  47. package/dist/internal/session-kernel.mjs +2 -1
  48. package/dist/internal/session-kernel.mjs.map +1 -1
  49. package/dist/internal/sql-query.mjs +1 -1
  50. package/dist/internal/sql-query.mjs.map +1 -1
  51. package/dist/internal/storage-mapping.d.mts +5 -4
  52. package/dist/internal/storage-mapping.mjs +16 -13
  53. package/dist/internal/storage-mapping.mjs.map +1 -1
  54. package/dist/internal/storage-validation.d.mts +13 -3
  55. package/dist/internal/storage-validation.mjs +128 -68
  56. package/dist/internal/storage-validation.mjs.map +1 -1
  57. package/dist/internal/transaction-execution-kernel.d.mts +4 -4
  58. package/dist/internal/transaction-execution-kernel.mjs +7 -5
  59. package/dist/internal/transaction-execution-kernel.mjs.map +1 -1
  60. package/package.json +1 -1
  61. package/src/Adapter.ts +7 -1
  62. package/src/internal/configuration.ts +5 -3
  63. package/src/internal/crypto.ts +15 -0
  64. package/src/internal/passkey/admission.ts +22 -15
  65. package/src/internal/passkey/credentials.ts +4 -4
  66. package/src/internal/passkey/enrollment.ts +7 -7
  67. package/src/internal/passkey/flow.ts +8 -8
  68. package/src/internal/passkey/management.ts +7 -2
  69. package/src/internal/passkey/registration-ceremony.ts +2 -2
  70. package/src/internal/passkey/registration-custody.ts +1 -1
  71. package/src/internal/passkey/registration-write.ts +12 -12
  72. package/src/internal/passkey/state.ts +10 -13
  73. package/src/internal/passkey/target.ts +16 -8
  74. package/src/internal/passkey/write-state.ts +10 -14
  75. package/src/internal/passkey/write-target.ts +11 -3
  76. package/src/internal/passkeys.ts +121 -70
  77. package/src/internal/password-kernel.ts +2 -2
  78. package/src/internal/persistence.ts +30 -13
  79. package/src/internal/phone-kernel.ts +26 -17
  80. package/src/internal/registration.ts +10 -8
  81. package/src/internal/session-kernel.ts +3 -2
  82. package/src/internal/sql-query.ts +3 -2
  83. package/src/internal/storage-mapping.ts +21 -13
  84. package/src/internal/storage-validation.ts +219 -139
  85. package/src/internal/transaction-execution-kernel.ts +30 -11
@@ -31,18 +31,19 @@ export const makePasskeyAdmissionKernel = (
31
31
 
32
32
  const chargeRetentionMillis = 86_400_000;
33
33
 
34
- const chargeScopes = (ceremony: PasskeyCeremony, subjectId?: string) => {
34
+ const chargeScopes = Effect.fnUntraced(function* (ceremony: PasskeyCeremony, subjectId?: string) {
35
35
  const scopes: { kind: PasskeyChargeKind; scope: string }[] = [
36
- { kind: "global", scope: key("global", [ceremony.moduleId]) },
36
+ { kind: "global", scope: yield* key("global", [ceremony.moduleId]) },
37
37
  ];
38
38
 
39
- if (subjectId !== undefined) scopes.push({ kind: "subject", scope: subjectScope(subjectId) });
40
- const target = targetScope(ceremony);
39
+ if (subjectId !== undefined)
40
+ scopes.push({ kind: "subject", scope: yield* subjectScope(subjectId) });
41
+ const target = yield* targetScope(ceremony);
41
42
 
42
43
  if (target !== null) scopes.push({ kind: "target", scope: target });
43
44
 
44
45
  return scopes;
45
- };
46
+ });
46
47
 
47
48
  const lockAdmission = Effect.fn("passkey.lockAdmission")(function* (mapping: any) {
48
49
  const owner = yield* CurrentPasskeyTransaction;
@@ -107,7 +108,7 @@ export const makePasskeyAdmissionKernel = (
107
108
  )})`;
108
109
  };
109
110
 
110
- const admissionCondition = (
111
+ const admissionCondition = Effect.fnUntraced(function* (
111
112
  mapping: any,
112
113
  policies: ReadonlyArray<PasskeyMethodPolicy>,
113
114
  ceremony: PasskeyCeremony,
@@ -115,19 +116,23 @@ export const makePasskeyAdmissionKernel = (
115
116
  added: ReadonlyArray<PasskeyChargeKind>,
116
117
  newFlow: boolean,
117
118
  resolvedSubject: boolean,
118
- ) =>
119
- both(
119
+ ) {
120
+ const subject = subjectId === undefined ? undefined : yield* subjectScope(subjectId);
121
+ const scopes = yield* chargeScopes(ceremony, subjectId);
122
+
123
+ return both(
120
124
  ...policies.flatMap((policy) => [
121
125
  sql`${pendingCount(mapping)} + ${newFlow ? 1 : 0} <= ${policy.maximumPending}`,
122
126
  subjectId === undefined
123
127
  ? undefined
124
- : sql`${pendingCount(mapping, subjectScope(subjectId))} + ${newFlow || resolvedSubject ? 1 : 0} <= ${policy.maximumPendingPerSubject}`,
125
- ...chargeScopes(ceremony, subjectId).map(
128
+ : sql`${pendingCount(mapping, subject)} + ${newFlow || resolvedSubject ? 1 : 0} <= ${policy.maximumPendingPerSubject}`,
129
+ ...scopes.map(
126
130
  ({ kind, scope }) =>
127
131
  sql`${chargedCount(mapping, kind, scope, policy.admission[kind].windowMillis)} + ${added.includes(kind) ? 1 : 0} <= ${policy.admission[kind].limit}`,
128
132
  ),
129
133
  ]),
130
134
  );
135
+ });
131
136
 
132
137
  const guardAdmission = (
133
138
  mapping: any,
@@ -135,9 +140,11 @@ export const makePasskeyAdmissionKernel = (
135
140
  ceremony: PasskeyCeremony,
136
141
  subjectId?: string,
137
142
  ) =>
138
- Effect.map(CurrentPasskeyTransaction, (owner) => {
143
+ Effect.gen(function* () {
144
+ const owner = yield* CurrentPasskeyTransaction;
145
+
139
146
  owner.postconditions.push(
140
- admissionCondition(mapping, policies, ceremony, subjectId, [], false, false),
147
+ yield* admissionCondition(mapping, policies, ceremony, subjectId, [], false, false),
141
148
  );
142
149
  });
143
150
 
@@ -161,7 +168,7 @@ export const makePasskeyAdmissionKernel = (
161
168
  columns: mappedColumns(table),
162
169
  });
163
170
 
164
- const scopes = chargeScopes(ceremony, subjectId);
171
+ const scopes = yield* chargeScopes(ceremony, subjectId);
165
172
 
166
173
  invariant(found.rows.length === scopes.length);
167
174
  for (const { kind, scope } of scopes) {
@@ -202,7 +209,7 @@ export const makePasskeyAdmissionKernel = (
202
209
  Effect.flatMap(CurrentPasskeyTransaction, (owner) =>
203
210
  Effect.gen(function* () {
204
211
  const table = mapping.charge;
205
- const scopes = chargeScopes(ceremony, subjectId);
212
+ const scopes = yield* chargeScopes(ceremony, subjectId);
206
213
 
207
214
  const where = equal(table.table, {
208
215
  [table.moduleId]: ceremony.moduleId,
@@ -241,7 +248,7 @@ export const makePasskeyAdmissionKernel = (
241
248
  const earliest = yield* owner.now(clock);
242
249
  const identities: Record<string, unknown>[] = [];
243
250
 
244
- for (const { kind, scope } of chargeScopes(ceremony, subjectId).filter((item) =>
251
+ for (const { kind, scope } of (yield* chargeScopes(ceremony, subjectId)).filter((item) =>
245
252
  kinds.includes(item.kind),
246
253
  )) {
247
254
  const identity = {
@@ -172,7 +172,7 @@ export const makePasskeyCredentialsKernel = (
172
172
  const found = yield* owner.read(
173
173
  descriptor.table,
174
174
  equal(descriptor.table, {
175
- [descriptor.credentialKey]: credentialKey(rpId, protocolCredentialId),
175
+ [descriptor.credentialKey]: yield* credentialKey(rpId, protocolCredentialId),
176
176
  }),
177
177
  { lock: false, observe: false, limit: 1, columns: mappedColumns(descriptor) },
178
178
  );
@@ -257,7 +257,7 @@ export const makePasskeyCredentialsKernel = (
257
257
  ) {
258
258
  const owner = yield* CurrentPasskeyTransaction;
259
259
  const ownership = mapping.credentialOwnership;
260
- const tupleKey = credentialKey(rpId, protocolCredentialId);
260
+ const tupleKey = yield* credentialKey(rpId, protocolCredentialId);
261
261
 
262
262
  const tuple = (yield* owner.read(
263
263
  ownership.table,
@@ -298,7 +298,7 @@ export const makePasskeyCredentialsKernel = (
298
298
  return undefined;
299
299
  let decoded = descriptor.decode(copiedRow(row));
300
300
  const handle = mapping.handleOwnership;
301
- const expectedHandleKey = handleKey(rpId, decoded.userHandle);
301
+ const expectedHandleKey = yield* handleKey(rpId, decoded.userHandle);
302
302
 
303
303
  const handleRow = (yield* owner.read(
304
304
  handle.table,
@@ -452,7 +452,7 @@ export const makePasskeyCredentialsKernel = (
452
452
  );
453
453
  // oxlint-disable-next-line no-restricted-properties -- native SQL column values enter through this untyped driver boundary.
454
454
  userHandle = Schema.decodeUnknownSync(PasskeyUserHandle)(handleRow[handle.userHandle]);
455
- invariant(handleRow[handle.handleKey] === handleKey(input.rpId, userHandle));
455
+ invariant(handleRow[handle.handleKey] === (yield* handleKey(input.rpId, userHandle)));
456
456
  }
457
457
  const credential = mapping.read.credential;
458
458
 
@@ -97,7 +97,7 @@ export const makePasskeyEnrollmentKernel = (
97
97
 
98
98
  const found = yield* owner.read(table.table, where, { limit: 1 });
99
99
  const row = found.rows[0];
100
- const hashed = handleKey(ceremony.profile.rpId, ceremony.context.userHandle);
100
+ const hashed = yield* handleKey(ceremony.profile.rpId, ceremony.context.userHandle);
101
101
 
102
102
  if (row !== undefined)
103
103
  return (
@@ -193,7 +193,7 @@ export const makePasskeyEnrollmentKernel = (
193
193
  action: "enroll-begin",
194
194
  commandId: ceremony.commandId,
195
195
  flowId: ceremony.flowId,
196
- bindingDigest: enrollmentDigest(ceremony),
196
+ bindingDigest: yield* enrollmentDigest(ceremony),
197
197
  revision: ceremony.context.revision,
198
198
  },
199
199
  {
@@ -207,11 +207,11 @@ export const makePasskeyEnrollmentKernel = (
207
207
  )
208
208
  return { _tag: "Rejected" } as const;
209
209
  invariant(
210
- digest(Ceremony.fields.context, ceremony.context) ===
211
- digest(Ceremony.fields.context, {
210
+ (yield* digest(Ceremony.fields.context, ceremony.context)) ===
211
+ (yield* digest(Ceremony.fields.context, {
212
212
  ...ceremony.context,
213
213
  authorization: input.authorization,
214
- }),
214
+ })),
215
215
  );
216
216
  const issued = yield* issueAssertion(mapping, ceremony, input.policy);
217
217
 
@@ -277,7 +277,7 @@ export const makePasskeyEnrollmentKernel = (
277
277
  action: "enroll-complete",
278
278
  commandId: ceremony.commandId,
279
279
  flowId: ceremony.flowId,
280
- bindingDigest: digest(Ceremony, ceremony),
280
+ bindingDigest: yield* digest(Ceremony, ceremony),
281
281
  revision: ceremony.context.revision,
282
282
  },
283
283
  {
@@ -293,7 +293,7 @@ export const makePasskeyEnrollmentKernel = (
293
293
  return yield* reject;
294
294
  const tuple = mapping.read.credentialOwnership;
295
295
  // Credential ownership is RP-global, including retained registration custody.
296
- const key = credentialKeyFor(ceremony.profile.rpId, input.verified.protocolCredentialId);
296
+ const key = yield* credentialKeyFor(ceremony.profile.rpId, input.verified.protocolCredentialId);
297
297
 
298
298
  if (
299
299
  (yield* owner.read(tuple.table, equal(tuple.table, { [tuple.credentialKey]: key }), {
@@ -233,8 +233,8 @@ export const makePasskeyFlowKernel = (
233
233
  const subject = credential?.revision.subjectId ?? knownSubject(ceremony);
234
234
 
235
235
  invariant(
236
- row[table.subjectScope] === (subject === undefined ? null : subjectScope(subject)) &&
237
- row[table.targetScope] === targetScope(ceremony),
236
+ row[table.subjectScope] === (subject === undefined ? null : yield* subjectScope(subject)) &&
237
+ row[table.targetScope] === (yield* targetScope(ceremony)),
238
238
  );
239
239
  if (state === "Pending") {
240
240
  invariant(
@@ -378,12 +378,12 @@ export const makePasskeyFlowKernel = (
378
378
 
379
379
  if (sameCommand.rows.length !== 0 || sameFlow !== undefined)
380
380
  return { _tag: "Rejected" } as const;
381
- const kinds = chargeScopes(ceremony, subjectId).map((item) => item.kind);
381
+ const kinds = (yield* chargeScopes(ceremony, subjectId)).map((item) => item.kind);
382
382
 
383
383
  if (
384
384
  !(yield* owner.check(liveCondition(mapping, ceremony))) ||
385
385
  !(yield* owner.check(
386
- admissionCondition(mapping, [current], ceremony, subjectId, kinds, true, false),
386
+ yield* admissionCondition(mapping, [current], ceremony, subjectId, kinds, true, false),
387
387
  ))
388
388
  )
389
389
  return { _tag: "Rejected" } as const;
@@ -410,8 +410,8 @@ export const makePasskeyFlowKernel = (
410
410
  [table.claimedAt]: null,
411
411
  [table.claimExpiresAt]: null,
412
412
  [table.credentialSnapshot]: null,
413
- [table.subjectScope]: subjectId === undefined ? null : subjectScope(subjectId),
414
- [table.targetScope]: targetScope(ceremony),
413
+ [table.subjectScope]: subjectId === undefined ? null : yield* subjectScope(subjectId),
414
+ [table.targetScope]: yield* targetScope(ceremony),
415
415
  };
416
416
 
417
417
  // Empty-key observations now expect this exact inserted row as well.
@@ -554,7 +554,7 @@ export const makePasskeyFlowKernel = (
554
554
 
555
555
  if (
556
556
  !(yield* owner.check(
557
- admissionCondition(
557
+ yield* admissionCondition(
558
558
  mapping,
559
559
  [read.policy, current],
560
560
  read.ceremony,
@@ -591,7 +591,7 @@ export const makePasskeyFlowKernel = (
591
591
  [table.claimExpiresAt]: mapping.clock.encodeInstant(claim.claimExpiresAtMillis),
592
592
  [table.credentialSnapshot]:
593
593
  input.credential === undefined ? null : credentialStorage.encode(input.credential),
594
- [table.subjectScope]: subjectId === undefined ? null : subjectScope(subjectId),
594
+ [table.subjectScope]: subjectId === undefined ? null : yield* subjectScope(subjectId),
595
595
  },
596
596
  );
597
597
  if (resolved) yield* insertCharges(mapping, read.ceremony, read.policy, ["subject"], subjectId);
@@ -335,7 +335,7 @@ export const makePasskeyManagementKernel = (
335
335
  action: "remove",
336
336
  commandId: input.commandId,
337
337
  flowId: input.commandId,
338
- bindingDigest: removeDigest(mapping.moduleId, input.commandId, input.credential),
338
+ bindingDigest: yield* removeDigest(mapping.moduleId, input.commandId, input.credential),
339
339
  revision: input.credential.revision,
340
340
  },
341
341
  {
@@ -384,7 +384,12 @@ export const makePasskeyManagementKernel = (
384
384
 
385
385
  yield* owner.update(
386
386
  tuple.table,
387
- { [tuple.credentialKey]: credentialKey(credential.rpId, credential.protocolCredentialId) },
387
+ {
388
+ [tuple.credentialKey]: yield* credentialKey(
389
+ credential.rpId,
390
+ credential.protocolCredentialId,
391
+ ),
392
+ },
388
393
  {
389
394
  [tuple.state]: mapping.write.credentialOwnership.removedState,
390
395
  [tuple.version]: owner.marker,
@@ -78,7 +78,7 @@ export const makePasskeyRegistrationCeremonyKernel = (
78
78
  })).rows[0];
79
79
 
80
80
  if (advisory === undefined) return false;
81
- const hashedHandle = handleKey(ceremony.profile.rpId, context.userHandle);
81
+ const hashedHandle = yield* handleKey(ceremony.profile.rpId, context.userHandle);
82
82
 
83
83
  const held = (yield* owner.read(
84
84
  handle.table,
@@ -204,7 +204,7 @@ export const makePasskeyRegistrationCeremonyKernel = (
204
204
  policyStorage.encode(current) !== policyStorage.encode(input.policy) ||
205
205
  !(yield* owner.check(liveCondition(mapping, read.ceremony))) ||
206
206
  !(yield* owner.check(
207
- admissionCondition(
207
+ yield* admissionCondition(
208
208
  mapping,
209
209
  [read.policy, current],
210
210
  read.ceremony,
@@ -26,7 +26,7 @@ export const makePasskeyRegistrationCustodyKernel = (
26
26
  const owner = yield* CurrentPasskeyTransaction;
27
27
  const handle = mapping.handle;
28
28
  const intent = mapping.intent;
29
- const hashed = handleKey(ceremony.profile.rpId, ceremony.context.userHandle);
29
+ const hashed = yield* handleKey(ceremony.profile.rpId, ceremony.context.userHandle);
30
30
 
31
31
  const held = (yield* owner.read(
32
32
  handle.table,
@@ -67,7 +67,7 @@ export const makePasskeyRegistrationWriteKernel = (
67
67
  const { digest, insertCredential, jsonStorage, validRegistration } = writeState;
68
68
  const { both } = transactions;
69
69
 
70
- const registrationData = (mapping: any, registration: any) => {
70
+ const registrationData = Effect.fnUntraced(function* (mapping: any, registration: any) {
71
71
  const codec = jsonStorage(mapping.registration.schema);
72
72
  const encoded = codec.encode(registration);
73
73
 
@@ -76,26 +76,26 @@ export const makePasskeyRegistrationWriteKernel = (
76
76
  return {
77
77
  encoded,
78
78
  registration: codec.decode(encoded),
79
- fingerprint: digest(mapping.registration.schema, registration),
79
+ fingerprint: yield* digest(mapping.registration.schema, registration),
80
80
  };
81
- };
81
+ });
82
82
 
83
- const describe = (mapping: any, registration: any) => {
84
- const data = registrationData(mapping, registration);
83
+ const describe = Effect.fnUntraced(function* (mapping: any, registration: any) {
84
+ const data = yield* registrationData(mapping, registration);
85
85
 
86
86
  const labels = Schema.decodeSync(
87
87
  Schema.Struct({ name: PasskeyLabel, displayName: PasskeyLabel }),
88
88
  )(mapping.registration.describe(data.registration));
89
89
 
90
90
  return { ...data, ...labels };
91
- };
91
+ });
92
92
 
93
93
  const inspectRegistration = Effect.fn("passkey.inspectRegistration")(function* (
94
94
  mapping: any,
95
95
  registration: any,
96
96
  ) {
97
97
  const owner = yield* CurrentPasskeyTransaction;
98
- const value = describe(mapping, registration);
98
+ const value = yield* describe(mapping, registration);
99
99
  const current = yield* readModule(mapping);
100
100
 
101
101
  yield* readPolicyGuards(mapping);
@@ -133,7 +133,7 @@ export const makePasskeyRegistrationWriteKernel = (
133
133
  ceremony.allowedCredentials.length !== 0
134
134
  )
135
135
  return { _tag: "Rejected" } as const;
136
- const value = describe(mapping, input.registration);
136
+ const value = yield* describe(mapping, input.registration);
137
137
 
138
138
  if (
139
139
  value.fingerprint !== ceremony.context.fingerprint ||
@@ -148,7 +148,7 @@ export const makePasskeyRegistrationWriteKernel = (
148
148
 
149
149
  if (!(yield* owner.check(eligible))) return { _tag: "Rejected" } as const;
150
150
  const handle = mapping.handle;
151
- const hashed = handleKey(ceremony.profile.rpId, ceremony.context.userHandle);
151
+ const hashed = yield* handleKey(ceremony.profile.rpId, ceremony.context.userHandle);
152
152
 
153
153
  const absentHandle = yield* owner.read(
154
154
  handle.table,
@@ -259,7 +259,7 @@ export const makePasskeyRegistrationWriteKernel = (
259
259
  const absentCredential = yield* owner.read(
260
260
  tuple.table,
261
261
  equal(tuple.table, {
262
- [tuple.credentialKey]: credentialKey(
262
+ [tuple.credentialKey]: yield* credentialKey(
263
263
  ceremony.profile.rpId,
264
264
  input.verified.protocolCredentialId,
265
265
  ),
@@ -268,7 +268,7 @@ export const makePasskeyRegistrationWriteKernel = (
268
268
  );
269
269
 
270
270
  const handle = mapping.handle;
271
- const hashed = handleKey(ceremony.profile.rpId, ceremony.context.userHandle);
271
+ const hashed = yield* handleKey(ceremony.profile.rpId, ceremony.context.userHandle);
272
272
 
273
273
  const held = (yield* owner.read(
274
274
  handle.table,
@@ -319,7 +319,7 @@ export const makePasskeyRegistrationWriteKernel = (
319
319
  )
320
320
  return yield* reject;
321
321
 
322
- const data = describe(
322
+ const data = yield* describe(
323
323
  mapping,
324
324
  jsonStorage(mapping.registration.schema).decode(row[intent.applicationSnapshot]),
325
325
  );
@@ -1,9 +1,5 @@
1
1
  /* oxlint-disable no-explicit-any -- existing mapped-row bridge; public adapters preserve table, ID, and Effect types. */
2
2
 
3
- // oxlint-disable-next-line import/extensions -- Noble exposes its explicit .js subpaths.
4
- import { sha256 } from "@noble/hashes/sha2.js";
5
- // oxlint-disable-next-line import/extensions -- Noble exposes its explicit .js subpaths.
6
- import { randomBytes } from "@noble/hashes/utils.js";
7
3
  import {
8
4
  PasskeyConfigurationError,
9
5
  PasskeyUnavailable,
@@ -15,9 +11,9 @@ import {
15
11
  PasskeyMethodPolicy,
16
12
  snapshotPasskeySync,
17
13
  } from "@yielded/auth/Passkey";
18
- import { Context, Effect, Schema } from "effect";
19
- import { Base64Url } from "effect/encoding";
14
+ import { Context, Crypto, Effect, Schema } from "effect";
20
15
 
16
+ import { randomId } from "../crypto";
21
17
  import {
22
18
  requiredPasskeyCredentialConstraints,
23
19
  requiredPasskeyPersistenceConstraints,
@@ -48,14 +44,14 @@ export const makePasskeyStateKernel = (
48
44
 
49
45
  const { col, equal, copiedRow, matchesNativeRow } = makeTransactionRows(unavailable);
50
46
 
51
- const nonce = () => Base64Url.encode(randomBytes(32));
47
+ const nonce = randomId;
52
48
 
53
49
  const encoder = new TextEncoder();
54
50
 
55
51
  const decoder = new TextDecoder("utf-8", { fatal: true });
56
52
 
57
53
  /** RP tuples and admission scopes have no collation-dependent string identity. */
58
- const key = (kind: string, fields: ReadonlyArray<string>) => {
54
+ const key = Effect.fnUntraced(function* (kind: string, fields: ReadonlyArray<string>) {
59
55
  const values = ["effect-auth/passkey/" + kind + "/v1", ...fields].map((field) => {
60
56
  const bytes = encoder.encode(field);
61
57
 
@@ -74,10 +70,11 @@ export const makePasskeyStateKernel = (
74
70
  offset += 4 + bytes.length;
75
71
  }
76
72
 
77
- return (
78
- "v1:" + Array.from(sha256(packed), (value) => value.toString(16).padStart(2, "0")).join("")
79
- );
80
- };
73
+ const crypto = yield* Crypto.Crypto;
74
+ const hashed = yield* crypto.digest("SHA-256", packed);
75
+
76
+ return "v1:" + Array.from(hashed, (value) => value.toString(16).padStart(2, "0")).join("");
77
+ });
81
78
 
82
79
  const credentialKey = (rpId: string, id: string) => key("credential", [rpId, id]);
83
80
 
@@ -88,7 +85,7 @@ export const makePasskeyStateKernel = (
88
85
  const targetScope = (ceremony: PasskeyCeremony) => {
89
86
  const context = ceremony.context;
90
87
 
91
- if (!("target" in context)) return null;
88
+ if (!("target" in context)) return Effect.succeed(null);
92
89
  const target = context.target;
93
90
 
94
91
  return key("target", [
@@ -26,7 +26,7 @@ import {
26
26
  snapshotPasskeySync,
27
27
  } from "@yielded/auth/Passkey";
28
28
  import { SubjectId } from "@yielded/auth/Schema";
29
- import { Effect, Schema } from "effect";
29
+ import { type Crypto, Effect, Schema } from "effect";
30
30
  import type { Statement } from "effect/sql/Statement";
31
31
 
32
32
  import type { PersistenceMappingError } from "../mapping-error";
@@ -130,7 +130,7 @@ export const makePasskeyTargetKernel = (
130
130
  ): Effect.fn.Return<
131
131
  PasskeyExecution,
132
132
  PasskeyConfigurationError,
133
- LifecycleHooks | NativeDatabase
133
+ LifecycleHooks | NativeDatabase | Crypto.Crypto
134
134
  > {
135
135
  const hooks = yield* LifecycleHooks;
136
136
 
@@ -437,7 +437,7 @@ export const makePasskeyTargetKernel = (
437
437
  ): Effect.Effect<
438
438
  PasskeyCredentialServices,
439
439
  PasskeyConfigurationError | PersistenceMappingError,
440
- RSetup | NativeDatabase
440
+ RSetup | NativeDatabase | Crypto.Crypto
441
441
  > =>
442
442
  Effect.gen(function* () {
443
443
  const mapping = yield* capturedMapping(source, "read", configuration);
@@ -455,7 +455,7 @@ export const makePasskeyTargetKernel = (
455
455
  ): Effect.Effect<
456
456
  PasskeyEnrollmentContextServices,
457
457
  PasskeyConfigurationError | PersistenceMappingError,
458
- RSetup | NativeDatabase
458
+ RSetup | NativeDatabase | Crypto.Crypto
459
459
  > =>
460
460
  Effect.gen(function* () {
461
461
  const mapping = yield* capturedMapping(source, "context", configuration);
@@ -473,7 +473,7 @@ export const makePasskeyTargetKernel = (
473
473
  ): Effect.Effect<
474
474
  PasskeyPersistenceServices,
475
475
  PasskeyConfigurationError | PersistenceMappingError,
476
- RSetup | LifecycleHooks | NativeDatabase
476
+ RSetup | LifecycleHooks | NativeDatabase | Crypto.Crypto
477
477
  > =>
478
478
  Effect.gen(function* () {
479
479
  const mapping = yield* capturedMapping(source, "assertion", configuration);
@@ -490,7 +490,7 @@ export const makePasskeyTargetKernel = (
490
490
  ): Effect.Effect<
491
491
  PasskeyRegistrationCeremonyServices,
492
492
  PasskeyConfigurationError | PersistenceMappingError,
493
- RSetup | LifecycleHooks | NativeDatabase
493
+ RSetup | LifecycleHooks | NativeDatabase | Crypto.Crypto
494
494
  > =>
495
495
  Effect.gen(function* () {
496
496
  const mapping = yield* capturedMapping(source, "registration", configuration);
@@ -508,7 +508,11 @@ export const makePasskeyTargetKernel = (
508
508
  services: PasskeyPersistenceServices,
509
509
  append: (statement: Statement<unknown>) => void,
510
510
  ) => Effect.Effect<A, E, R>,
511
- ): Effect.Effect<A, PasskeyCoordinatorError<E>, R | RSetup | LifecycleHooks | NativeDatabase> =>
511
+ ): Effect.Effect<
512
+ A,
513
+ PasskeyCoordinatorError<E>,
514
+ R | RSetup | LifecycleHooks | NativeDatabase | Crypto.Crypto
515
+ > =>
512
516
  Effect.flatMap(capturedMapping(source, "assertion", configuration), (mapping) =>
513
517
  Effect.flatMap(LifecycleHooks, (hooks) =>
514
518
  coordinateTransactionOwner(
@@ -545,7 +549,11 @@ export const makePasskeyTargetKernel = (
545
549
  services: PasskeyRegistrationCeremonyServices,
546
550
  append: (statement: Statement<unknown>) => void,
547
551
  ) => Effect.Effect<A, E, R>,
548
- ): Effect.Effect<A, PasskeyCoordinatorError<E>, R | RSetup | LifecycleHooks | NativeDatabase> =>
552
+ ): Effect.Effect<
553
+ A,
554
+ PasskeyCoordinatorError<E>,
555
+ R | RSetup | LifecycleHooks | NativeDatabase | Crypto.Crypto
556
+ > =>
549
557
  Effect.flatMap(capturedMapping(source, "registration", configuration), (mapping) =>
550
558
  Effect.flatMap(LifecycleHooks, (hooks) =>
551
559
  coordinateTransactionOwner(
@@ -1,7 +1,5 @@
1
1
  /* oxlint-disable no-explicit-any -- existing mapped-row bridge; public adapters preserve table, ID, and Effect types. */
2
2
 
3
- // oxlint-disable-next-line import/extensions -- Noble exposes explicit .js subpaths.
4
- import { sha256 } from "@noble/hashes/sha2.js";
5
3
  import {
6
4
  type PasskeyActionAuthorization,
7
5
  type PasskeyCeremony,
@@ -18,11 +16,11 @@ import {
18
16
  PasskeyManagementPolicy,
19
17
  snapshotPasskeySync,
20
18
  } from "@yielded/auth/Passkey";
21
- import { TokenDigest, type SubjectId } from "@yielded/auth/Schema";
19
+ import { type SubjectId } from "@yielded/auth/Schema";
22
20
  import { SessionInvalidationWindow } from "@yielded/auth/Sessions";
23
21
  import { DateTime, Effect, Schema } from "effect";
24
- import { Base64Url } from "effect/encoding";
25
22
 
23
+ import { digest as digestText } from "../crypto";
26
24
  import type { PersistenceMappingError } from "../mapping-error";
27
25
  import type { QueryOperations } from "../query-operations";
28
26
  import type { makeTransactionKernel } from "../transaction-kernel";
@@ -85,10 +83,7 @@ export const makePasskeyWriteStateKernel = (
85
83
  const digest = <S extends Schema.Codec<unknown, unknown, never, never>>(
86
84
  schema: S,
87
85
  value: S["Type"],
88
- ) =>
89
- TokenDigest.make(
90
- Base64Url.encode(sha256(new TextEncoder().encode(jsonStorage(schema).encode(value)))),
91
- );
86
+ ) => digestText(jsonStorage(schema).encode(value));
92
87
 
93
88
  const currentSubject = Effect.fn("passkey.currentWriteSubject")(function* (
94
89
  mapping: any,
@@ -205,7 +200,7 @@ export const makePasskeyWriteStateKernel = (
205
200
  read.subjectIds.equals(table.decodeSubjectId(copiedRow(row)), subject.nativeId),
206
201
  );
207
202
  const ownership = read.credentialOwnership;
208
- const key = credentialKey(decoded.rpId, decoded.protocolCredentialId);
203
+ const key = yield* credentialKey(decoded.rpId, decoded.protocolCredentialId);
209
204
 
210
205
  const tuple = (yield* owner.read(
211
206
  ownership.table,
@@ -226,7 +221,7 @@ export const makePasskeyWriteStateKernel = (
226
221
  row[table.credentialKey] === key,
227
222
  );
228
223
  const handle = read.handleOwnership;
229
- const hashedHandle = handleKey(decoded.rpId, decoded.userHandle);
224
+ const hashedHandle = yield* handleKey(decoded.rpId, decoded.userHandle);
230
225
 
231
226
  const held = (yield* owner.read(
232
227
  handle.table,
@@ -470,9 +465,10 @@ export const makePasskeyWriteStateKernel = (
470
465
  }
471
466
  owner.postconditions.push(mapping.invalidation.postcondition(input));
472
467
  const flow = mapping.flow;
468
+ const scope = yield* subjectScope(subject.subjectId);
473
469
 
474
470
  const where = both(
475
- equal(flow.table, { [flow.subjectScope]: subjectScope(subject.subjectId) }),
471
+ equal(flow.table, { [flow.subjectScope]: scope }),
476
472
  sql`${col(flow.table, flow.state)} in (${flow.states.Pending}, ${flow.states.Claimed})`,
477
473
  );
478
474
 
@@ -485,7 +481,7 @@ export const makePasskeyWriteStateKernel = (
485
481
  for (const observation of owner.observations)
486
482
  if (observation.table === flow.table)
487
483
  observation.rows = observation.rows.map((row) =>
488
- row[flow.subjectScope] === subjectScope(subject.subjectId) &&
484
+ row[flow.subjectScope] === scope &&
489
485
  [flow.states.Pending, flow.states.Claimed].includes(row[flow.state])
490
486
  ? { ...row, [flow.state]: flow.states.Rejected, [flow.version]: owner.marker }
491
487
  : row,
@@ -559,8 +555,8 @@ export const makePasskeyWriteStateKernel = (
559
555
  });
560
556
 
561
557
  const input = { subjectId: subject.nativeId, credential, summary: safe, marker: owner.marker };
562
- const tupleKey = credentialKey(credential.rpId, credential.protocolCredentialId);
563
- const hashedHandle = handleKey(credential.rpId, credential.userHandle);
558
+ const tupleKey = yield* credentialKey(credential.rpId, credential.protocolCredentialId);
559
+ const hashedHandle = yield* handleKey(credential.rpId, credential.userHandle);
564
560
  const tuple = read.credentialOwnership;
565
561
 
566
562
  const absent = yield* owner.read(
@@ -10,7 +10,7 @@ import {
10
10
  import * as M from "@yielded/auth/Passkey";
11
11
  import { SubjectId } from "@yielded/auth/Schema";
12
12
  import { SessionInvalidationWindow } from "@yielded/auth/Sessions";
13
- import { Effect, Layer, Schema } from "effect";
13
+ import { type Crypto, Effect, Layer, Schema } from "effect";
14
14
  import type { Statement } from "effect/sql/Statement";
15
15
 
16
16
  import type { PasskeyMappingSource } from "../models/passkey-model";
@@ -371,7 +371,11 @@ export const makePasskeyWriteTargetKernel = (
371
371
  services: PasskeyManagementServices,
372
372
  append: (statement: Statement<unknown>) => void,
373
373
  ) => Effect.Effect<A, E, R>,
374
- ): Effect.Effect<A, PasskeyCoordinatorError<E>, R | RSetup | LifecycleHooks | NativeDatabase> =>
374
+ ): Effect.Effect<
375
+ A,
376
+ PasskeyCoordinatorError<E>,
377
+ R | RSetup | LifecycleHooks | NativeDatabase | Crypto.Crypto
378
+ > =>
375
379
  Effect.gen(function* () {
376
380
  const mapping = yield* writeMapping(source, configuration, false);
377
381
  const hooks = yield* LifecycleHooks;
@@ -406,7 +410,11 @@ export const makePasskeyWriteTargetKernel = (
406
410
  services: PasskeyRegistrationServices<Value>,
407
411
  append: (statement: Statement<unknown>) => void,
408
412
  ) => Effect.Effect<A, E, R>,
409
- ): Effect.Effect<A, PasskeyCoordinatorError<E>, R | RSetup | LifecycleHooks | NativeDatabase> =>
413
+ ): Effect.Effect<
414
+ A,
415
+ PasskeyCoordinatorError<E>,
416
+ R | RSetup | LifecycleHooks | NativeDatabase | Crypto.Crypto
417
+ > =>
410
418
  Effect.gen(function* () {
411
419
  const mapping = yield* writeMapping(source, configuration, true);
412
420
  const hooks = yield* LifecycleHooks;