@yielded/auth-persistence 0.1.0-beta.30 → 0.1.0-beta.31

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 (48) hide show
  1. package/README.md +3 -6
  2. package/dist/Adapter.d.mts +1 -1
  3. package/dist/internal/configuration.d.mts +18 -8
  4. package/dist/internal/configuration.mjs.map +1 -1
  5. package/dist/internal/models/password-model.d.mts +2 -1
  6. package/dist/internal/models/password-model.mjs.map +1 -1
  7. package/dist/internal/passkey-managed.mjs +4 -2
  8. package/dist/internal/passkey-managed.mjs.map +1 -1
  9. package/dist/internal/password-native.mjs +14 -0
  10. package/dist/internal/password-native.mjs.map +1 -1
  11. package/dist/internal/password-registration-native.d.mts +1 -1
  12. package/dist/internal/persistence.d.mts +3 -0
  13. package/dist/internal/persistence.mjs +4 -6
  14. package/dist/internal/persistence.mjs.map +1 -1
  15. package/dist/internal/registration.mjs +17 -8
  16. package/dist/internal/registration.mjs.map +1 -1
  17. package/dist/internal/session-native-authority.mjs +2 -1
  18. package/dist/internal/session-native-authority.mjs.map +1 -1
  19. package/dist/internal/session-native-login.mjs +2 -1
  20. package/dist/internal/session-native-login.mjs.map +1 -1
  21. package/dist/internal/session-native-pending-read.mjs +1 -1
  22. package/dist/internal/session-native-pending-read.mjs.map +1 -1
  23. package/dist/internal/session-native-state.mjs +21 -8
  24. package/dist/internal/session-native-state.mjs.map +1 -1
  25. package/dist/internal/session-native-stateful.mjs +2 -1
  26. package/dist/internal/session-native-stateful.mjs.map +1 -1
  27. package/dist/internal/session-native-step-up.mjs +4 -2
  28. package/dist/internal/session-native-step-up.mjs.map +1 -1
  29. package/dist/internal/sqlite-policy-guard.mjs +23 -0
  30. package/dist/internal/sqlite-policy-guard.mjs.map +1 -0
  31. package/dist/internal/storage-mapping.d.mts +9 -5
  32. package/dist/internal/storage-mapping.mjs +16 -4
  33. package/dist/internal/storage-mapping.mjs.map +1 -1
  34. package/package.json +1 -1
  35. package/src/internal/configuration.ts +26 -8
  36. package/src/internal/models/password-model.ts +2 -1
  37. package/src/internal/passkey-managed.ts +8 -2
  38. package/src/internal/password-native.ts +28 -2
  39. package/src/internal/persistence.ts +174 -164
  40. package/src/internal/registration.ts +53 -40
  41. package/src/internal/session-native-authority.ts +3 -1
  42. package/src/internal/session-native-login.ts +3 -1
  43. package/src/internal/session-native-pending-read.ts +1 -1
  44. package/src/internal/session-native-state.ts +29 -10
  45. package/src/internal/session-native-stateful.ts +3 -1
  46. package/src/internal/session-native-step-up.ts +5 -2
  47. package/src/internal/sqlite-policy-guard.ts +34 -0
  48. package/src/internal/storage-mapping.ts +40 -4
@@ -65,6 +65,8 @@ export interface Backend<T extends object, R, Database extends object = object>
65
65
  readonly describe: (table: T) => StorageTable;
66
66
  readonly acquire: Effect.Effect<Database, PersistenceConfigurationError, R | SqlClient.SqlClient>;
67
67
  readonly nativeTables: (database: Database) => NativeSqlTables;
68
+ /** Fixed atomic batches replace interactive transactions on backends such as D1. */
69
+ readonly batch?: (database: Database) => NonNullable<SqlBatchCommit["Service"]>;
68
70
  }
69
71
 
70
72
  const configError = (reason: string) => PersistenceConfigurationError.make({ reason });
@@ -233,194 +235,202 @@ export const createPersistence = <T extends object, R, Database extends object =
233
235
  };
234
236
 
235
237
  const services = Layer.effectContext(
236
- Effect.gen(function* () {
237
- const storage = yield* ConfigKey;
238
- const client = yield* SqlClient.SqlClient;
239
- const database = yield* backend.acquire;
240
-
241
- const dialect = client.onDialectOrElse({
242
- pg: () => "pg" as const,
243
- sqlite: () => "sqlite" as const,
244
- orElse: () => undefined,
245
- });
238
+ Effect.flatMap(backend.acquire, (database) =>
239
+ Effect.gen(function* () {
240
+ const storage = yield* ConfigKey;
241
+ const client = yield* SqlClient.SqlClient;
242
+
243
+ const dialect = client.onDialectOrElse({
244
+ pg: () => "pg" as const,
245
+ sqlite: () => "sqlite" as const,
246
+ orElse: () => undefined,
247
+ });
246
248
 
247
- if (dialect === undefined)
248
- return yield* configError("Use an explicit adapter for this SQL dialect");
249
- if (auth.sessionMode !== "stateful")
250
- return yield* configError("The composed layer currently requires stateful sessions");
251
- if (
252
- features.some(
253
- (feature) =>
254
- feature === undefined ||
255
- ("lifecycle" in feature && feature.lifecycle) ||
256
- (feature.kind === "email" && !feature.addresses),
249
+ if (dialect === undefined)
250
+ return yield* configError("Use an explicit adapter for this SQL dialect");
251
+ if (auth.sessionMode !== "stateful")
252
+ return yield* configError("The composed layer currently requires stateful sessions");
253
+ if (
254
+ features.some(
255
+ (feature) =>
256
+ feature === undefined ||
257
+ ("lifecycle" in feature && feature.lifecycle) ||
258
+ (feature.kind === "email" && !feature.addresses),
259
+ )
257
260
  )
258
- )
259
- return yield* configError(
260
- "Use explicit services for strategies without composed persistence support",
261
- );
262
- if (storage.namespace !== auth.namespace)
263
- return yield* configError(
264
- "Persistence configuration belongs to a different Auth definition",
265
- );
266
- const validations: StorageValidation[] = [];
261
+ return yield* configError(
262
+ "Use explicit services for strategies without composed persistence support",
263
+ );
264
+ if (storage.namespace !== auth.namespace)
265
+ return yield* configError(
266
+ "Persistence configuration belongs to a different Auth definition",
267
+ );
268
+ const validations: StorageValidation[] = [];
267
269
 
268
- for (const role of roles) {
269
- const table = (storage.schema as Partial<Record<StorageRole, T>>)[role];
270
+ for (const role of roles) {
271
+ const table = (storage.schema as Partial<Record<StorageRole, T>>)[role];
270
272
 
271
- if (table === undefined) return yield* configError(`Missing ${role} table`);
272
- const description = backend.describe(table);
273
- const columns = description.columns;
273
+ if (table === undefined) return yield* configError(`Missing ${role} table`);
274
+ const description = backend.describe(table);
275
+ const columns = description.columns;
274
276
 
275
- for (const name of Object.keys(storageTables[role].columns)) {
276
- if (columns[name] === undefined)
277
- return yield* configError(`Missing ${role}.${name} column`);
277
+ for (const name of Object.keys(storageTables[role].columns)) {
278
+ if (columns[name] === undefined)
279
+ return yield* configError(`Missing ${role}.${name} column`);
280
+ }
281
+ validations.push({ table: description, required: storageTables[role].unique });
278
282
  }
279
- validations.push({ table: description, required: storageTables[role].unique });
280
- }
281
- validations.push({
282
- table: backend.describe(storage.subjects.table as T),
283
- required: [[storage.subjects.id]],
284
- });
285
- yield* validateStorageBatch(dialect, validations);
286
- const mappings = yield* makeMappings(storage);
287
-
288
- const nativeTables = backend.nativeTables(database);
289
- const sessionMapping = mappings.sessions(auth.claims, auth.sessions.moduleId);
290
- const pendingMapping = mappings.pending(auth.claims, auth.sessions.moduleId);
291
-
292
- const sessionServices = yield* makeNativeStatefulSessionServices(
293
- nativeTables,
294
- sessionMapping,
295
- );
296
-
297
- const { authenticationAuthority } = yield* makeNativeAuthenticationAuthorityServices(
298
- nativeTables,
299
- sessionMapping,
300
- );
301
-
302
- const { pendingAuthentication } = yield* makeNativePendingAuthenticationServices(
303
- nativeTables,
304
- pendingMapping,
305
- );
306
-
307
- const { sessionStepUpPersistence } = yield* makeNativeSessionStepUpServices(nativeTables, {
308
- ...pendingMapping,
309
- source: {
310
- kind: "Stateful",
311
- session: sessionMapping.session,
312
- sessionId: sessionMapping.sessionId,
313
- constraints: { sessionDigest: "unique(session.digest)" },
314
- },
315
- });
316
-
317
- const { sessionCleanup } = yield* makeNativeSessionCleanupServices(
318
- nativeTables,
319
- pendingMapping,
320
- );
321
-
322
- let context: Context.Context<never> = Context.make(
323
- AuthenticationAuthority,
324
- authenticationAuthority,
325
- ).pipe(
326
- Context.add(
327
- auth.sessions.StatefulSessionPersistence,
328
- sessionServices.statefulSessionPersistence,
329
- ),
330
- Context.add(auth.sessions.SessionRepository, sessionServices.sessionRepository),
331
- Context.add(auth.sessions.PendingAuthentication, pendingAuthentication),
332
- Context.add(auth.sessions.SessionStepUpPersistence, sessionStepUpPersistence),
333
- Context.add(auth.sessions.SessionCleanup, sessionCleanup),
334
- );
283
+ validations.push({
284
+ table: backend.describe(storage.subjects.table as T),
285
+ required: [[storage.subjects.id]],
286
+ });
287
+ yield* validateStorageBatch(dialect, validations);
288
+ const nativeTables = backend.nativeTables(database);
289
+ const mappings = yield* makeMappings(storage, nativeTables);
290
+ const sessionMapping = mappings.sessions(auth.claims, auth.sessions.moduleId);
291
+ const pendingMapping = mappings.pending(auth.claims, auth.sessions.moduleId);
292
+
293
+ const sessionServices = yield* makeNativeStatefulSessionServices(
294
+ nativeTables,
295
+ sessionMapping,
296
+ );
335
297
 
336
- const proofMapping = proofs ? mappings.proofs() : undefined;
298
+ const { authenticationAuthority } = yield* makeNativeAuthenticationAuthorityServices(
299
+ nativeTables,
300
+ sessionMapping,
301
+ );
337
302
 
338
- if (proofMapping !== undefined) {
339
- const services = yield* makeNativeProofServices(
340
- backend.nativeTables(database),
341
- proofMapping,
303
+ const { pendingAuthentication } = yield* makeNativePendingAuthenticationServices(
304
+ nativeTables,
305
+ pendingMapping,
342
306
  );
343
307
 
344
- context = Context.add(context, ProofPersistence, services.proofPersistence);
345
- }
308
+ const { sessionStepUpPersistence } = yield* makeNativeSessionStepUpServices(
309
+ nativeTables,
310
+ {
311
+ ...pendingMapping,
312
+ source: {
313
+ kind: "Stateful",
314
+ session: sessionMapping.session,
315
+ sessionId: sessionMapping.sessionId,
316
+ constraints: { sessionDigest: "unique(session.digest)" },
317
+ },
318
+ },
319
+ );
346
320
 
347
- if (password) {
348
- const mapping = mappings.passwords();
321
+ const { sessionCleanup } = yield* makeNativeSessionCleanupServices(
322
+ nativeTables,
323
+ pendingMapping,
324
+ );
349
325
 
350
- const services = yield* makeNativePasswordServices(
351
- backend.nativeTables(database),
352
- mapping,
353
- proofMapping,
326
+ let context: Context.Context<never> = Context.make(
327
+ AuthenticationAuthority,
328
+ authenticationAuthority,
329
+ ).pipe(
330
+ Context.add(
331
+ auth.sessions.StatefulSessionPersistence,
332
+ sessionServices.statefulSessionPersistence,
333
+ ),
334
+ Context.add(auth.sessions.SessionRepository, sessionServices.sessionRepository),
335
+ Context.add(auth.sessions.PendingAuthentication, pendingAuthentication),
336
+ Context.add(auth.sessions.SessionStepUpPersistence, sessionStepUpPersistence),
337
+ Context.add(auth.sessions.SessionCleanup, sessionCleanup),
354
338
  );
355
339
 
356
- context = Context.add(context, PasswordPersistence, services.passwordPersistence);
357
- }
358
- for (const [name, strategy] of Object.entries(auth.strategies)) {
359
- if (strategy.persistence?.kind !== "password" || !strategy.persistence.management)
360
- continue;
361
- const key = strategy.RegistrationAuthority;
340
+ const proofMapping = proofs ? mappings.proofs() : undefined;
362
341
 
363
- if (key === undefined)
364
- return yield* configError(`Missing subject provisioning for ${name}`);
342
+ if (proofMapping !== undefined) {
343
+ const services = yield* makeNativeProofServices(
344
+ backend.nativeTables(database),
345
+ proofMapping,
346
+ );
365
347
 
366
- const registration = yield* makeRegistrationAuthority(
367
- backend.nativeTables(database),
368
- mappings.passwords(),
369
- ProvisioningKey,
370
- name,
371
- );
348
+ context = Context.add(context, ProofPersistence, services.proofPersistence);
349
+ }
372
350
 
373
- context = Context.add(context, key, registration);
374
- }
375
- if (email)
376
- context = Context.add(
377
- context,
378
- EmailAddressPersistence,
379
- (yield* makeNativeEmailAddressServices(
351
+ if (password) {
352
+ const mapping = mappings.passwords();
353
+
354
+ const services = yield* makeNativePasswordServices(
380
355
  backend.nativeTables(database),
381
- mappings.emails(),
356
+ mapping,
382
357
  proofMapping,
383
- )).emailAddressPersistence,
384
- );
358
+ );
359
+
360
+ context = Context.add(context, PasswordPersistence, services.passwordPersistence);
361
+ }
362
+ for (const [name, strategy] of Object.entries(auth.strategies)) {
363
+ if (strategy.persistence?.kind !== "password" || !strategy.persistence.management)
364
+ continue;
365
+ const key = strategy.RegistrationAuthority;
385
366
 
386
- if (phone)
387
- context = Context.add(
388
- context,
389
- PhoneSignInTargets,
390
- yield* makeComposedPhoneTargets(
367
+ if (key === undefined)
368
+ return yield* configError(`Missing subject provisioning for ${name}`);
369
+
370
+ const registration = yield* makeRegistrationAuthority(
391
371
  backend.nativeTables(database),
392
- storage,
393
- mappings.proofs(),
394
- features.flatMap((feature) => (feature?.kind === "phone" ? [feature.moduleId] : [])),
395
- ),
396
- );
372
+ mappings.passwords(),
373
+ ProvisioningKey,
374
+ name,
375
+ );
397
376
 
398
- if (passkeys.length > 0) {
399
- // Auth capability metadata determines whether PasskeyConfig is required.
400
- const services = makeManagedPasskeys(
401
- {
402
- storage,
403
- namespace: auth.namespace,
404
- dialect,
405
- features: passkeys,
406
- passwordModules: features.flatMap((feature) =>
407
- feature?.kind === "password" ? [feature.moduleId] : [],
377
+ context = Context.add(context, key, registration);
378
+ }
379
+ if (email)
380
+ context = Context.add(
381
+ context,
382
+ EmailAddressPersistence,
383
+ (yield* makeNativeEmailAddressServices(
384
+ backend.nativeTables(database),
385
+ mappings.emails(),
386
+ proofMapping,
387
+ )).emailAddressPersistence,
388
+ );
389
+
390
+ if (phone)
391
+ context = Context.add(
392
+ context,
393
+ PhoneSignInTargets,
394
+ yield* makeComposedPhoneTargets(
395
+ backend.nativeTables(database),
396
+ storage,
397
+ mappings.proofs(),
398
+ features.flatMap((feature) =>
399
+ feature?.kind === "phone" ? [feature.moduleId] : [],
400
+ ),
408
401
  ),
409
- },
410
- backend.nativeTables(database),
411
- ) as Effect.Effect<
412
- Context.Context<never>,
413
- PersistenceConfigurationError,
414
- PasskeyRequirement<A> | Crypto.Crypto | LifecycleHooks | SqlClient.SqlClient
415
- >;
416
-
417
- context = Context.merge(context, yield* services);
418
- }
402
+ );
419
403
 
420
- // The checked capability metadata above determines exactly these service keys.
421
- return context as Context.Context<Ports<C, Id, A>>;
422
- }).pipe(
423
- Effect.provideService(SqlBatchCommit, undefined),
404
+ if (passkeys.length > 0) {
405
+ // Auth capability metadata determines whether PasskeyConfig is required.
406
+ const services = makeManagedPasskeys(
407
+ {
408
+ storage,
409
+ namespace: auth.namespace,
410
+ dialect,
411
+ features: passkeys,
412
+ passwordModules: features.flatMap((feature) =>
413
+ feature?.kind === "password" ? [feature.moduleId] : [],
414
+ ),
415
+ },
416
+ backend.nativeTables(database),
417
+ ) as Effect.Effect<
418
+ Context.Context<never>,
419
+ PersistenceConfigurationError,
420
+ | PasskeyRequirement<A>
421
+ | Crypto.Crypto
422
+ | LifecycleHooks
423
+ | SqlClient.SqlClient
424
+ | SqlBatchCommit
425
+ >;
426
+
427
+ context = Context.merge(context, yield* services);
428
+ }
429
+
430
+ // The checked capability metadata above determines exactly these service keys.
431
+ return context as Context.Context<Ports<C, Id, A>>;
432
+ }).pipe(Effect.provideService(SqlBatchCommit, backend.batch?.(database))),
433
+ ).pipe(
424
434
  withStorageValidation,
425
435
  Effect.mapError((error) =>
426
436
  Schema.is(PersistenceConfigurationError)(error)
@@ -1,12 +1,10 @@
1
1
  import type { LifecycleHooks } from "@yielded/auth/Hooks";
2
- import type { LoginIdentifier } from "@yielded/auth/Identity";
3
2
  import { PasswordUnavailable } from "@yielded/auth/Password";
4
- import type { SubjectId } from "@yielded/auth/Schema";
5
3
  import { SecurityRevision } from "@yielded/auth/Sessions";
6
4
  import { type Context, Crypto, Effect, Schema, Option, Cause } from "effect";
7
5
  import { SqlClient } from "effect/sql/SqlClient";
8
6
 
9
- import { PersistenceConfigurationError } from "./configuration";
7
+ import { PersistenceConfigurationError, type SubjectProvisioning } from "./configuration";
10
8
  import { randomId } from "./crypto";
11
9
  import type { AnyPasswordPersistenceMapping } from "./models/password-model";
12
10
  import type { NativeSqlTables } from "./native-sql-table";
@@ -15,13 +13,9 @@ import {
15
13
  PasswordIdentifierTaken,
16
14
  } from "./password-registration-native";
17
15
  import type { PasswordRegistrationAuthority } from "./registration-contract";
18
- import { makeSqlCommitExecutor, CurrentSqlCommit } from "./sql-commit";
16
+ import { makeSqlCommitExecutor, CurrentSqlCommit, SqlBatchCommit } from "./sql-commit";
19
17
 
20
- type CreateSubject = (input: {
21
- readonly requestId: string;
22
- readonly identifier: LoginIdentifier;
23
- readonly registration: unknown;
24
- }) => Effect.Effect<SubjectId, PasswordUnavailable>;
18
+ const SubjectValues = Schema.Record(Schema.String, Schema.Unknown);
25
19
 
26
20
  /** Application provisioning and the unverified identifier/password binding share
27
21
  * one physical owner. Identifier uniqueness arbitrates registration; correlation
@@ -34,49 +28,56 @@ export const makeRegistrationAuthority = Effect.fnUntraced(function* <R>(
34
28
  ): Effect.fn.Return<
35
29
  PasswordRegistrationAuthority<unknown>,
36
30
  PersistenceConfigurationError,
37
- LifecycleHooks | SqlClient | Crypto.Crypto | R
31
+ LifecycleHooks | SqlClient | Crypto.Crypto | SqlBatchCommit | R
38
32
  > {
39
33
  const sql = (yield* SqlClient).withoutTransforms();
40
34
  const crypto = yield* Crypto.Crypto;
41
35
  const executor = yield* makeSqlCommitExecutor(() => PasswordUnavailable.make({}));
42
36
  const parent = yield* Effect.serviceOption(CurrentSqlCommit);
43
- const store = yield* makeNativePasswordRegistrationStore(tables, mapping);
37
+ const batch = yield* SqlBatchCommit;
38
+ const store = yield* makeNativePasswordRegistrationStore(tables, mapping, batch !== undefined);
44
39
  // The strategy has already decoded its registration Schema. Only its callback
45
40
  // signature is erased by the heterogeneous strategy service map.
46
- const creators = (yield* provisioning) as Readonly<Record<string, CreateSubject>>;
41
+ const creators = (yield* provisioning) as Readonly<Record<string, SubjectProvisioning<unknown>>>;
47
42
  const createSubject = creators[strategy];
48
43
 
49
44
  if (createSubject === undefined)
50
45
  return yield* PersistenceConfigurationError.make({
51
46
  reason: `Missing subject provisioning for ${strategy}`,
52
47
  });
48
+ if (batch !== undefined && typeof createSubject === "function")
49
+ return yield* PersistenceConfigurationError.make({
50
+ reason: `D1 subject provisioning for ${strategy} requires { values }; return the new subject row instead of inserting it`,
51
+ });
53
52
  const s = mapping.subject;
54
53
  const subject = tables(s.table);
55
54
  const lock = sql.onDialectOrElse({ sqlite: () => sql``, orElse: () => sql`for update` });
56
55
 
57
56
  const run = <A, E, R2>(work: Effect.Effect<A, E, R2>, suppressed: Effect.Effect<A, E, R2>) =>
58
- Option.isSome(parent)
59
- ? executor.run(work)
60
- : executor
61
- .operation(
62
- work.pipe(
63
- Effect.mapError((error) =>
64
- error instanceof PasswordIdentifierTaken ? error : PasswordUnavailable.make({}),
57
+ batch !== undefined
58
+ ? executor.batch(work).pipe(Effect.provideService(SqlBatchCommit, batch))
59
+ : Option.isSome(parent)
60
+ ? executor.run(work)
61
+ : executor
62
+ .operation(
63
+ work.pipe(
64
+ Effect.mapError((error) =>
65
+ error instanceof PasswordIdentifierTaken ? error : PasswordUnavailable.make({}),
66
+ ),
65
67
  ),
66
- ),
67
- )
68
- .pipe(
69
- Effect.catchCause((cause) =>
70
- cause.reasons.length > 0 &&
71
- cause.reasons.every(
72
- (reason) =>
73
- Cause.isFailReason(reason) && reason.error instanceof PasswordIdentifierTaken,
74
- )
75
- ? executor.run(suppressed, "statement")
76
- : Effect.failCause(cause),
77
- ),
78
- Effect.mapError(() => PasswordUnavailable.make({})),
79
- );
68
+ )
69
+ .pipe(
70
+ Effect.catchCause((cause) =>
71
+ cause.reasons.length > 0 &&
72
+ cause.reasons.every(
73
+ (reason) =>
74
+ Cause.isFailReason(reason) && reason.error instanceof PasswordIdentifierTaken,
75
+ )
76
+ ? executor.run(suppressed, "statement")
77
+ : Effect.failCause(cause),
78
+ ),
79
+ Effect.mapError(() => PasswordUnavailable.make({})),
80
+ );
80
81
 
81
82
  return {
82
83
  register: (input, project) =>
@@ -86,26 +87,38 @@ export const makeRegistrationAuthority = Effect.fnUntraced(function* <R>(
86
87
  ? executor.run(store.prepare({ _tag: "Suppressed" }, project), "statement")
87
88
  : run(
88
89
  Effect.gen(function* () {
89
- const subjectId = yield* createSubject({
90
+ const provisioningInput = {
90
91
  requestId: input.requestId,
91
92
  identifier: input.identifier,
92
93
  registration: input.registration,
93
- });
94
+ };
94
95
 
95
- const nativeId = yield* mapping.subjectId.toNative(subjectId);
96
+ const row = yield* Effect.gen(function* () {
97
+ if (typeof createSubject !== "function")
98
+ return yield* createSubject
99
+ .values(provisioningInput)
100
+ .pipe(Effect.flatMap(Schema.decodeEffect(SubjectValues)));
101
+ const subjectId = yield* createSubject(provisioningInput);
102
+ const nativeId = yield* mapping.subjectId.toNative(subjectId);
96
103
 
97
- const rows =
98
- yield* sql`select ${subject.fields("registration_subject_")} from ${subject.name} where ${subject.column(s.id)} = ${subject.value(s.id, nativeId)} limit 2 ${lock}`;
104
+ const rows =
105
+ yield* sql`select ${subject.fields("registration_subject_")} from ${subject.name} where ${subject.column(s.id)} = ${subject.value(s.id, nativeId)} limit 2 ${lock}`;
99
106
 
100
- if (rows.length !== 1) return yield* PasswordUnavailable.make({});
101
- const row = subject.decode(rows[0]!, "registration_subject_");
107
+ if (rows.length !== 1) return yield* PasswordUnavailable.make({});
108
+
109
+ return subject.decode(rows[0]!, "registration_subject_");
110
+ });
102
111
 
103
112
  if (!s.isActiveStatus(row[s.status])) return yield* PasswordUnavailable.make({});
113
+ const subjectId = yield* mapping.subjectId.toSubject(row[s.id]);
104
114
 
105
115
  const securityRevision = yield* Schema.decodeUnknownEffect(SecurityRevision)(
106
116
  row[s.securityRevision],
107
117
  );
108
118
 
119
+ if (typeof createSubject !== "function")
120
+ yield* store.stage(sql`${subject.insert(row)}`);
121
+
109
122
  const identifierRevision = SecurityRevision.make(yield* randomId);
110
123
  const credentialId = yield* randomId;
111
124
  const credentialRevision = SecurityRevision.make(yield* randomId);
@@ -195,7 +195,9 @@ export const makeNativeAuthenticationAuthorityServices = Effect.fnUntraced(funct
195
195
  : yield* consume(input.pending, input.evidence, current.native);
196
196
 
197
197
  const deadline = Math.min(assessment.validUntil, expires, pendingExpiry ?? Infinity);
198
- const condition = sql`${state.authorityCondition(current.revision, current.native)} and ${state.policyCondition(current.row, current.native)} and ${now} >= ${current.now} and ${now} < ${deadline}`;
198
+
199
+ yield* state.guardPolicy(current);
200
+ const condition = sql`${state.authorityCondition(current.revision, current.native)} and ${now} >= ${current.now} and ${now} < ${deadline}`;
199
201
 
200
202
  if (batch !== undefined) {
201
203
  yield* appendSqlBatchStatement(sqlBatchAssertion(sql, condition));
@@ -129,7 +129,9 @@ export const makeNativePendingAuthenticationServices = Effect.fnUntraced(functio
129
129
 
130
130
  const record: PendingAuthenticationRecord<Claims> = { ...input, version };
131
131
  const snapshot = yield* mapping.login.encode(record);
132
- const condition = sql`${state.authorityCondition(current.revision, current.native)} and ${state.policyCondition(current.row, current.native)} and ${now} >= ${current.now} and ${now} < ${expires}`;
132
+
133
+ yield* state.guardPolicy(current);
134
+ const condition = sql`${state.authorityCondition(current.revision, current.native)} and ${now} >= ${current.now} and ${now} < ${expires}`;
133
135
 
134
136
  if (batch !== undefined)
135
137
  yield* appendSqlBatchStatement(sqlBatchAssertion(sql, condition));
@@ -47,7 +47,7 @@ export const makeNativeSessionPendingReader = Effect.fnUntraced(function* (
47
47
  const authority = yield* state.decode(
48
48
  record.subjectId,
49
49
  owner,
50
- st.decode(rows[0], "s_"),
50
+ rows[0],
51
51
  rows.map((row) => ct.decode(row, "c_")),
52
52
  rows[0].engine_now,
53
53
  );