@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
@@ -1,4 +1,3 @@
1
- import { digest } from "@yielded/auth-crypto";
2
1
  import {
3
2
  PasskeyCredentials,
4
3
  PasskeyCredential,
@@ -19,6 +18,7 @@ import {
19
18
  type MappingInput,
20
19
  type PasskeyFeature,
21
20
  } from "./configuration";
21
+ import { digest } from "./crypto";
22
22
  import { PersistenceMappingError } from "./mapping-error";
23
23
  import {
24
24
  requiredPasskeyCredentialConstraints,
@@ -54,6 +54,13 @@ const ModuleRow = Schema.Struct({
54
54
  policyRevision: Schema.String,
55
55
  });
56
56
 
57
+ const ConfiguredModuleRow = Schema.Struct({
58
+ moduleId: Schema.String,
59
+ active: Schema.Boolean,
60
+ policyRevision: Schema.String,
61
+ admitted: Schema.Literals(["present", "missing"]),
62
+ });
63
+
57
64
  const credentialData = PasskeyCredential.mapFields(
58
65
  ({ revision: _revision, active: _active, ...fields }) => fields,
59
66
  );
@@ -390,7 +397,21 @@ export const makeComposedPasskeys = (
390
397
  const client = yield* SqlClient.SqlClient;
391
398
 
392
399
  const policies = yield* Effect.forEach(features, (feature) =>
393
- Effect.map(feature.policy, (policy) => ({ feature, policy })),
400
+ Effect.gen(function* () {
401
+ const policy = yield* feature.policy;
402
+ const encoded = yield* Schema.encodeEffect(policyJson)(policy);
403
+
404
+ const policyRevision = yield* digest(
405
+ yield* Schema.encodeEffect(seedJson)({
406
+ policy,
407
+ ...(feature.managementPolicy === undefined
408
+ ? {}
409
+ : { management: feature.managementPolicy }),
410
+ }),
411
+ );
412
+
413
+ return { feature, policy, encoded, policyRevision };
414
+ }),
394
415
  );
395
416
 
396
417
  const m = yield* Effect.try({
@@ -413,82 +434,112 @@ export const makeComposedPasskeys = (
413
434
  };
414
435
 
415
436
  const native = yield* CurrentProofSql;
437
+ const module = m.table("passkeyModules");
438
+ const columns = getTableColumns(module);
439
+ const admission = m.table("passkeyAdmissions");
440
+ const ac = getTableColumns(admission);
441
+
442
+ // Read live configuration on every acquisition. Matching modules and
443
+ // admission ownership need no initialization writes or advisory lock.
444
+ const configured = yield* native
445
+ .select({
446
+ moduleId: columns.moduleId,
447
+ active: columns.active,
448
+ policyRevision: columns.policyRevision,
449
+ // Predicate SQL preserves qualifiers through single-table projections.
450
+ admitted: sql`case when exists(select 1 from ${admission}
451
+ where ${and(eq(ac.authorityScope, namespace), eq(ac.moduleId, columns.moduleId))})
452
+ then 'present' else 'missing' end`,
453
+ })
454
+ .from(module)
455
+ .where(
456
+ inArray(
457
+ columns.moduleId,
458
+ policies.map(({ feature }) => feature.moduleId),
459
+ ),
460
+ )
461
+ .pipe(
462
+ Effect.flatMap(Schema.decodeUnknownEffect(Schema.Array(ConfiguredModuleRow))),
463
+ Effect.map((rows) =>
464
+ policies.every(({ feature, policyRevision }) =>
465
+ rows.some(
466
+ (row) =>
467
+ row.moduleId === feature.moduleId &&
468
+ row.active &&
469
+ row.admitted === "present" &&
470
+ row.policyRevision === policyRevision,
471
+ ),
472
+ ),
473
+ ),
474
+ );
416
475
 
417
- yield* client.withTransaction(
418
- Effect.gen(function* () {
419
- if (dialect === "pg")
420
- yield* client`select pg_advisory_xact_lock(hashtext(${`${namespace}/passkeys`}))`;
421
- for (const { feature, policy } of policies) {
422
- const encoded = yield* Schema.encodeEffect(policyJson)(policy);
423
-
424
- const policyRevision = digest(
425
- yield* Schema.encodeEffect(seedJson)({
426
- policy,
427
- ...(feature.managementPolicy === undefined
428
- ? {}
429
- : { management: feature.managementPolicy }),
430
- }),
431
- );
432
-
433
- const module = m.table("passkeyModules");
434
- const columns = getTableColumns(module);
435
-
436
- yield* native
437
- .insert(module)
438
- .values({ moduleId: feature.moduleId, active: true, policy: encoded, policyRevision })
439
- .onConflictDoNothing();
440
-
441
- const rows = yield* native
442
- .select()
443
- .from(module)
444
- .where(eq(columns.moduleId, feature.moduleId));
445
-
446
- const row = yield* Schema.decodeUnknownEffect(ModuleRow)(rows[0]);
447
-
448
- if (!row.active)
449
- return yield* configurationError(`Passkey module ${feature.moduleId} is disabled`);
450
- if (row.policyRevision !== policyRevision) {
451
- const previous = yield* Schema.decodeEffect(policyJson)(row.policy);
452
-
453
- if (policy.generation <= previous.generation)
454
- return yield* configurationError(
455
- `Increase the passkey policy generation to change ${feature.moduleId}`,
456
- );
476
+ if (!configured)
477
+ yield* client.withTransaction(
478
+ Effect.gen(function* () {
479
+ if (dialect === "pg")
480
+ yield* client`select pg_advisory_xact_lock(hashtext(${`${namespace}/passkeys`}))`;
481
+ for (const { feature, policy, encoded, policyRevision } of policies) {
457
482
  yield* native
458
- .update(module)
459
- .set({ policy: encoded, policyRevision })
460
- .where(
461
- and(
462
- eq(columns.moduleId, feature.moduleId),
463
- eq(columns.policyRevision, row.policyRevision),
464
- eq(columns.active, true),
465
- ),
466
- );
467
-
468
- const updated = yield* native
483
+ .insert(module)
484
+ .values({
485
+ moduleId: feature.moduleId,
486
+ active: true,
487
+ policy: encoded,
488
+ policyRevision,
489
+ })
490
+ .onConflictDoNothing();
491
+
492
+ const rows = yield* native
469
493
  .select()
470
494
  .from(module)
471
495
  .where(eq(columns.moduleId, feature.moduleId));
472
496
 
473
- const applied = yield* Schema.decodeUnknownEffect(ModuleRow)(updated[0]);
497
+ const row = yield* Schema.decodeUnknownEffect(ModuleRow)(rows[0]);
474
498
 
475
- if (!applied.active || applied.policyRevision !== policyRevision)
476
- return yield* configurationError(
477
- `Passkey policy changed while configuring ${feature.moduleId}`,
478
- );
499
+ if (!row.active)
500
+ return yield* configurationError(`Passkey module ${feature.moduleId} is disabled`);
501
+ if (row.policyRevision !== policyRevision) {
502
+ const previous = yield* Schema.decodeEffect(policyJson)(row.policy);
503
+
504
+ if (policy.generation <= previous.generation)
505
+ return yield* configurationError(
506
+ `Increase the passkey policy generation to change ${feature.moduleId}`,
507
+ );
508
+ yield* native
509
+ .update(module)
510
+ .set({ policy: encoded, policyRevision })
511
+ .where(
512
+ and(
513
+ eq(columns.moduleId, feature.moduleId),
514
+ eq(columns.policyRevision, row.policyRevision),
515
+ eq(columns.active, true),
516
+ ),
517
+ );
518
+
519
+ const updated = yield* native
520
+ .select()
521
+ .from(module)
522
+ .where(eq(columns.moduleId, feature.moduleId));
523
+
524
+ const applied = yield* Schema.decodeUnknownEffect(ModuleRow)(updated[0]);
525
+
526
+ if (!applied.active || applied.policyRevision !== policyRevision)
527
+ return yield* configurationError(
528
+ `Passkey policy changed while configuring ${feature.moduleId}`,
529
+ );
530
+ }
531
+ yield* native
532
+ .insert(m.table("passkeyAdmissions"))
533
+ .values({
534
+ authorityScope: namespace,
535
+ moduleId: feature.moduleId,
536
+ version: policyRevision,
537
+ ownerMarker: policyRevision,
538
+ })
539
+ .onConflictDoNothing();
479
540
  }
480
- yield* native
481
- .insert(m.table("passkeyAdmissions"))
482
- .values({
483
- authorityScope: namespace,
484
- moduleId: feature.moduleId,
485
- version: policyRevision,
486
- ownerMarker: policyRevision,
487
- })
488
- .onConflictDoNothing();
489
- }
490
- }),
491
- );
541
+ }),
542
+ );
492
543
 
493
544
  const credential = yield* makeTargetPasskeyCredentials<ReadMapping, never>(
494
545
  m.read,
@@ -1168,9 +1168,9 @@ export const makePasswordKernel = <
1168
1168
  moduleId: input.moduleId,
1169
1169
  action: input.action,
1170
1170
  identifier: input.identifier,
1171
- ...(resolved === undefined
1171
+ ...(resolved?.snapshot === undefined
1172
1172
  ? {}
1173
- : { subjectId: resolved.snapshot?.revision.subjectId }),
1173
+ : { subjectId: resolved.snapshot.revision.subjectId }),
1174
1174
  });
1175
1175
 
1176
1176
  const scopes = scopeEntries(keys, input.policy);
@@ -1,4 +1,3 @@
1
- import { digest, randomId } from "@yielded/auth-crypto";
2
1
  import { EmailAddressPersistence, EmailUnavailable } from "@yielded/auth/Email";
3
2
  import { LifecycleHooks } from "@yielded/auth/Hooks";
4
3
  import type { PasskeyConfig } from "@yielded/auth/Passkey";
@@ -7,7 +6,7 @@ import { hooksLayer } from "@yielded/auth/Persistence";
7
6
  import { PhoneAdmission, PhoneSignInTargets, PhoneOtpUnavailable } from "@yielded/auth/PhoneOtp";
8
7
  import { ProofPersistence, ProofUnavailable } from "@yielded/auth/Proofs";
9
8
  import { AuthenticationAuthority, SessionUnavailable } from "@yielded/auth/Sessions";
10
- import { Context, Effect, Layer, Schema } from "effect";
9
+ import { Context, type Crypto, Effect, Layer, Schema } from "effect";
11
10
  import { SqlClient } from "effect/sql";
12
11
 
13
12
  import {
@@ -27,6 +26,7 @@ import {
27
26
  type PasskeyRequirement,
28
27
  type MappingInput,
29
28
  } from "./configuration";
29
+ import { randomId } from "./crypto";
30
30
  import { makeEmailKernel, CurrentEmailSql, type EmailSqlDatabase } from "./email-kernel";
31
31
  import { PersistenceMappingError } from "./mapping-error";
32
32
  import {
@@ -52,7 +52,11 @@ import {
52
52
  type StorageRole,
53
53
  type StorageTable,
54
54
  } from "./storage-tables";
55
- import { validateStorage } from "./storage-validation";
55
+ import {
56
+ validateStorageBatch,
57
+ withStorageValidation,
58
+ type StorageValidation,
59
+ } from "./storage-validation";
56
60
  import { makeTransactionExecutionKernel } from "./transaction-execution-kernel";
57
61
  import {
58
62
  makeTransactionKernel,
@@ -74,7 +78,12 @@ export interface Backend<T extends object, R> {
74
78
  }) => Effect.Effect<
75
79
  Context.Context<never>,
76
80
  PersistenceConfigurationError,
77
- PasskeyConfig | LifecycleHooks | SqlClient.SqlClient | NativeDatabase | CurrentProofSql
81
+ | PasskeyConfig
82
+ | LifecycleHooks
83
+ | SqlClient.SqlClient
84
+ | NativeDatabase
85
+ | CurrentProofSql
86
+ | Crypto.Crypto
78
87
  >;
79
88
  }
80
89
 
@@ -208,8 +217,10 @@ export const createPersistence = <T extends object, R>(
208
217
  if (idColumn === undefined || idColumn.type === "boolean")
209
218
  throw configError("Subject ID must map a text or integer column");
210
219
 
211
- const prefix =
212
- options.prefix ?? `auth_${digest(auth.namespace).slice(0, 12).replaceAll("-", "_")}`;
220
+ const prefix = options.prefix;
221
+
222
+ if (managed && (prefix === undefined || !/^[A-Za-z_][A-Za-z0-9_]{0,100}$/.test(prefix)))
223
+ throw configError("Managed persistence requires an explicit SQL table prefix");
213
224
 
214
225
  const schema: Partial<Record<StorageRole, T>> = { ...options.tables };
215
226
 
@@ -310,6 +321,8 @@ export const createPersistence = <T extends object, R>(
310
321
  return yield* configError(
311
322
  "Persistence configuration belongs to a different Auth definition",
312
323
  );
324
+ const validations: StorageValidation[] = [];
325
+
313
326
  for (const role of roles) {
314
327
  const table = (storage.schema as Partial<Record<StorageRole, T>>)[role];
315
328
 
@@ -321,12 +334,14 @@ export const createPersistence = <T extends object, R>(
321
334
  if (columns[name] === undefined)
322
335
  return yield* configError(`Missing ${role}.${name} column`);
323
336
  }
324
- yield* validateStorage(dialect, description, storageTables[role].unique);
337
+ validations.push({ table: description, required: storageTables[role].unique });
325
338
  }
326
- yield* validateStorage(dialect, backend.describe(storage.subjects.table as T), [
327
- [storage.subjects.id],
328
- ]);
329
- const mappings = makeMappings(storage);
339
+ validations.push({
340
+ table: backend.describe(storage.subjects.table as T),
341
+ required: [[storage.subjects.id]],
342
+ });
343
+ yield* validateStorageBatch(dialect, validations);
344
+ const mappings = yield* makeMappings(storage);
330
345
 
331
346
  const standalone = <E>(error: () => E) =>
332
347
  requireStandalone(error, client.transactionService);
@@ -403,7 +418,7 @@ export const createPersistence = <T extends object, R>(
403
418
  locking: dialect === "pg",
404
419
  standaloneGuard: standalone(() => PasswordUnavailable.make({})),
405
420
  insertIfAbsent: (query) => query.onConflictDoNothing(),
406
- proof: proofConfiguration,
421
+ ...(proofConfiguration === undefined ? {} : { proof: proofConfiguration }),
407
422
  })
408
423
  .pipe(
409
424
  Effect.provideService(CurrentPasswordSql, native),
@@ -439,7 +454,7 @@ export const createPersistence = <T extends object, R>(
439
454
  mode: "interactive",
440
455
  locking: dialect === "pg",
441
456
  standaloneGuard: standalone(() => EmailUnavailable.make({})),
442
- proof: proofConfiguration,
457
+ ...(proofConfiguration === undefined ? {} : { proof: proofConfiguration }),
443
458
  })
444
459
  .pipe(
445
460
  Effect.provideService(CurrentEmailSql, native),
@@ -564,6 +579,7 @@ export const createPersistence = <T extends object, R>(
564
579
  Context.Context<never>,
565
580
  PersistenceConfigurationError,
566
581
  | PasskeyRequirement<A>
582
+ | Crypto.Crypto
567
583
  | LifecycleHooks
568
584
  | SqlClient.SqlClient
569
585
  | NativeDatabase
@@ -582,6 +598,7 @@ export const createPersistence = <T extends object, R>(
582
598
  // The checked capability metadata above determines exactly these service keys.
583
599
  return context as Context.Context<Ports<C, Id, A>>;
584
600
  }).pipe(
601
+ withStorageValidation,
585
602
  Effect.mapError((error) =>
586
603
  Schema.is(PersistenceConfigurationError)(error)
587
604
  ? error
@@ -1,4 +1,3 @@
1
- import { digest, randomId } from "@yielded/auth-crypto";
2
1
  import {
3
2
  PhoneCustody,
4
3
  PhoneCommandId,
@@ -14,6 +13,7 @@ import type { SubjectId } from "@yielded/auth/Schema";
14
13
  import { AuthenticationRequirement, SecurityRevision } from "@yielded/auth/Sessions";
15
14
  import { Context, Effect, Option, Schema } from "effect";
16
15
 
16
+ import { digest, randomId } from "./crypto";
17
17
  /* oxlint-disable no-explicit-any -- existing storage kernels erase foreign table shapes; domain errors remain typed. */
18
18
  import type { SqlExpression as SQL } from "./query-operations";
19
19
  import type { QueryOperations, SqlFragment, SqlColumn } from "./query-operations";
@@ -100,7 +100,7 @@ export const makePhoneKernel = <
100
100
  moduleId: mapping.moduleId,
101
101
  record: value,
102
102
  }),
103
- [mapping.state.version]: randomId(),
103
+ [mapping.state.version]: yield* randomId,
104
104
  };
105
105
 
106
106
  if (observed.rows.length === 0) {
@@ -128,7 +128,7 @@ export const makePhoneKernel = <
128
128
  mapping: any,
129
129
  phoneNumber: string,
130
130
  ) {
131
- const key = scope(mapping.moduleId, "custody", phoneNumber),
131
+ const key = yield* scope(mapping.moduleId, "custody", phoneNumber),
132
132
  observation = yield* stateRead(mapping, key);
133
133
 
134
134
  const custody =
@@ -327,7 +327,7 @@ export const makePhoneKernel = <
327
327
  now = yield* owner.now(mapping),
328
328
  policy = mapping.admission;
329
329
 
330
- const key = scope(mapping.moduleId, "admission", input.action + "/" + input.requestId),
330
+ const key = yield* scope(mapping.moduleId, "admission", input.action + "/" + input.requestId),
331
331
  receipt = yield* stateRead(mapping, key);
332
332
 
333
333
  if (receipt.rows.length !== 0) {
@@ -338,7 +338,7 @@ export const makePhoneKernel = <
338
338
 
339
339
  return (
340
340
  saved.fingerprint === input.fingerprint &&
341
- saved.network === digest(input.networkKey) &&
341
+ saved.network === (yield* digest(input.networkKey)) &&
342
342
  saved.accepted === true &&
343
343
  saved.expiresAtMillis > now
344
344
  );
@@ -347,11 +347,20 @@ export const makePhoneKernel = <
347
347
  const window = Math.floor(now / policy.windowMillis),
348
348
  entries = [
349
349
  {
350
- key: scope(mapping.moduleId, "network", input.action + "/" + digest(input.networkKey)),
350
+ key: yield* scope(
351
+ mapping.moduleId,
352
+ "network",
353
+ input.action + "/" + (yield* digest(input.networkKey)),
354
+ ),
351
355
  limit: input.action === "request" ? policy.networkRequests : policy.networkAttempts,
352
356
  },
353
357
  ...(input.action === "request"
354
- ? [{ key: scope(mapping.moduleId, "messages", "global"), limit: policy.maximumMessages }]
358
+ ? [
359
+ {
360
+ key: yield* scope(mapping.moduleId, "messages", "global"),
361
+ limit: policy.maximumMessages,
362
+ },
363
+ ]
355
364
  : []),
356
365
  ].sort((a, b) => a.key.localeCompare(b.key));
357
366
 
@@ -383,7 +392,7 @@ export const makePhoneKernel = <
383
392
  key,
384
393
  {
385
394
  fingerprint: input.fingerprint,
386
- network: digest(input.networkKey),
395
+ network: yield* digest(input.networkKey),
387
396
  accepted,
388
397
  expiresAtMillis: now + Math.min(policy.requestRetentionMillis, input.replayLifetimeMillis),
389
398
  },
@@ -446,7 +455,7 @@ export const makePhoneKernel = <
446
455
  )
447
456
  return { decision: rejected };
448
457
 
449
- const commandKey = scope(mapping.moduleId, "command", input.commandId),
458
+ const commandKey = yield* scope(mapping.moduleId, "command", input.commandId),
450
459
  command = yield* stateRead(mapping, commandKey);
451
460
 
452
461
  if (command.rows.length !== 0) return { decision: rejected };
@@ -564,10 +573,10 @@ export const makePhoneKernel = <
564
573
  ? mapping.subjectIds.toSubject(mapping.subjectIds.allocate())
565
574
  : captured.target.revision!.subjectId,
566
575
  nativeId = mapping.subjectIds.toNative(subjectId),
567
- securityRevision = SecurityRevision.make(randomId()),
568
- credentialRevision = SecurityRevision.make(randomId()),
569
- custodyRevision = SecurityRevision.make(randomId()),
570
- credentialId = captured.target.custody?.credentialId ?? randomId();
576
+ securityRevision = SecurityRevision.make(yield* randomId),
577
+ credentialRevision = SecurityRevision.make(yield* randomId),
578
+ custodyRevision = SecurityRevision.make(yield* randomId),
579
+ credentialId = captured.target.custody?.credentialId ?? (yield* randomId);
571
580
 
572
581
  invariant(mapping.subjectIds.toSubject(nativeId) === subjectId);
573
582
 
@@ -647,12 +656,12 @@ export const makePhoneKernel = <
647
656
  yield* owner.update(
648
657
  i.table,
649
658
  { [i.namespace]: "phone", [i.value]: old.phoneNumber },
650
- { [i.status]: i.encodeStatus(false), [i.revision]: randomId() },
659
+ { [i.status]: i.encodeStatus(false), [i.revision]: yield* randomId },
651
660
  );
652
661
  yield* owner.update(
653
662
  c.table,
654
663
  { [c.id]: old.credentialId, [c.subjectId]: nativeId },
655
- { [c.status]: c.encodeStatus(false), [c.revision]: randomId() },
664
+ { [c.status]: c.encodeStatus(false), [c.revision]: yield* randomId },
656
665
  );
657
666
  yield* stateWrite(
658
667
  mapping,
@@ -660,8 +669,8 @@ export const makePhoneKernel = <
660
669
  {
661
670
  ...old,
662
671
  state: "retired",
663
- custodyRevision: SecurityRevision.make(randomId()),
664
- credentialRevision: SecurityRevision.make(randomId()),
672
+ custodyRevision: SecurityRevision.make(yield* randomId),
673
+ credentialRevision: SecurityRevision.make(yield* randomId),
665
674
  },
666
675
  captured.source.observation,
667
676
  );
@@ -1,13 +1,13 @@
1
- import { randomId } from "@yielded/auth-crypto";
2
1
  import { coordinateCommit, hasCommitScope, LifecycleHooks } from "@yielded/auth/Hooks";
3
2
  import type { LoginIdentifier } from "@yielded/auth/Identity";
4
3
  import { PasswordUnavailable } from "@yielded/auth/Password";
5
4
  import { reportPersistenceFailure } from "@yielded/auth/Persistence";
6
5
  import type { SubjectId } from "@yielded/auth/Schema";
7
6
  import { SecurityRevision } from "@yielded/auth/Sessions";
8
- import { type Context, Effect, Schema } from "effect";
7
+ import { type Context, Crypto, Effect, Schema } from "effect";
9
8
 
10
9
  import { PersistenceConfigurationError } from "./configuration";
10
+ import { randomId } from "./crypto";
11
11
  import { CurrentPasswordSql } from "./password-kernel";
12
12
  import type { QueryOperations } from "./query-operations";
13
13
  import type { PasswordRegistrationAuthority } from "./registration-contract";
@@ -25,7 +25,7 @@ type CreateSubject = (input: {
25
25
  * always suppress; a public request ID never recovers a private password intent.
26
26
  */
27
27
  export const makeRegistrationAuthority = Effect.fn("makeRegistrationAuthority")(function* <R>(
28
- mappings: ReturnType<typeof makeMappings>,
28
+ mappings: Effect.Success<ReturnType<typeof makeMappings>>,
29
29
  operations: QueryOperations,
30
30
  standalone: Effect.Effect<void, PasswordUnavailable>,
31
31
  provisioning: Context.Key<R, object>,
@@ -33,10 +33,11 @@ export const makeRegistrationAuthority = Effect.fn("makeRegistrationAuthority")(
33
33
  ): Effect.fn.Return<
34
34
  PasswordRegistrationAuthority<unknown>,
35
35
  PersistenceConfigurationError,
36
- LifecycleHooks | CurrentPasswordSql | R
36
+ LifecycleHooks | CurrentPasswordSql | Crypto.Crypto | R
37
37
  > {
38
38
  const database = yield* CurrentPasswordSql;
39
39
  const hooks = yield* LifecycleHooks;
40
+ const crypto = yield* Crypto.Crypto;
40
41
  // Core has already decoded registration with the selected strategy's Schema;
41
42
  // the dynamic strategy table erases only that heterogeneous callback signature.
42
43
  const creators = (yield* provisioning) as Readonly<Record<string, CreateSubject>>;
@@ -129,7 +130,7 @@ export const makeRegistrationAuthority = Effect.fn("makeRegistrationAuthority")(
129
130
  mapping.identifier.encodeInitialInsert(
130
131
  input.identifier,
131
132
  nativeId,
132
- SecurityRevision.make(randomId()),
133
+ SecurityRevision.make(yield* randomId),
133
134
  ),
134
135
  )
135
136
  .onConflictDoNothing()
@@ -139,8 +140,8 @@ export const makeRegistrationAuthority = Effect.fn("makeRegistrationAuthority")(
139
140
  // subject too, then persist a non-authorizing receipt in a new transaction.
140
141
  if (bound.length !== 1) return yield* new IdentifierTaken({});
141
142
 
142
- const credentialId = randomId();
143
- const credentialRevision = SecurityRevision.make(randomId());
143
+ const credentialId = yield* randomId;
144
+ const credentialRevision = SecurityRevision.make(yield* randomId);
144
145
 
145
146
  yield* tx.insert(mapping.credential.table).values(
146
147
  mapping.credential.encodeInsert({
@@ -148,7 +149,7 @@ export const makeRegistrationAuthority = Effect.fn("makeRegistrationAuthority")(
148
149
  subjectId: nativeId,
149
150
  credentialId,
150
151
  credentialRevision,
151
- verifierVersion: SecurityRevision.make(randomId()),
152
+ verifierVersion: SecurityRevision.make(yield* randomId),
152
153
  replacement: input.replacement,
153
154
  }),
154
155
  );
@@ -169,6 +170,7 @@ export const makeRegistrationAuthority = Effect.fn("makeRegistrationAuthority")(
169
170
  return committed.value;
170
171
  }).pipe(
171
172
  Effect.provideService(LifecycleHooks, hooks),
173
+ Effect.provideService(Crypto.Crypto, crypto),
172
174
  (work) => reportPersistenceFailure(work, Schema.is(PasswordUnavailable)),
173
175
  Effect.mapError(() => PasswordUnavailable.make({})),
174
176
  ),
@@ -466,7 +466,7 @@ export const makeSessionKernel = (operations: QueryOperations) => {
466
466
  const ensureDirectFlowAvailable = Effect.fn("DrizzleSession.ensureFlowAvailable")(function* (
467
467
  mapping: AnyAuthorityMapping &
468
468
  AnyFlowMapping & {
469
- readonly pending?: PendingAuthenticationTables<any, any, any, any>["pending"];
469
+ readonly pending?: PendingAuthenticationTables<any, any, any, any>["pending"] | undefined;
470
470
  },
471
471
  evidence: AuthenticationEvidence,
472
472
  nativeSubjectId: unknown,
@@ -1389,10 +1389,11 @@ export const makeSessionKernel = (operations: QueryOperations) => {
1389
1389
  );
1390
1390
 
1391
1391
  const extra = decoded[input.limit];
1392
+ const nextCursor = extra === undefined ? undefined : sessions.at(-1)?.sessionId;
1392
1393
 
1393
1394
  return {
1394
1395
  sessions,
1395
- ...(extra === undefined ? {} : { nextCursor: sessions.at(-1)?.sessionId }),
1396
+ ...(nextCursor === undefined ? {} : { nextCursor }),
1396
1397
  };
1397
1398
  }),
1398
1399
  ),
@@ -107,7 +107,7 @@ interface QueryState {
107
107
  readonly on: Fragment;
108
108
  }>;
109
109
  readonly selection?: Selection;
110
- readonly where?: Fragment;
110
+ readonly where?: Fragment | undefined;
111
111
  readonly values?: Row;
112
112
  readonly returning?: Selection | true;
113
113
  readonly order?: ReadonlyArray<Fragment>;
@@ -258,7 +258,8 @@ export const makeSqlDatabase = Effect.fnUntraced(function* (dialect: Dialect) {
258
258
 
259
259
  const database: SqlDatabase = {
260
260
  $client: client,
261
- select: (selection?: Selection) => query({ operation: "select", selection }),
261
+ select: (selection?: Selection) =>
262
+ query({ operation: "select", ...(selection === undefined ? {} : { selection }) }),
262
263
  insert: (table: Table) => query({ operation: "insert", table }),
263
264
  update: (table: Table) => query({ operation: "update", table }),
264
265
  delete: (table: Table) => query({ operation: "delete", table }),