@yielded/auth-persistence 0.1.0-beta.10 → 0.1.0-beta.12

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 +7 -7
  2. package/dist/Adapter.d.mts +2 -2
  3. package/dist/Adapter.mjs +4 -4
  4. package/dist/index.d.mts +1 -2
  5. package/dist/index.mjs +1 -2
  6. package/dist/internal/PasswordPreparedPostconditions.d.mts +1 -1
  7. package/dist/internal/configuration.d.mts +2 -2
  8. package/dist/internal/configuration.mjs.map +1 -1
  9. package/dist/internal/email-kernel.d.mts +6 -6
  10. package/dist/internal/email-kernel.mjs +8 -6
  11. package/dist/internal/email-kernel.mjs.map +1 -1
  12. package/dist/internal/models/email-model.d.mts +1 -1
  13. package/dist/internal/oauth-server.d.mts +1 -1
  14. package/dist/internal/oauth-server.mjs +2 -2
  15. package/dist/internal/oauth-server.mjs.map +1 -1
  16. package/dist/internal/passkey/state.mjs +3 -2
  17. package/dist/internal/passkey/state.mjs.map +1 -1
  18. package/dist/internal/passkey/target.d.mts +1 -1
  19. package/dist/internal/passkey/target.mjs +21 -12
  20. package/dist/internal/passkey/target.mjs.map +1 -1
  21. package/dist/internal/passkey/write-state.mjs +3 -2
  22. package/dist/internal/passkey/write-state.mjs.map +1 -1
  23. package/dist/internal/passkey/write-target.mjs +6 -6
  24. package/dist/internal/passkey/write-target.mjs.map +1 -1
  25. package/dist/internal/passkey-kernel.d.mts +13 -16
  26. package/dist/internal/passkeys.mjs +9 -8
  27. package/dist/internal/passkeys.mjs.map +1 -1
  28. package/dist/internal/password-kernel.d.mts +4 -4
  29. package/dist/internal/password-kernel.mjs +3 -2
  30. package/dist/internal/password-kernel.mjs.map +1 -1
  31. package/dist/internal/persistence.d.mts +4 -3
  32. package/dist/internal/persistence.mjs +23 -27
  33. package/dist/internal/persistence.mjs.map +1 -1
  34. package/dist/internal/proof-kernel.d.mts +3 -3
  35. package/dist/internal/proof-kernel.mjs +2 -1
  36. package/dist/internal/proof-kernel.mjs.map +1 -1
  37. package/dist/internal/query-operations.d.mts +1 -1
  38. package/dist/internal/registration.mjs +6 -1
  39. package/dist/internal/registration.mjs.map +1 -1
  40. package/dist/internal/session-kernel.d.mts +20 -10
  41. package/dist/internal/session-kernel.mjs +155 -147
  42. package/dist/internal/session-kernel.mjs.map +1 -1
  43. package/dist/internal/sql-persistence.mjs +3 -4
  44. package/dist/internal/sql-persistence.mjs.map +1 -1
  45. package/dist/internal/sql-query.mjs +4 -2
  46. package/dist/internal/sql-query.mjs.map +1 -1
  47. package/dist/internal/standalone.d.mts +2 -4
  48. package/dist/internal/standalone.mjs +2 -2
  49. package/dist/internal/standalone.mjs.map +1 -1
  50. package/dist/internal/storage-mapping.mjs +2 -2
  51. package/dist/internal/storage-validation.mjs +4 -2
  52. package/dist/internal/storage-validation.mjs.map +1 -1
  53. package/dist/internal/transaction-execution-kernel.d.mts +8 -9
  54. package/dist/internal/transaction-execution-kernel.mjs +9 -7
  55. package/dist/internal/transaction-execution-kernel.mjs.map +1 -1
  56. package/dist/internal/transaction-kernel.d.mts +9 -5
  57. package/dist/internal/transaction-kernel.mjs +5 -2
  58. package/dist/internal/transaction-kernel.mjs.map +1 -1
  59. package/package.json +1 -1
  60. package/src/Adapter.ts +6 -1
  61. package/src/index.ts +0 -1
  62. package/src/internal/configuration.ts +1 -1
  63. package/src/internal/email-kernel.ts +31 -36
  64. package/src/internal/oauth-server.ts +2 -2
  65. package/src/internal/passkey/state.ts +3 -2
  66. package/src/internal/passkey/target.ts +37 -44
  67. package/src/internal/passkey/write-state.ts +3 -2
  68. package/src/internal/passkey/write-target.ts +5 -15
  69. package/src/internal/passkeys.ts +5 -10
  70. package/src/internal/password-kernel.ts +3 -3
  71. package/src/internal/persistence.ts +78 -69
  72. package/src/internal/proof-kernel.ts +3 -3
  73. package/src/internal/query-operations.ts +1 -1
  74. package/src/internal/registration.ts +22 -7
  75. package/src/internal/session-kernel.ts +1123 -1140
  76. package/src/internal/sql-persistence.ts +2 -2
  77. package/src/internal/sql-query.ts +6 -4
  78. package/src/internal/standalone.ts +6 -8
  79. package/src/internal/storage-validation.ts +20 -16
  80. package/src/internal/transaction-execution-kernel.ts +20 -18
  81. package/src/internal/transaction-kernel.ts +10 -4
  82. package/dist/internal/oauth-app.d.mts +0 -11
  83. package/dist/internal/oauth-app.mjs +0 -57
  84. package/dist/internal/oauth-app.mjs.map +0 -1
  85. package/src/internal/oauth-app.ts +0 -82
@@ -1,10 +1,11 @@
1
1
  import { QueryFailure, QueryOperations } from "./query-operations.mjs";
2
2
  import { AuthenticationAuthorityMapping, PendingAuthenticationMapping, SignedSessionValidityMapping, StatefulSessionMapping } from "./models/session-model.mjs";
3
3
  import { SessionStepUpMapping } from "./models/step-up-model.mjs";
4
- import { Context, Effect } from "effect";
4
+ import { Context, DateTime, Effect } from "effect";
5
+ import { SubjectId } from "@yielded/auth/Schema";
5
6
  import { LifecycleHooks } from "@yielded/auth/Hooks";
6
- import { AuthenticationAuthority, PendingAuthentication, SessionRepository, SessionStepUpPersistence, SessionUnavailable, SignedSessionValidity, StatefulSessionPersistence } from "@yielded/auth/Sessions";
7
- import * as SqlError$1 from "effect/unstable/sql/SqlError";
7
+ import { AuthenticationEvidence, AuthenticationRevision, PendingAuthentication, PendingAuthenticationInvalid, PendingConsumption, PrepareSessionCommit, SessionRepository, SessionStepUpPersistence, SessionUnavailable, SignedSessionValidity, StaleAuthentication, StatefulSessionPersistence } from "@yielded/auth/Sessions";
8
+ import * as SqlError$1 from "effect/sql/SqlError";
8
9
  //#region src/internal/session-kernel.d.ts
9
10
  type CommitMode = "interactive" | "synchronous";
10
11
  interface SessionSqlQuery<A = ReadonlyArray<any>> extends Effect.Effect<A, QueryFailure | SqlError$1.SqlError> {
@@ -23,7 +24,6 @@ export interface SessionSqlDatabase {
23
24
  readonly delete: (...args: ReadonlyArray<any>) => SessionSqlQuery;
24
25
  readonly transaction: <A, E, R>(body: (transaction: SessionSqlDatabase) => Effect.Effect<A, E, R>) => Effect.Effect<A, E | SqlError$1.SqlError, R>;
25
26
  }
26
- type Database = SessionSqlDatabase;
27
27
  declare const CurrentSessionSql_base: Context.ServiceClass<CurrentSessionSql, "effect-auth/drizzle/CurrentSessionSql", SessionSqlDatabase>;
28
28
  /** Native SQL authority for the current transaction or savepoint. */
29
29
  export declare class CurrentSessionSql extends CurrentSessionSql_base {}
@@ -35,14 +35,24 @@ export interface SessionSqlOptions {
35
35
  readonly standaloneGuard: Guard;
36
36
  }
37
37
  export declare const makeSessionKernel: (operations: QueryOperations) => {
38
- makeSqlAuthenticationAuthority: <Claims>(database: Database, mapping: AuthenticationAuthorityMapping<Claims, any, any, any, any, any>, options: SessionSqlOptions) => Effect.Effect<AuthenticationAuthority["Service"], never, LifecycleHooks>;
39
- makeSqlPendingAuthentication: <Claims>(database: Database, mapping: PendingAuthenticationMapping<Claims, any, any, any, any, any>, options: SessionSqlOptions) => Effect.Effect<PendingAuthentication<Claims>, never, LifecycleHooks>;
40
- makeSqlStatefulSessions: <Claims>(database: Database, mapping: StatefulSessionMapping<Claims, any, any, any, any, any, any, any>, options: SessionSqlOptions) => Effect.Effect<{
38
+ makeSqlAuthenticationAuthority: <Claims>(mapping: AuthenticationAuthorityMapping<Claims, any, any, any, any, any>, options: SessionSqlOptions) => Effect.Effect<{
39
+ readonly capture: (subjectId: SubjectId, credentialIds: ReadonlyArray<string>) => Effect.Effect<AuthenticationRevision, StaleAuthentication | SessionUnavailable>;
40
+ readonly requirements: (evidence: AuthenticationEvidence) => Effect.Effect<import("@yielded/auth/Sessions").AuthenticationRequirement, StaleAuthentication | SessionUnavailable>;
41
+ readonly approve: <A>(input: {
42
+ readonly evidence: AuthenticationEvidence;
43
+ readonly now: DateTime.Utc;
44
+ readonly expiresAt: DateTime.Utc;
45
+ readonly absoluteExpiresAt: DateTime.Utc;
46
+ readonly pending?: PendingConsumption;
47
+ }, prepare: PrepareSessionCommit<void, A>) => Effect.Effect<import("@yielded/auth/Hooks").PreparedCommit<A>, StaleAuthentication | PendingAuthenticationInvalid | SessionUnavailable>;
48
+ }, never, CurrentSessionSql | LifecycleHooks>;
49
+ makeSqlPendingAuthentication: <Claims>(mapping: PendingAuthenticationMapping<Claims, any, any, any, any, any>, options: SessionSqlOptions) => Effect.Effect<PendingAuthentication<Claims>, never, CurrentSessionSql | LifecycleHooks>;
50
+ makeSqlStatefulSessions: <Claims>(mapping: StatefulSessionMapping<Claims, any, any, any, any, any, any, any>, options: SessionSqlOptions) => Effect.Effect<{
41
51
  readonly statefulSessionPersistence: StatefulSessionPersistence<Claims>;
42
52
  readonly sessionRepository: SessionRepository;
43
- }, never, LifecycleHooks>;
44
- makeSqlSignedValidity: (database: Database, mapping: SignedSessionValidityMapping<any, any, any, any>, options: SessionSqlOptions) => Effect.Effect<SignedSessionValidity, never, LifecycleHooks>;
45
- makeSqlSessionStepUp: <Claims>(database: Database, mapping: SessionStepUpMapping<Claims, any, any, any, any, any, any, any>, options: SessionSqlOptions) => Effect.Effect<SessionStepUpPersistence<Claims>, never, LifecycleHooks>;
53
+ }, never, CurrentSessionSql | LifecycleHooks>;
54
+ makeSqlSignedValidity: (mapping: SignedSessionValidityMapping<any, any, any, any>, options: SessionSqlOptions) => Effect.Effect<SignedSessionValidity, never, CurrentSessionSql | LifecycleHooks>;
55
+ makeSqlSessionStepUp: <Claims>(mapping: SessionStepUpMapping<Claims, any, any, any, any, any, any, any>, options: SessionSqlOptions) => Effect.Effect<SessionStepUpPersistence<Claims>, never, CurrentSessionSql | LifecycleHooks>;
46
56
  };
47
57
  //#endregion
48
58
  //# sourceMappingURL=session-kernel.d.mts.map
@@ -179,7 +179,9 @@ const makeSessionKernel = (operations) => {
179
179
  })
180
180
  });
181
181
  };
182
- const makeSqlAuthenticationAuthority = (database, mapping, options) => Effect.map(LifecycleHooks, (hooks) => {
182
+ const makeSqlAuthenticationAuthority = Effect.fnUntraced(function* (mapping, options) {
183
+ const database = yield* CurrentSessionSql;
184
+ const hooks = yield* LifecycleHooks;
183
185
  const service = {
184
186
  capture: (subjectId, credentialIds) => safeTransaction(inTransaction(database, captureIn(mapping, subjectId, credentialIds, true))),
185
187
  requirements: (evidence) => safeTransaction(inTransaction(database, validateEvidenceIn(mapping, evidence, true).pipe(Effect.map((result) => result.requirement)))),
@@ -233,7 +235,9 @@ const makeSessionKernel = (operations) => {
233
235
  if (DateTime.toEpochMillis(yield* freshNow) >= DateTime.toEpochMillis(record.expiresAt)) return yield* invalidPending();
234
236
  return record;
235
237
  });
236
- const makeSqlPendingAuthentication = (database, mapping, options) => Effect.map(LifecycleHooks, (hooks) => {
238
+ const makeSqlPendingAuthentication = Effect.fnUntraced(function* (mapping, options) {
239
+ const database = yield* CurrentSessionSql;
240
+ const hooks = yield* LifecycleHooks;
237
241
  const service = {
238
242
  create: (input, _inputNow, prepare) => owned(database, options, mapping.isConstraintConflict, Effect.gen(function* () {
239
243
  const transaction = yield* CurrentSessionSql;
@@ -302,7 +306,9 @@ const makeSessionKernel = (operations) => {
302
306
  expiresAt: column(mapping.session.table, mapping.session.expiresAt),
303
307
  absoluteExpiresAt: column(mapping.session.table, mapping.session.absoluteExpiresAt)
304
308
  });
305
- const makeSqlStatefulSessions = (database, mapping, options) => Effect.map(LifecycleHooks, (hooks) => {
309
+ const makeSqlStatefulSessions = Effect.fnUntraced(function* (mapping, options) {
310
+ const database = yield* CurrentSessionSql;
311
+ const hooks = yield* LifecycleHooks;
306
312
  const c = sessionColumns(mapping);
307
313
  const persistence = {
308
314
  establish: (input, prepare) => owned(database, options, mapping.isConstraintConflict, Effect.gen(function* () {
@@ -449,7 +455,9 @@ const makeSessionKernel = (operations) => {
449
455
  sessionRepository: { list: (input) => repository.list(input).pipe(Effect.provideService(LifecycleHooks, hooks)) }
450
456
  };
451
457
  });
452
- const makeSqlSignedValidity = (database, mapping, options) => Effect.map(LifecycleHooks, (hooks) => {
458
+ const makeSqlSignedValidity = Effect.fnUntraced(function* (mapping, options) {
459
+ const database = yield* CurrentSessionSql;
460
+ const hooks = yield* LifecycleHooks;
453
461
  const subject = subjectColumns(mapping);
454
462
  const tombstone = {
455
463
  subjectId: column(mapping.tombstone.table, mapping.tombstone.subjectId),
@@ -580,155 +588,155 @@ const makeSessionKernel = (operations) => {
580
588
  if (cause.reasons.length > 0 && cause.reasons.every((reason) => Cause.isFailReason(reason) && isSqlStepUpRejectAbsence(reason.error))) return Effect.succeed(rejected);
581
589
  return Effect.failCause(Cause.map(cause, (error) => isSqlStepUpRejectAbsence(error) ? unavailable() : error));
582
590
  }));
583
- /** Shared implementation; public driver entrypoints restore concrete tables/IDs. */
584
- const makeSqlSessionStepUp = (database, mapping, options) => Effect.map(LifecycleHooks, (hooks) => {
585
- const m = mapping.intent, c = stepUpColumns(mapping);
586
- const read = (digest) => mapFailureCause(safeTransaction(inTransaction(database, readSqlStepUp(mapping, digest, options.locking))), (error) => error._tag === "StaleAuthentication" ? SessionStepUpInvalid.make({}) : error);
587
- const service = {
588
- create: (input, _now, prepare) => owned(database, options, mapping.isConstraintConflict, Effect.gen(function* () {
589
- const tx = yield* CurrentSessionSql;
590
- const journal = yield* CurrentCommitJournal;
591
- const authority = yield* stepUpAuthority(mapping, input.revision, options.locking);
592
- const version = yield* allocate(options.mode, m.allocateVersion, m.allocateVersionSync);
593
- const record = {
594
- ...input,
595
- version
596
- };
597
- yield* stepUpSource(mapping, record, authority.nativeSubjectId, yield* freshNow, options.locking);
598
- if ((yield* selectRows(tx.select().from(m.table).where(or(eq(c.digest, input.digest), eq(c.flowId, input.flowId))).limit(1), options.locking)).length > 0) return yield* SessionConflict.make({});
599
- const snapshot = yield* encodeStepUpIntent(record);
600
- const values = {
601
- ...m.encodeInsert(record, { subjectId: authority.nativeSubjectId }),
602
- ...updateValues([
603
- [m.digest, record.digest],
604
- [m.version, version],
605
- [m.flowId, record.flowId],
606
- [m.subjectId, authority.nativeSubjectId],
607
- [m.bindingDigest, record.bindingDigest],
608
- [m.snapshot, snapshot],
609
- [m.expiresAt, m.encodeInstant(record.expiresAt)],
610
- [m.attemptLimit, record.attemptLimit],
611
- [m.failedAttempts, 0],
612
- [m.consumed, false]
613
- ])
614
- };
615
- const receipt = prepare(record, journal);
616
- yield* tx.insert(m.table).values(values).pipe(normalizeMutation(mapping));
617
- if ((yield* tx.select().from(m.table).where(and(eq(c.digest, record.digest), eq(c.version, version), eq(c.flowId, record.flowId), eq(c.subjectId, authority.nativeSubjectId), eq(c.bindingDigest, record.bindingDigest), eq(c.snapshot, snapshot), eq(c.expiresAt, m.encodeInstant(record.expiresAt)), eq(c.attemptLimit, record.attemptLimit), eq(c.failedAttempts, 0), eq(c.consumed, false))).limit(1))[0] === void 0) return yield* unavailable();
618
- return receipt;
619
- })).pipe((effect) => mapFailureCause(effect, (error) => error._tag === "SessionStepUpInvalid" ? stale() : error)),
620
- context: (input) => mapFailureCause(read(input.digest).pipe(Effect.flatMap(({ intent }) => snapshotPendingAuthenticationContext({
621
- flowId: intent.flowId,
622
- bindingDigest: intent.bindingDigest,
623
- revision: intent.revision,
624
- expiresAtMillis: DateTime.toEpochMillis(intent.expiresAt)
625
- }))), (error) => error._tag === "PendingAuthenticationInvalid" ? unavailable() : error),
626
- read: (input) => read(input.digest).pipe(Effect.flatMap((value) => value.intent.bindingDigest === input.bindingDigest ? Effect.succeed(value.intent) : Effect.fail(SessionStepUpInvalid.make({})))),
627
- reject: (input, prepare) => owned(database, options, mapping.isConstraintConflict, Effect.gen(function* () {
628
- const tx = yield* CurrentSessionSql;
629
- const journal = yield* CurrentCommitJournal;
630
- const selected = yield* recoverSqlStepUpRejectAbsence(readSqlStepUp(mapping, input.digest, options.locking).pipe(Effect.map((value) => ({
631
- _tag: "Found",
632
- value
633
- }))), { _tag: "Rejected" });
634
- if (selected._tag === "Rejected") return prepare({ _tag: "Rejected" }, journal);
635
- const { row, intent } = selected.value;
636
- const receipt = prepare({ _tag: "Rejected" }, journal);
637
- yield* tx.update(m.table).set(updateValues([[m.failedAttempts, sql`${c.failedAttempts} + 1`]])).where(and(eq(c.digest, input.digest), eq(c.snapshot, row[m.snapshot]), eq(c.version, intent.version), eq(c.consumed, false), sql`${c.failedAttempts} < ${c.attemptLimit}`));
638
- const written = yield* stepUpLocator(mapping, input.digest, false);
639
- if (written === void 0 || written[m.version] !== intent.version || written[m.snapshot] !== row[m.snapshot] || written[m.failedAttempts] !== row[m.failedAttempts] + 1) return yield* unavailable();
640
- return receipt;
641
- })),
642
- complete: (plan, prepare) => owned(database, options, mapping.isConstraintConflict, Effect.gen(function* () {
643
- const tx = yield* CurrentSessionSql;
644
- const journal = yield* CurrentCommitJournal;
645
- yield* validateStepUpPlan(plan);
646
- const current = yield* stepUpAuthority(mapping, plan.evidence.revision, options.locking);
647
- const stored = yield* readSqlStepUp(mapping, plan.intent.digest, options.locking);
648
- if ((yield* encodeStepUpIntent(stored.intent)) !== (yield* encodeStepUpIntent(plan.intent))) return yield* SessionStepUpInvalid.make({});
649
- const base = yield* assessAuthentication(plan.evidence, current.requirement).pipe(Effect.mapError(() => stale()));
650
- const profile = yield* assessAuthentication(plan.evidence, stored.intent.requirement).pipe(Effect.mapError(() => stale()));
651
- if (!base.satisfied || !profile.satisfied) return yield* stale();
652
- const replacement = plan.replacement, now = yield* freshNow;
653
- if (DateTime.toEpochMillis(replacement.inspection.session.expiresAt) > DateTime.toEpochMillis(stored.intent.sourceAbsoluteExpiresAt) || DateTime.toEpochMillis(now) < DateTime.toEpochMillis(replacement.inspection.session.issuedAt) || DateTime.toEpochMillis(now) >= Math.min(DateTime.toEpochMillis(replacement.inspection.session.expiresAt), DateTime.toEpochMillis(stored.intent.expiresAt))) return yield* SessionStepUpInvalid.make({});
654
- let rotation;
655
- if (replacement._tag === "Stateful") {
656
- if (mapping.source.kind !== "Stateful") return yield* SessionStepUpInvalid.make({});
657
- const s = mapping.source.session, row = stored.source.row;
658
- if (row[s.digest] !== replacement.expectedDigest || row[s.version] !== replacement.expectedRowVersion) return yield* SessionConflict.make({});
659
- const version = yield* allocate(options.mode, s.allocateVersion, s.allocateVersionSync);
660
- const next = {
661
- ...replacement.inspection.session,
662
- provenance: replacement.inspection.provenance,
663
- credentialVersion: replacement.inspection.credentialVersion,
664
- digest: replacement.nextDigest,
591
+ return {
592
+ makeSqlAuthenticationAuthority,
593
+ makeSqlPendingAuthentication,
594
+ makeSqlStatefulSessions,
595
+ makeSqlSignedValidity,
596
+ makeSqlSessionStepUp: Effect.fnUntraced(function* (mapping, options) {
597
+ const database = yield* CurrentSessionSql;
598
+ const hooks = yield* LifecycleHooks;
599
+ const m = mapping.intent, c = stepUpColumns(mapping);
600
+ const read = (digest) => mapFailureCause(safeTransaction(inTransaction(database, readSqlStepUp(mapping, digest, options.locking))), (error) => error._tag === "StaleAuthentication" ? SessionStepUpInvalid.make({}) : error);
601
+ const service = {
602
+ create: (input, _now, prepare) => owned(database, options, mapping.isConstraintConflict, Effect.gen(function* () {
603
+ const tx = yield* CurrentSessionSql;
604
+ const journal = yield* CurrentCommitJournal;
605
+ const authority = yield* stepUpAuthority(mapping, input.revision, options.locking);
606
+ const version = yield* allocate(options.mode, m.allocateVersion, m.allocateVersionSync);
607
+ const record = {
608
+ ...input,
665
609
  version
666
610
  };
667
- rotation = {
668
- ...s.encodeRotation(next),
611
+ yield* stepUpSource(mapping, record, authority.nativeSubjectId, yield* freshNow, options.locking);
612
+ if ((yield* selectRows(tx.select().from(m.table).where(or(eq(c.digest, input.digest), eq(c.flowId, input.flowId))).limit(1), options.locking)).length > 0) return yield* SessionConflict.make({});
613
+ const snapshot = yield* encodeStepUpIntent(record);
614
+ const values = {
615
+ ...m.encodeInsert(record, { subjectId: authority.nativeSubjectId }),
669
616
  ...updateValues([
670
- [s.version, version],
671
- [s.digest, next.digest],
672
- [s.credentialVersion, next.credentialVersion],
673
- [s.authenticatedAt, s.encodeInstant(next.assurance.authenticatedAt)],
674
- [s.issuedAt, s.encodeInstant(next.issuedAt)],
675
- [s.expiresAt, s.encodeInstant(next.expiresAt)],
676
- [s.absoluteExpiresAt, s.encodeInstant(next.absoluteExpiresAt)]
617
+ [m.digest, record.digest],
618
+ [m.version, version],
619
+ [m.flowId, record.flowId],
620
+ [m.subjectId, authority.nativeSubjectId],
621
+ [m.bindingDigest, record.bindingDigest],
622
+ [m.snapshot, snapshot],
623
+ [m.expiresAt, m.encodeInstant(record.expiresAt)],
624
+ [m.attemptLimit, record.attemptLimit],
625
+ [m.failedAttempts, 0],
626
+ [m.consumed, false]
677
627
  ])
678
628
  };
679
- }
680
- const completionVersion = yield* allocate(options.mode, m.allocateVersion, m.allocateVersionSync);
681
- if (completionVersion === stored.intent.version) return yield* unavailable();
682
- const completionSnapshot = yield* encodeStepUpIntent({
683
- ...stored.intent,
684
- version: completionVersion
685
- });
686
- const receipt = prepare(replacement.inspection, journal);
687
- if (replacement._tag === "Stateful" && mapping.source.kind === "Stateful") {
688
- const s = mapping.source.session, sc = sessionColumns(mapping.source);
689
- yield* tx.update(s.table).set(rotation).where(and(eq(sc.sessionId, stored.source.nativeSessionId), eq(sc.digest, replacement.expectedDigest), eq(sc.version, replacement.expectedRowVersion)));
690
- const written = (yield* tx.select().from(s.table).where(eq(sc.sessionId, stored.source.nativeSessionId)).limit(1))[0];
691
- if (written === void 0 || written[s.version] !== rotation[s.version]) return yield* unavailable();
692
- const decoded = yield* s.decode(written);
693
- if (decoded.version !== rotation[s.version] || !stepUpRotationMatches(plan, decoded)) return yield* unavailable();
694
- } else if (replacement._tag === "StateAssistedSigned" && mapping.source.kind === "StateAssistedSigned") {
695
- const t = mapping.source.tombstone;
696
- const owner = column(t.table, t.subjectId), id = column(t.table, t.sessionId), expiry = column(t.table, t.absoluteExpiresAt);
697
- yield* tx.delete(t.table).where(and(eq(owner, stored.nativeSubjectId), eq(id, stored.source.nativeSessionId), lte(expiry, t.encodeInstant(yield* freshNow))));
698
- const values = t.encodeInsert({
699
- subjectId: stored.nativeSubjectId,
700
- sessionId: stored.source.nativeSessionId,
701
- absoluteExpiresAt: replacement.tombstoneUntil
629
+ const receipt = prepare(record, journal);
630
+ yield* tx.insert(m.table).values(values).pipe(normalizeMutation(mapping));
631
+ if ((yield* tx.select().from(m.table).where(and(eq(c.digest, record.digest), eq(c.version, version), eq(c.flowId, record.flowId), eq(c.subjectId, authority.nativeSubjectId), eq(c.bindingDigest, record.bindingDigest), eq(c.snapshot, snapshot), eq(c.expiresAt, m.encodeInstant(record.expiresAt)), eq(c.attemptLimit, record.attemptLimit), eq(c.failedAttempts, 0), eq(c.consumed, false))).limit(1))[0] === void 0) return yield* unavailable();
632
+ return receipt;
633
+ })).pipe((effect) => mapFailureCause(effect, (error) => error._tag === "SessionStepUpInvalid" ? stale() : error)),
634
+ context: (input) => mapFailureCause(read(input.digest).pipe(Effect.flatMap(({ intent }) => snapshotPendingAuthenticationContext({
635
+ flowId: intent.flowId,
636
+ bindingDigest: intent.bindingDigest,
637
+ revision: intent.revision,
638
+ expiresAtMillis: DateTime.toEpochMillis(intent.expiresAt)
639
+ }))), (error) => error._tag === "PendingAuthenticationInvalid" ? unavailable() : error),
640
+ read: (input) => read(input.digest).pipe(Effect.flatMap((value) => value.intent.bindingDigest === input.bindingDigest ? Effect.succeed(value.intent) : Effect.fail(SessionStepUpInvalid.make({})))),
641
+ reject: (input, prepare) => owned(database, options, mapping.isConstraintConflict, Effect.gen(function* () {
642
+ const tx = yield* CurrentSessionSql;
643
+ const journal = yield* CurrentCommitJournal;
644
+ const selected = yield* recoverSqlStepUpRejectAbsence(readSqlStepUp(mapping, input.digest, options.locking).pipe(Effect.map((value) => ({
645
+ _tag: "Found",
646
+ value
647
+ }))), { _tag: "Rejected" });
648
+ if (selected._tag === "Rejected") return prepare({ _tag: "Rejected" }, journal);
649
+ const { row, intent } = selected.value;
650
+ const receipt = prepare({ _tag: "Rejected" }, journal);
651
+ yield* tx.update(m.table).set(updateValues([[m.failedAttempts, sql`${c.failedAttempts} + 1`]])).where(and(eq(c.digest, input.digest), eq(c.snapshot, row[m.snapshot]), eq(c.version, intent.version), eq(c.consumed, false), sql`${c.failedAttempts} < ${c.attemptLimit}`));
652
+ const written = yield* stepUpLocator(mapping, input.digest, false);
653
+ if (written === void 0 || written[m.version] !== intent.version || written[m.snapshot] !== row[m.snapshot] || written[m.failedAttempts] !== row[m.failedAttempts] + 1) return yield* unavailable();
654
+ return receipt;
655
+ })),
656
+ complete: (plan, prepare) => owned(database, options, mapping.isConstraintConflict, Effect.gen(function* () {
657
+ const tx = yield* CurrentSessionSql;
658
+ const journal = yield* CurrentCommitJournal;
659
+ yield* validateStepUpPlan(plan);
660
+ const current = yield* stepUpAuthority(mapping, plan.evidence.revision, options.locking);
661
+ const stored = yield* readSqlStepUp(mapping, plan.intent.digest, options.locking);
662
+ if ((yield* encodeStepUpIntent(stored.intent)) !== (yield* encodeStepUpIntent(plan.intent))) return yield* SessionStepUpInvalid.make({});
663
+ const base = yield* assessAuthentication(plan.evidence, current.requirement).pipe(Effect.mapError(() => stale()));
664
+ const profile = yield* assessAuthentication(plan.evidence, stored.intent.requirement).pipe(Effect.mapError(() => stale()));
665
+ if (!base.satisfied || !profile.satisfied) return yield* stale();
666
+ const replacement = plan.replacement, now = yield* freshNow;
667
+ if (DateTime.toEpochMillis(replacement.inspection.session.expiresAt) > DateTime.toEpochMillis(stored.intent.sourceAbsoluteExpiresAt) || DateTime.toEpochMillis(now) < DateTime.toEpochMillis(replacement.inspection.session.issuedAt) || DateTime.toEpochMillis(now) >= Math.min(DateTime.toEpochMillis(replacement.inspection.session.expiresAt), DateTime.toEpochMillis(stored.intent.expiresAt))) return yield* SessionStepUpInvalid.make({});
668
+ let rotation;
669
+ if (replacement._tag === "Stateful") {
670
+ if (mapping.source.kind !== "Stateful") return yield* SessionStepUpInvalid.make({});
671
+ const s = mapping.source.session, row = stored.source.row;
672
+ if (row[s.digest] !== replacement.expectedDigest || row[s.version] !== replacement.expectedRowVersion) return yield* SessionConflict.make({});
673
+ const version = yield* allocate(options.mode, s.allocateVersion, s.allocateVersionSync);
674
+ const next = {
675
+ ...replacement.inspection.session,
676
+ provenance: replacement.inspection.provenance,
677
+ credentialVersion: replacement.inspection.credentialVersion,
678
+ digest: replacement.nextDigest,
679
+ version
680
+ };
681
+ rotation = {
682
+ ...s.encodeRotation(next),
683
+ ...updateValues([
684
+ [s.version, version],
685
+ [s.digest, next.digest],
686
+ [s.credentialVersion, next.credentialVersion],
687
+ [s.authenticatedAt, s.encodeInstant(next.assurance.authenticatedAt)],
688
+ [s.issuedAt, s.encodeInstant(next.issuedAt)],
689
+ [s.expiresAt, s.encodeInstant(next.expiresAt)],
690
+ [s.absoluteExpiresAt, s.encodeInstant(next.absoluteExpiresAt)]
691
+ ])
692
+ };
693
+ }
694
+ const completionVersion = yield* allocate(options.mode, m.allocateVersion, m.allocateVersionSync);
695
+ if (completionVersion === stored.intent.version) return yield* unavailable();
696
+ const completionSnapshot = yield* encodeStepUpIntent({
697
+ ...stored.intent,
698
+ version: completionVersion
702
699
  });
703
- yield* tx.insert(t.table).values(values).pipe(normalizeMutation(mapping));
704
- if ((yield* tx.select().from(t.table).where(and(eq(owner, stored.nativeSubjectId), eq(id, stored.source.nativeSessionId), eq(expiry, t.encodeInstant(replacement.tombstoneUntil)))).limit(1))[0] === void 0) return yield* unavailable();
705
- }
706
- yield* tx.update(m.table).set(updateValues([
707
- [m.consumed, true],
708
- [m.version, completionVersion],
709
- [m.snapshot, completionSnapshot]
710
- ])).where(and(eq(c.digest, stored.intent.digest), eq(c.version, stored.intent.version), eq(c.snapshot, stored.row[m.snapshot]), eq(c.consumed, false), eq(c.failedAttempts, stored.row[m.failedAttempts])));
711
- const completed = yield* stepUpLocator(mapping, stored.intent.digest, false);
712
- if (completed === void 0 || !completed[m.consumed] || completed[m.version] !== completionVersion || completed[m.snapshot] !== completionSnapshot) return yield* unavailable();
713
- const commitNow = yield* freshNow;
714
- if (!stepUpIntentLive(stored.intent, mapping.source.kind, commitNow) || DateTime.toEpochMillis(commitNow) >= DateTime.toEpochMillis(replacement.inspection.session.expiresAt) || !(yield* assessAuthentication(plan.evidence, current.requirement).pipe(Effect.mapError(() => stale()))).satisfied || !(yield* assessAuthentication(plan.evidence, stored.intent.requirement).pipe(Effect.mapError(() => stale()))).satisfied) return yield* stale();
715
- return receipt;
716
- }))
717
- };
718
- return {
719
- create: (input, now, prepare) => service.create(input, now, prepare).pipe(Effect.provideService(LifecycleHooks, hooks)),
720
- context: (input) => service.context(input).pipe(Effect.provideService(LifecycleHooks, hooks)),
721
- read: (input) => service.read(input).pipe(Effect.provideService(LifecycleHooks, hooks)),
722
- reject: (input, prepare) => service.reject(input, prepare).pipe(Effect.provideService(LifecycleHooks, hooks)),
723
- complete: (plan, prepare) => service.complete(plan, prepare).pipe(Effect.provideService(LifecycleHooks, hooks))
724
- };
725
- });
726
- return {
727
- makeSqlAuthenticationAuthority,
728
- makeSqlPendingAuthentication,
729
- makeSqlStatefulSessions,
730
- makeSqlSignedValidity,
731
- makeSqlSessionStepUp
700
+ const receipt = prepare(replacement.inspection, journal);
701
+ if (replacement._tag === "Stateful" && mapping.source.kind === "Stateful") {
702
+ const s = mapping.source.session, sc = sessionColumns(mapping.source);
703
+ yield* tx.update(s.table).set(rotation).where(and(eq(sc.sessionId, stored.source.nativeSessionId), eq(sc.digest, replacement.expectedDigest), eq(sc.version, replacement.expectedRowVersion)));
704
+ const written = (yield* tx.select().from(s.table).where(eq(sc.sessionId, stored.source.nativeSessionId)).limit(1))[0];
705
+ if (written === void 0 || written[s.version] !== rotation[s.version]) return yield* unavailable();
706
+ const decoded = yield* s.decode(written);
707
+ if (decoded.version !== rotation[s.version] || !stepUpRotationMatches(plan, decoded)) return yield* unavailable();
708
+ } else if (replacement._tag === "StateAssistedSigned" && mapping.source.kind === "StateAssistedSigned") {
709
+ const t = mapping.source.tombstone;
710
+ const owner = column(t.table, t.subjectId), id = column(t.table, t.sessionId), expiry = column(t.table, t.absoluteExpiresAt);
711
+ yield* tx.delete(t.table).where(and(eq(owner, stored.nativeSubjectId), eq(id, stored.source.nativeSessionId), lte(expiry, t.encodeInstant(yield* freshNow))));
712
+ const values = t.encodeInsert({
713
+ subjectId: stored.nativeSubjectId,
714
+ sessionId: stored.source.nativeSessionId,
715
+ absoluteExpiresAt: replacement.tombstoneUntil
716
+ });
717
+ yield* tx.insert(t.table).values(values).pipe(normalizeMutation(mapping));
718
+ if ((yield* tx.select().from(t.table).where(and(eq(owner, stored.nativeSubjectId), eq(id, stored.source.nativeSessionId), eq(expiry, t.encodeInstant(replacement.tombstoneUntil)))).limit(1))[0] === void 0) return yield* unavailable();
719
+ }
720
+ yield* tx.update(m.table).set(updateValues([
721
+ [m.consumed, true],
722
+ [m.version, completionVersion],
723
+ [m.snapshot, completionSnapshot]
724
+ ])).where(and(eq(c.digest, stored.intent.digest), eq(c.version, stored.intent.version), eq(c.snapshot, stored.row[m.snapshot]), eq(c.consumed, false), eq(c.failedAttempts, stored.row[m.failedAttempts])));
725
+ const completed = yield* stepUpLocator(mapping, stored.intent.digest, false);
726
+ if (completed === void 0 || !completed[m.consumed] || completed[m.version] !== completionVersion || completed[m.snapshot] !== completionSnapshot) return yield* unavailable();
727
+ const commitNow = yield* freshNow;
728
+ if (!stepUpIntentLive(stored.intent, mapping.source.kind, commitNow) || DateTime.toEpochMillis(commitNow) >= DateTime.toEpochMillis(replacement.inspection.session.expiresAt) || !(yield* assessAuthentication(plan.evidence, current.requirement).pipe(Effect.mapError(() => stale()))).satisfied || !(yield* assessAuthentication(plan.evidence, stored.intent.requirement).pipe(Effect.mapError(() => stale()))).satisfied) return yield* stale();
729
+ return receipt;
730
+ }))
731
+ };
732
+ return {
733
+ create: (input, now, prepare) => service.create(input, now, prepare).pipe(Effect.provideService(LifecycleHooks, hooks)),
734
+ context: (input) => service.context(input).pipe(Effect.provideService(LifecycleHooks, hooks)),
735
+ read: (input) => service.read(input).pipe(Effect.provideService(LifecycleHooks, hooks)),
736
+ reject: (input, prepare) => service.reject(input, prepare).pipe(Effect.provideService(LifecycleHooks, hooks)),
737
+ complete: (plan, prepare) => service.complete(plan, prepare).pipe(Effect.provideService(LifecycleHooks, hooks))
738
+ };
739
+ })
732
740
  };
733
741
  };
734
742
  //#endregion