@yielded/auth-persistence 0.1.0-beta.11 → 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 (66) hide show
  1. package/README.md +6 -1
  2. package/dist/Adapter.d.mts +2 -2
  3. package/dist/Adapter.mjs +4 -4
  4. package/dist/internal/PasswordPreparedPostconditions.d.mts +1 -1
  5. package/dist/internal/configuration.d.mts +1 -1
  6. package/dist/internal/email-kernel.d.mts +5 -5
  7. package/dist/internal/email-kernel.mjs +8 -6
  8. package/dist/internal/email-kernel.mjs.map +1 -1
  9. package/dist/internal/models/email-model.d.mts +1 -1
  10. package/dist/internal/oauth-server.mjs +1 -1
  11. package/dist/internal/oauth-server.mjs.map +1 -1
  12. package/dist/internal/passkey/target.mjs +21 -12
  13. package/dist/internal/passkey/target.mjs.map +1 -1
  14. package/dist/internal/passkey/write-target.mjs +6 -6
  15. package/dist/internal/passkey/write-target.mjs.map +1 -1
  16. package/dist/internal/passkey-kernel.d.mts +9 -12
  17. package/dist/internal/passkeys.mjs +8 -7
  18. package/dist/internal/passkeys.mjs.map +1 -1
  19. package/dist/internal/password-kernel.d.mts +3 -3
  20. package/dist/internal/password-kernel.mjs +3 -2
  21. package/dist/internal/password-kernel.mjs.map +1 -1
  22. package/dist/internal/persistence.d.mts +3 -2
  23. package/dist/internal/persistence.mjs +22 -26
  24. package/dist/internal/persistence.mjs.map +1 -1
  25. package/dist/internal/proof-kernel.d.mts +2 -2
  26. package/dist/internal/proof-kernel.mjs +2 -1
  27. package/dist/internal/proof-kernel.mjs.map +1 -1
  28. package/dist/internal/registration.mjs +6 -1
  29. package/dist/internal/registration.mjs.map +1 -1
  30. package/dist/internal/session-kernel.d.mts +19 -9
  31. package/dist/internal/session-kernel.mjs +155 -147
  32. package/dist/internal/session-kernel.mjs.map +1 -1
  33. package/dist/internal/sql-persistence.mjs +2 -3
  34. package/dist/internal/sql-persistence.mjs.map +1 -1
  35. package/dist/internal/sql-query.mjs +4 -2
  36. package/dist/internal/sql-query.mjs.map +1 -1
  37. package/dist/internal/standalone.d.mts +2 -4
  38. package/dist/internal/standalone.mjs +2 -2
  39. package/dist/internal/standalone.mjs.map +1 -1
  40. package/dist/internal/storage-mapping.mjs +2 -2
  41. package/dist/internal/storage-validation.mjs +4 -2
  42. package/dist/internal/storage-validation.mjs.map +1 -1
  43. package/dist/internal/transaction-execution-kernel.d.mts +6 -7
  44. package/dist/internal/transaction-execution-kernel.mjs +9 -7
  45. package/dist/internal/transaction-execution-kernel.mjs.map +1 -1
  46. package/dist/internal/transaction-kernel.d.mts +7 -3
  47. package/dist/internal/transaction-kernel.mjs +5 -2
  48. package/dist/internal/transaction-kernel.mjs.map +1 -1
  49. package/package.json +1 -1
  50. package/src/Adapter.ts +6 -1
  51. package/src/internal/email-kernel.ts +30 -35
  52. package/src/internal/oauth-server.ts +1 -1
  53. package/src/internal/passkey/target.ts +36 -43
  54. package/src/internal/passkey/write-target.ts +4 -14
  55. package/src/internal/passkeys.ts +4 -9
  56. package/src/internal/password-kernel.ts +2 -2
  57. package/src/internal/persistence.ts +77 -68
  58. package/src/internal/proof-kernel.ts +2 -2
  59. package/src/internal/registration.ts +22 -7
  60. package/src/internal/session-kernel.ts +1122 -1139
  61. package/src/internal/sql-persistence.ts +1 -1
  62. package/src/internal/sql-query.ts +5 -3
  63. package/src/internal/standalone.ts +5 -7
  64. package/src/internal/storage-validation.ts +20 -16
  65. package/src/internal/transaction-execution-kernel.ts +17 -15
  66. package/src/internal/transaction-kernel.ts +7 -1
@@ -625,62 +625,44 @@ export const makeSessionKernel = (operations: QueryOperations) => {
625
625
  });
626
626
  };
627
627
 
628
- const makeSqlAuthenticationAuthority = <Claims>(
629
- database: Database,
628
+ const makeSqlAuthenticationAuthority = Effect.fnUntraced(function* <Claims>(
630
629
  mapping: AuthenticationAuthorityMapping<Claims, any, any, any, any, any>,
631
630
  options: SessionSqlOptions,
632
- ): Effect.Effect<AuthenticationAuthority["Service"], never, LifecycleHooks> =>
633
- Effect.map(LifecycleHooks, (hooks) => {
634
- const service = {
635
- capture: (subjectId: SubjectId, credentialIds: ReadonlyArray<string>) =>
636
- safeTransaction(
637
- inTransaction(database, captureIn(mapping, subjectId, credentialIds, true)),
638
- ),
639
- requirements: (evidence: AuthenticationEvidence) =>
640
- safeTransaction(
641
- inTransaction(
642
- database,
643
- validateEvidenceIn(mapping, evidence, true).pipe(
644
- Effect.map((result) => result.requirement),
645
- ),
631
+ ): Effect.fn.Return<
632
+ AuthenticationAuthority["Service"],
633
+ never,
634
+ LifecycleHooks | CurrentSessionSql
635
+ > {
636
+ const database = yield* CurrentSessionSql;
637
+ const hooks = yield* LifecycleHooks;
638
+
639
+ const service = {
640
+ capture: (subjectId: SubjectId, credentialIds: ReadonlyArray<string>) =>
641
+ safeTransaction(
642
+ inTransaction(database, captureIn(mapping, subjectId, credentialIds, true)),
643
+ ),
644
+ requirements: (evidence: AuthenticationEvidence) =>
645
+ safeTransaction(
646
+ inTransaction(
647
+ database,
648
+ validateEvidenceIn(mapping, evidence, true).pipe(
649
+ Effect.map((result) => result.requirement),
646
650
  ),
647
651
  ),
648
- approve: <A>(
649
- input: Parameters<AuthenticationAuthority["Service"]["approve"]>[0],
650
- prepare: PrepareSessionCommit<void, A>,
651
- ) =>
652
- owned(
653
- database,
654
- options,
655
- mapping.isConstraintConflict,
656
- Effect.gen(function* () {
657
- const journal = yield* CurrentCommitJournal;
658
- const { requirement } = yield* validateEvidenceIn(mapping, input.evidence, true);
659
-
660
- if (input.pending === undefined) {
661
- const assessed = yield* assessAuthentication(input.evidence, requirement).pipe(
662
- Effect.mapError(unavailable),
663
- );
664
-
665
- const now = yield* freshNow;
666
-
667
- if (
668
- !assessed.satisfied ||
669
- DateTime.toEpochMillis(now) >= DateTime.toEpochMillis(input.expiresAt) ||
670
- DateTime.toEpochMillis(input.expiresAt) >
671
- DateTime.toEpochMillis(input.absoluteExpiresAt)
672
- )
673
- return yield* stale();
674
-
675
- return prepare(undefined, journal);
676
- }
677
- if (mapping.pending === undefined) return yield* invalidPending();
678
- const pendingMapping = { ...mapping, ...mapping.pending };
679
- const pending = yield* validatePending(pendingMapping, input.pending);
680
-
681
- if (!preservesRevision(input.evidence, pending.evidence))
682
- return yield* invalidPending();
683
-
652
+ ),
653
+ approve: <A>(
654
+ input: Parameters<AuthenticationAuthority["Service"]["approve"]>[0],
655
+ prepare: PrepareSessionCommit<void, A>,
656
+ ) =>
657
+ owned(
658
+ database,
659
+ options,
660
+ mapping.isConstraintConflict,
661
+ Effect.gen(function* () {
662
+ const journal = yield* CurrentCommitJournal;
663
+ const { requirement } = yield* validateEvidenceIn(mapping, input.evidence, true);
664
+
665
+ if (input.pending === undefined) {
684
666
  const assessed = yield* assessAuthentication(input.evidence, requirement).pipe(
685
667
  Effect.mapError(unavailable),
686
668
  );
@@ -689,32 +671,55 @@ export const makeSessionKernel = (operations: QueryOperations) => {
689
671
 
690
672
  if (
691
673
  !assessed.satisfied ||
692
- DateTime.toEpochMillis(now) >= DateTime.toEpochMillis(pending.expiresAt) ||
693
674
  DateTime.toEpochMillis(now) >= DateTime.toEpochMillis(input.expiresAt) ||
694
675
  DateTime.toEpochMillis(input.expiresAt) >
695
676
  DateTime.toEpochMillis(input.absoluteExpiresAt)
696
677
  )
697
678
  return yield* stale();
698
- const receipt = prepare(undefined, journal);
699
679
 
700
- yield* consumePending(pendingMapping, input.pending, input.absoluteExpiresAt);
680
+ return prepare(undefined, journal);
681
+ }
682
+ if (mapping.pending === undefined) return yield* invalidPending();
683
+ const pendingMapping = { ...mapping, ...mapping.pending };
684
+ const pending = yield* validatePending(pendingMapping, input.pending);
701
685
 
702
- return receipt;
703
- }),
704
- ),
705
- };
686
+ if (!preservesRevision(input.evidence, pending.evidence))
687
+ return yield* invalidPending();
706
688
 
707
- return {
708
- capture: (subjectId, credentialIds) =>
709
- service
710
- .capture(subjectId, credentialIds)
711
- .pipe(Effect.provideService(LifecycleHooks, hooks)),
712
- requirements: (evidence) =>
713
- service.requirements(evidence).pipe(Effect.provideService(LifecycleHooks, hooks)),
714
- approve: (input, prepare) =>
715
- service.approve(input, prepare).pipe(Effect.provideService(LifecycleHooks, hooks)),
716
- } satisfies AuthenticationAuthority["Service"];
717
- });
689
+ const assessed = yield* assessAuthentication(input.evidence, requirement).pipe(
690
+ Effect.mapError(unavailable),
691
+ );
692
+
693
+ const now = yield* freshNow;
694
+
695
+ if (
696
+ !assessed.satisfied ||
697
+ DateTime.toEpochMillis(now) >= DateTime.toEpochMillis(pending.expiresAt) ||
698
+ DateTime.toEpochMillis(now) >= DateTime.toEpochMillis(input.expiresAt) ||
699
+ DateTime.toEpochMillis(input.expiresAt) >
700
+ DateTime.toEpochMillis(input.absoluteExpiresAt)
701
+ )
702
+ return yield* stale();
703
+ const receipt = prepare(undefined, journal);
704
+
705
+ yield* consumePending(pendingMapping, input.pending, input.absoluteExpiresAt);
706
+
707
+ return receipt;
708
+ }),
709
+ ),
710
+ };
711
+
712
+ return {
713
+ capture: (subjectId, credentialIds) =>
714
+ service
715
+ .capture(subjectId, credentialIds)
716
+ .pipe(Effect.provideService(LifecycleHooks, hooks)),
717
+ requirements: (evidence) =>
718
+ service.requirements(evidence).pipe(Effect.provideService(LifecycleHooks, hooks)),
719
+ approve: (input, prepare) =>
720
+ service.approve(input, prepare).pipe(Effect.provideService(LifecycleHooks, hooks)),
721
+ } satisfies AuthenticationAuthority["Service"];
722
+ });
718
723
 
719
724
  /** The same locked security checks serve full consumption reads and the
720
725
  * claims-free additional-factor context. No lock spans proof verification. */
@@ -782,167 +787,163 @@ export const makeSessionKernel = (operations: QueryOperations) => {
782
787
  return record;
783
788
  });
784
789
 
785
- const makeSqlPendingAuthentication = <Claims>(
786
- database: Database,
790
+ const makeSqlPendingAuthentication = Effect.fnUntraced(function* <Claims>(
787
791
  mapping: PendingAuthenticationMapping<Claims, any, any, any, any, any>,
788
792
  options: SessionSqlOptions,
789
- ): Effect.Effect<PendingAuthentication<Claims>, never, LifecycleHooks> =>
790
- Effect.map(LifecycleHooks, (hooks) => {
791
- const service = {
792
- create: <A>(
793
- input: Omit<PendingAuthenticationRecord<Claims>, "version">,
794
- _inputNow: DateTime.Utc,
795
- prepare: PrepareSessionCommit<PendingAuthenticationRecord<Claims>, A>,
796
- ) =>
797
- owned(
798
- database,
799
- options,
800
- mapping.isConstraintConflict,
801
- Effect.gen(function* () {
802
- const transaction = yield* CurrentSessionSql;
803
- const journal = yield* CurrentCommitJournal;
804
-
805
- const { nativeSubjectId, requirement } = yield* validateEvidenceIn(
806
- mapping,
807
- input.evidence,
808
- true,
809
- );
810
-
811
- const existing = (yield* readFlow(mapping, input.evidence.flowId, true))[0];
812
-
813
- if (existing !== undefined) {
814
- const dedupUntil = yield* mapping.flow.decodeInstant(
815
- existing[mapping.flow.dedupUntil],
816
- );
817
-
818
- if (DateTime.toEpochMillis(yield* freshNow) < DateTime.toEpochMillis(dedupUntil))
819
- return yield* SessionConflict.make({});
820
- yield* transaction
821
- .delete(mapping.pending.table)
822
- .where(eq(pendingColumns(mapping).flowId, input.evidence.flowId));
823
- yield* transaction
824
- .delete(mapping.flow.table)
825
- .where(eq(flowColumns(mapping).flowId, input.evidence.flowId));
826
- }
827
-
828
- const version = yield* allocate(
829
- options.mode,
830
- mapping.pending.allocateVersion,
831
- mapping.pending.allocateVersionSync,
793
+ ): Effect.fn.Return<PendingAuthentication<Claims>, never, LifecycleHooks | CurrentSessionSql> {
794
+ const database = yield* CurrentSessionSql;
795
+ const hooks = yield* LifecycleHooks;
796
+
797
+ const service = {
798
+ create: <A>(
799
+ input: Omit<PendingAuthenticationRecord<Claims>, "version">,
800
+ _inputNow: DateTime.Utc,
801
+ prepare: PrepareSessionCommit<PendingAuthenticationRecord<Claims>, A>,
802
+ ) =>
803
+ owned(
804
+ database,
805
+ options,
806
+ mapping.isConstraintConflict,
807
+ Effect.gen(function* () {
808
+ const transaction = yield* CurrentSessionSql;
809
+ const journal = yield* CurrentCommitJournal;
810
+
811
+ const { nativeSubjectId, requirement } = yield* validateEvidenceIn(
812
+ mapping,
813
+ input.evidence,
814
+ true,
815
+ );
816
+
817
+ const existing = (yield* readFlow(mapping, input.evidence.flowId, true))[0];
818
+
819
+ if (existing !== undefined) {
820
+ const dedupUntil = yield* mapping.flow.decodeInstant(
821
+ existing[mapping.flow.dedupUntil],
832
822
  );
833
823
 
834
- yield* assessAuthentication(input.evidence, requirement);
835
- const now = yield* freshNow;
836
-
837
- if (DateTime.toEpochMillis(now) >= DateTime.toEpochMillis(input.expiresAt))
838
- return yield* stale();
839
- const record = { ...input, version };
840
- const receipt = prepare(record, journal);
841
-
842
- yield* transaction.insert(mapping.flow.table).values(
843
- mapping.flow.encodePendingInsert({
844
- evidence: input.evidence,
845
- subjectId: nativeSubjectId,
846
- pendingDigest: input.digest,
847
- dedupUntil: input.expiresAt,
848
- }),
849
- );
850
- yield* transaction.insert(mapping.pending.table).values(
851
- mapping.pending.encodeInsert(record, {
852
- subjectId: nativeSubjectId,
853
- failedAttempts: 0,
854
- consumed: false,
855
- }),
856
- );
824
+ if (DateTime.toEpochMillis(yield* freshNow) < DateTime.toEpochMillis(dedupUntil))
825
+ return yield* SessionConflict.make({});
826
+ yield* transaction
827
+ .delete(mapping.pending.table)
828
+ .where(eq(pendingColumns(mapping).flowId, input.evidence.flowId));
829
+ yield* transaction
830
+ .delete(mapping.flow.table)
831
+ .where(eq(flowColumns(mapping).flowId, input.evidence.flowId));
832
+ }
833
+
834
+ const version = yield* allocate(
835
+ options.mode,
836
+ mapping.pending.allocateVersion,
837
+ mapping.pending.allocateVersionSync,
838
+ );
839
+
840
+ yield* assessAuthentication(input.evidence, requirement);
841
+ const now = yield* freshNow;
842
+
843
+ if (DateTime.toEpochMillis(now) >= DateTime.toEpochMillis(input.expiresAt))
844
+ return yield* stale();
845
+ const record = { ...input, version };
846
+ const receipt = prepare(record, journal);
847
+
848
+ yield* transaction.insert(mapping.flow.table).values(
849
+ mapping.flow.encodePendingInsert({
850
+ evidence: input.evidence,
851
+ subjectId: nativeSubjectId,
852
+ pendingDigest: input.digest,
853
+ dedupUntil: input.expiresAt,
854
+ }),
855
+ );
856
+ yield* transaction.insert(mapping.pending.table).values(
857
+ mapping.pending.encodeInsert(record, {
858
+ subjectId: nativeSubjectId,
859
+ failedAttempts: 0,
860
+ consumed: false,
861
+ }),
862
+ );
857
863
 
858
- return receipt;
859
- }).pipe(normalizeMutation(mapping)),
864
+ return receipt;
865
+ }).pipe(normalizeMutation(mapping)),
866
+ ),
867
+ context: (input: { readonly digest: TokenDigest; readonly now: DateTime.Utc }) =>
868
+ safeTransaction(
869
+ inTransaction(
870
+ database,
871
+ readAuthenticatedPending<PendingAuthenticationState>(mapping, input, "context"),
860
872
  ),
861
- context: (input: { readonly digest: TokenDigest; readonly now: DateTime.Utc }) =>
862
- safeTransaction(
863
- inTransaction(
864
- database,
865
- readAuthenticatedPending<PendingAuthenticationState>(mapping, input, "context"),
866
- ),
867
- ).pipe(Effect.flatMap(pendingAuthenticationContext)),
868
- read: (input: {
869
- readonly digest: TokenDigest;
870
- readonly bindingDigest: TokenDigest;
871
- readonly now: DateTime.Utc;
872
- }) =>
873
- safeTransaction(
874
- inTransaction(
875
- database,
876
- readAuthenticatedPending<PendingAuthenticationRecord<Claims>>(
877
- mapping,
878
- input,
879
- "record",
880
- ),
881
- ),
882
- ).pipe(Effect.map((record) => record as PendingAuthenticationRecord<Claims>)),
883
- reject: <A>(
884
- input: Parameters<PendingAuthentication<Claims>["reject"]>[0],
885
- prepare: PrepareSessionCommit<{ readonly _tag: "Rejected" }, A>,
886
- ) =>
887
- owned(
873
+ ).pipe(Effect.flatMap(pendingAuthenticationContext)),
874
+ read: (input: {
875
+ readonly digest: TokenDigest;
876
+ readonly bindingDigest: TokenDigest;
877
+ readonly now: DateTime.Utc;
878
+ }) =>
879
+ safeTransaction(
880
+ inTransaction(
888
881
  database,
889
- options,
890
- mapping.isConstraintConflict,
891
- Effect.gen(function* () {
892
- const transaction = yield* CurrentSessionSql;
893
- const journal = yield* CurrentCommitJournal;
894
- const initial = (yield* readPending(mapping, input.digest, false))[0];
895
-
896
- if (initial === undefined) return prepare({ _tag: "Rejected" }, journal);
897
- const record = yield* mapping.pending.decodeContext(initial);
898
-
899
- const nativeSubjectId = yield* mapping.subjectId.toNative(
900
- record.evidence.revision.subjectId,
901
- );
902
-
903
- const subject = (yield* readSubject(mapping, nativeSubjectId, true))[0];
904
- const current = (yield* readPending(mapping, input.digest, true))[0];
905
- const now = yield* freshNow;
906
- const receipt = prepare({ _tag: "Rejected" }, journal);
882
+ readAuthenticatedPending<PendingAuthenticationRecord<Claims>>(mapping, input, "record"),
883
+ ),
884
+ ).pipe(Effect.map((record) => record as PendingAuthenticationRecord<Claims>)),
885
+ reject: <A>(
886
+ input: Parameters<PendingAuthentication<Claims>["reject"]>[0],
887
+ prepare: PrepareSessionCommit<{ readonly _tag: "Rejected" }, A>,
888
+ ) =>
889
+ owned(
890
+ database,
891
+ options,
892
+ mapping.isConstraintConflict,
893
+ Effect.gen(function* () {
894
+ const transaction = yield* CurrentSessionSql;
895
+ const journal = yield* CurrentCommitJournal;
896
+ const initial = (yield* readPending(mapping, input.digest, false))[0];
897
+
898
+ if (initial === undefined) return prepare({ _tag: "Rejected" }, journal);
899
+ const record = yield* mapping.pending.decodeContext(initial);
900
+
901
+ const nativeSubjectId = yield* mapping.subjectId.toNative(
902
+ record.evidence.revision.subjectId,
903
+ );
904
+
905
+ const subject = (yield* readSubject(mapping, nativeSubjectId, true))[0];
906
+ const current = (yield* readPending(mapping, input.digest, true))[0];
907
+ const now = yield* freshNow;
908
+ const receipt = prepare({ _tag: "Rejected" }, journal);
909
+
910
+ if (
911
+ subject !== undefined &&
912
+ mapping.subject.isActiveStatus(subject[mapping.subject.status]) &&
913
+ subject[mapping.subject.securityRevision] ===
914
+ record.evidence.revision.securityRevision &&
915
+ current !== undefined &&
916
+ current[mapping.pending.consumed] === false &&
917
+ record.evidence.bindingDigest === input.bindingDigest &&
918
+ DateTime.toEpochMillis(now) < DateTime.toEpochMillis(record.expiresAt)
919
+ ) {
920
+ yield* transaction
921
+ .update(mapping.pending.table)
922
+ .set(
923
+ updateValues([
924
+ [
925
+ mapping.pending.failedAttempts,
926
+ sql`case when ${pendingColumns(mapping).failedAttempts} < ${pendingColumns(mapping).attemptLimit} then ${pendingColumns(mapping).failedAttempts} + 1 else ${pendingColumns(mapping).failedAttempts} end`,
927
+ ],
928
+ ]),
929
+ )
930
+ .where(eq(pendingColumns(mapping).digest, input.digest));
931
+ }
907
932
 
908
- if (
909
- subject !== undefined &&
910
- mapping.subject.isActiveStatus(subject[mapping.subject.status]) &&
911
- subject[mapping.subject.securityRevision] ===
912
- record.evidence.revision.securityRevision &&
913
- current !== undefined &&
914
- current[mapping.pending.consumed] === false &&
915
- record.evidence.bindingDigest === input.bindingDigest &&
916
- DateTime.toEpochMillis(now) < DateTime.toEpochMillis(record.expiresAt)
917
- ) {
918
- yield* transaction
919
- .update(mapping.pending.table)
920
- .set(
921
- updateValues([
922
- [
923
- mapping.pending.failedAttempts,
924
- sql`case when ${pendingColumns(mapping).failedAttempts} < ${pendingColumns(mapping).attemptLimit} then ${pendingColumns(mapping).failedAttempts} + 1 else ${pendingColumns(mapping).failedAttempts} end`,
925
- ],
926
- ]),
927
- )
928
- .where(eq(pendingColumns(mapping).digest, input.digest));
929
- }
933
+ return receipt;
934
+ }),
935
+ ),
936
+ };
930
937
 
931
- return receipt;
932
- }),
933
- ),
934
- };
935
-
936
- return {
937
- create: (input, now, prepare) =>
938
- service.create(input, now, prepare).pipe(Effect.provideService(LifecycleHooks, hooks)),
939
- context: (input) =>
940
- service.context(input).pipe(Effect.provideService(LifecycleHooks, hooks)),
941
- read: (input) => service.read(input).pipe(Effect.provideService(LifecycleHooks, hooks)),
942
- reject: (input, prepare) =>
943
- service.reject(input, prepare).pipe(Effect.provideService(LifecycleHooks, hooks)),
944
- } satisfies PendingAuthentication<Claims>;
945
- });
938
+ return {
939
+ create: (input, now, prepare) =>
940
+ service.create(input, now, prepare).pipe(Effect.provideService(LifecycleHooks, hooks)),
941
+ context: (input) => service.context(input).pipe(Effect.provideService(LifecycleHooks, hooks)),
942
+ read: (input) => service.read(input).pipe(Effect.provideService(LifecycleHooks, hooks)),
943
+ reject: (input, prepare) =>
944
+ service.reject(input, prepare).pipe(Effect.provideService(LifecycleHooks, hooks)),
945
+ } satisfies PendingAuthentication<Claims>;
946
+ });
946
947
 
947
948
  const sessionColumns = (mapping: any) => ({
948
949
  sessionId: column(mapping.session.table, mapping.session.sessionId),
@@ -955,637 +956,626 @@ export const makeSessionKernel = (operations: QueryOperations) => {
955
956
  absoluteExpiresAt: column(mapping.session.table, mapping.session.absoluteExpiresAt),
956
957
  });
957
958
 
958
- const makeSqlStatefulSessions = <Claims>(
959
- database: Database,
959
+ const makeSqlStatefulSessions = Effect.fnUntraced(function* <Claims>(
960
960
  mapping: StatefulSessionMapping<Claims, any, any, any, any, any, any, any>,
961
961
  options: SessionSqlOptions,
962
- ): Effect.Effect<
962
+ ): Effect.fn.Return<
963
963
  {
964
964
  readonly statefulSessionPersistence: StatefulSessionPersistence<Claims>;
965
965
  readonly sessionRepository: SessionRepository;
966
966
  },
967
967
  never,
968
- LifecycleHooks
969
- > =>
970
- Effect.map(LifecycleHooks, (hooks) => {
971
- const c = sessionColumns(mapping);
972
-
973
- const persistence = {
974
- establish: <A>(
975
- input: Parameters<StatefulSessionPersistence<Claims>["establish"]>[0],
976
- prepare: PrepareSessionCommit<StatefulSessionRecord<Claims>, A>,
977
- ) =>
978
- owned(
979
- database,
980
- options,
981
- mapping.isConstraintConflict,
982
- Effect.gen(function* () {
983
- const transaction = yield* CurrentSessionSql;
984
- const journal = yield* CurrentCommitJournal;
985
-
986
- const { nativeSubjectId, requirement } = yield* validateEvidenceIn(
968
+ LifecycleHooks | CurrentSessionSql
969
+ > {
970
+ const database = yield* CurrentSessionSql;
971
+ const hooks = yield* LifecycleHooks;
972
+
973
+ const c = sessionColumns(mapping);
974
+
975
+ const persistence = {
976
+ establish: <A>(
977
+ input: Parameters<StatefulSessionPersistence<Claims>["establish"]>[0],
978
+ prepare: PrepareSessionCommit<StatefulSessionRecord<Claims>, A>,
979
+ ) =>
980
+ owned(
981
+ database,
982
+ options,
983
+ mapping.isConstraintConflict,
984
+ Effect.gen(function* () {
985
+ const transaction = yield* CurrentSessionSql;
986
+ const journal = yield* CurrentCommitJournal;
987
+
988
+ const { nativeSubjectId, requirement } = yield* validateEvidenceIn(
989
+ mapping,
990
+ input.evidence,
991
+ true,
992
+ );
993
+
994
+ let flowInsert: unknown | undefined;
995
+ let pendingExpiresAt: DateTime.Utc | undefined;
996
+
997
+ if (input.pending === undefined)
998
+ flowInsert = yield* ensureDirectFlowAvailable(
987
999
  mapping,
988
1000
  input.evidence,
989
- true,
990
- );
991
-
992
- let flowInsert: unknown | undefined;
993
- let pendingExpiresAt: DateTime.Utc | undefined;
994
-
995
- if (input.pending === undefined)
996
- flowInsert = yield* ensureDirectFlowAvailable(
997
- mapping,
998
- input.evidence,
999
- nativeSubjectId,
1000
- input.session.absoluteExpiresAt,
1001
- );
1002
- else {
1003
- if (mapping.pending === undefined) return yield* invalidPending();
1004
- const pending = yield* validatePending(mapping, input.pending);
1005
-
1006
- if (!preservesRevision(input.evidence, pending.evidence))
1007
- return yield* invalidPending();
1008
- pendingExpiresAt = pending.expiresAt;
1009
- }
1010
-
1011
- const nativeSessionId = yield* allocate(
1012
- options.mode,
1013
- mapping.session.allocateId,
1014
- mapping.session.allocateIdSync,
1001
+ nativeSubjectId,
1002
+ input.session.absoluteExpiresAt,
1015
1003
  );
1004
+ else {
1005
+ if (mapping.pending === undefined) return yield* invalidPending();
1006
+ const pending = yield* validatePending(mapping, input.pending);
1016
1007
 
1017
- const sessionId = yield* mapping.sessionId.toSession(nativeSessionId);
1018
-
1019
- const version = yield* allocate(
1020
- options.mode,
1021
- mapping.session.allocateVersion,
1022
- mapping.session.allocateVersionSync,
1023
- );
1024
-
1025
- const assessed = yield* assessAuthentication(input.evidence, requirement);
1026
- const now = yield* freshNow;
1027
-
1028
- if (
1029
- !assessed.satisfied ||
1030
- (pendingExpiresAt !== undefined &&
1031
- DateTime.toEpochMillis(now) >= DateTime.toEpochMillis(pendingExpiresAt)) ||
1032
- DateTime.toEpochMillis(now) >= DateTime.toEpochMillis(input.session.expiresAt) ||
1033
- DateTime.toEpochMillis(input.session.expiresAt) >
1034
- DateTime.toEpochMillis(input.session.absoluteExpiresAt)
1035
- )
1036
- return yield* stale();
1037
-
1038
- const record = {
1039
- ...input.session,
1040
- sessionId,
1041
- version,
1042
- subjectId: input.evidence.revision.subjectId,
1043
- securityRevision: input.evidence.revision.securityRevision,
1044
- assurance: assessed.assurance,
1045
- provenance: yield* snapshotSessionAuthenticationProvenance({
1046
- evidence: input.evidence,
1047
- }),
1048
- issuedAt: now,
1049
- };
1050
-
1051
- const receipt = prepare(record, journal);
1052
-
1053
- if (flowInsert !== undefined)
1054
- yield* transaction.insert(mapping.flow.table).values(flowInsert);
1055
- else
1056
- yield* consumePending(
1057
- { ...mapping, pending: mapping.pending! },
1058
- input.pending!,
1059
- input.session.absoluteExpiresAt,
1060
- );
1061
- yield* transaction.insert(mapping.session.table).values(
1062
- mapping.session.encodeInsert(record, {
1063
- subjectId: nativeSubjectId,
1064
- sessionId: nativeSessionId,
1065
- }),
1008
+ if (!preservesRevision(input.evidence, pending.evidence))
1009
+ return yield* invalidPending();
1010
+ pendingExpiresAt = pending.expiresAt;
1011
+ }
1012
+
1013
+ const nativeSessionId = yield* allocate(
1014
+ options.mode,
1015
+ mapping.session.allocateId,
1016
+ mapping.session.allocateIdSync,
1017
+ );
1018
+
1019
+ const sessionId = yield* mapping.sessionId.toSession(nativeSessionId);
1020
+
1021
+ const version = yield* allocate(
1022
+ options.mode,
1023
+ mapping.session.allocateVersion,
1024
+ mapping.session.allocateVersionSync,
1025
+ );
1026
+
1027
+ const assessed = yield* assessAuthentication(input.evidence, requirement);
1028
+ const now = yield* freshNow;
1029
+
1030
+ if (
1031
+ !assessed.satisfied ||
1032
+ (pendingExpiresAt !== undefined &&
1033
+ DateTime.toEpochMillis(now) >= DateTime.toEpochMillis(pendingExpiresAt)) ||
1034
+ DateTime.toEpochMillis(now) >= DateTime.toEpochMillis(input.session.expiresAt) ||
1035
+ DateTime.toEpochMillis(input.session.expiresAt) >
1036
+ DateTime.toEpochMillis(input.session.absoluteExpiresAt)
1037
+ )
1038
+ return yield* stale();
1039
+
1040
+ const record = {
1041
+ ...input.session,
1042
+ sessionId,
1043
+ version,
1044
+ subjectId: input.evidence.revision.subjectId,
1045
+ securityRevision: input.evidence.revision.securityRevision,
1046
+ assurance: assessed.assurance,
1047
+ provenance: yield* snapshotSessionAuthenticationProvenance({
1048
+ evidence: input.evidence,
1049
+ }),
1050
+ issuedAt: now,
1051
+ };
1052
+
1053
+ const receipt = prepare(record, journal);
1054
+
1055
+ if (flowInsert !== undefined)
1056
+ yield* transaction.insert(mapping.flow.table).values(flowInsert);
1057
+ else
1058
+ yield* consumePending(
1059
+ { ...mapping, pending: mapping.pending! },
1060
+ input.pending!,
1061
+ input.session.absoluteExpiresAt,
1066
1062
  );
1067
-
1068
- return receipt;
1069
- }).pipe(normalizeMutation(mapping)),
1070
- ),
1071
- verify: (input: any) =>
1072
- safeTransaction(
1073
- inTransaction(
1074
- database,
1075
- Effect.gen(function* () {
1076
- const read = yield* CurrentSessionSql;
1077
-
1078
- const rows = yield* read
1079
- .select()
1080
- .from(mapping.session.table)
1081
- .where(eq(c.digest, input.digest))
1082
- .limit(1);
1083
-
1084
- const row = rows[0];
1085
-
1086
- if (row === undefined) return yield* invalidSession();
1087
- const record = yield* mapping.session.decode(row);
1088
- const nativeSubjectId = yield* mapping.subjectId.toNative(record.subjectId);
1089
- const subjects = yield* readSubject(mapping, nativeSubjectId, false);
1090
- const subject = subjects[0];
1091
- const now = yield* freshNow;
1092
-
1093
- if (
1094
- subject === undefined ||
1095
- !mapping.subject.isActiveStatus(subject[mapping.subject.status]) ||
1096
- subject[mapping.subject.securityRevision] !== record.securityRevision ||
1097
- DateTime.toEpochMillis(now) >= DateTime.toEpochMillis(record.expiresAt) ||
1098
- DateTime.toEpochMillis(now) >= DateTime.toEpochMillis(record.absoluteExpiresAt)
1099
- )
1100
- return yield* invalidSession();
1101
-
1102
- return record;
1063
+ yield* transaction.insert(mapping.session.table).values(
1064
+ mapping.session.encodeInsert(record, {
1065
+ subjectId: nativeSubjectId,
1066
+ sessionId: nativeSessionId,
1103
1067
  }),
1104
- ),
1105
- ),
1106
- rotate: <A>(
1107
- input: Parameters<StatefulSessionPersistence<Claims>["rotate"]>[0],
1108
- prepare: PrepareSessionCommit<StatefulSessionRecord<Claims>, A>,
1109
- ) =>
1110
- owned(
1068
+ );
1069
+
1070
+ return receipt;
1071
+ }).pipe(normalizeMutation(mapping)),
1072
+ ),
1073
+ verify: (input: any) =>
1074
+ safeTransaction(
1075
+ inTransaction(
1111
1076
  database,
1112
- options,
1113
- mapping.isConstraintConflict,
1114
1077
  Effect.gen(function* () {
1115
- const transaction = yield* CurrentSessionSql;
1116
- const journal = yield* CurrentCommitJournal;
1117
- const nativeSessionId = yield* mapping.sessionId.toNative(input.sessionId);
1078
+ const read = yield* CurrentSessionSql;
1118
1079
 
1119
- const initial = (yield* transaction
1080
+ const rows = yield* read
1120
1081
  .select()
1121
1082
  .from(mapping.session.table)
1122
- .where(eq(c.sessionId, nativeSessionId))
1123
- .limit(1))[0];
1124
-
1125
- if (initial === undefined) return yield* SessionConflict.make({});
1126
- const initialRecord = yield* mapping.session.decode(initial);
1127
- const nativeSubjectId = yield* mapping.subjectId.toNative(initialRecord.subjectId);
1128
- const subject = (yield* readSubject(mapping, nativeSubjectId, true))[0];
1129
-
1130
- const row = (yield* selectRows(
1131
- transaction
1132
- .select()
1133
- .from(mapping.session.table)
1134
- .where(eq(c.sessionId, nativeSessionId))
1135
- .limit(1),
1136
- options.locking,
1137
- ))[0];
1138
-
1139
- if (row === undefined) return yield* SessionConflict.make({});
1140
- const record = yield* mapping.session.decode(row);
1083
+ .where(eq(c.digest, input.digest))
1084
+ .limit(1);
1141
1085
 
1142
- const version = yield* allocate(
1143
- options.mode,
1144
- mapping.session.allocateVersion,
1145
- mapping.session.allocateVersionSync,
1146
- );
1086
+ const row = rows[0];
1147
1087
 
1088
+ if (row === undefined) return yield* invalidSession();
1089
+ const record = yield* mapping.session.decode(row);
1090
+ const nativeSubjectId = yield* mapping.subjectId.toNative(record.subjectId);
1091
+ const subjects = yield* readSubject(mapping, nativeSubjectId, false);
1092
+ const subject = subjects[0];
1148
1093
  const now = yield* freshNow;
1149
1094
 
1150
1095
  if (
1151
1096
  subject === undefined ||
1152
1097
  !mapping.subject.isActiveStatus(subject[mapping.subject.status]) ||
1153
- subject[mapping.subject.securityRevision] !== input.expectedSecurityRevision ||
1154
- record.securityRevision !== input.expectedSecurityRevision ||
1155
- record.digest !== input.expectedDigest ||
1156
- record.version !== input.expectedVersion ||
1098
+ subject[mapping.subject.securityRevision] !== record.securityRevision ||
1157
1099
  DateTime.toEpochMillis(now) >= DateTime.toEpochMillis(record.expiresAt) ||
1158
- DateTime.toEpochMillis(now) >= DateTime.toEpochMillis(record.absoluteExpiresAt) ||
1159
- DateTime.toEpochMillis(input.nextExpiresAt) >
1160
- DateTime.toEpochMillis(record.absoluteExpiresAt) ||
1161
- DateTime.toEpochMillis(input.nextExpiresAt) <= DateTime.toEpochMillis(now)
1100
+ DateTime.toEpochMillis(now) >= DateTime.toEpochMillis(record.absoluteExpiresAt)
1162
1101
  )
1163
- return yield* SessionConflict.make({});
1164
-
1165
- const next = {
1166
- ...record,
1167
- digest: input.nextDigest,
1168
- credentialVersion: input.nextCredentialVersion,
1169
- version,
1170
- issuedAt: now,
1171
- expiresAt: input.nextExpiresAt,
1172
- };
1173
-
1174
- const receipt = prepare(next, journal);
1102
+ return yield* invalidSession();
1175
1103
 
1176
- yield* transaction
1177
- .update(mapping.session.table)
1178
- .set(mapping.session.encodeRotation(next))
1179
- .where(
1180
- and(
1181
- eq(c.sessionId, nativeSessionId),
1182
- eq(c.digest, input.expectedDigest),
1183
- eq(c.version, input.expectedVersion),
1184
- ),
1185
- );
1186
-
1187
- return receipt;
1104
+ return record;
1188
1105
  }),
1189
1106
  ),
1190
- revokeDigest: <A>(digest: TokenDigest, prepare: PrepareSessionCommit<boolean, A>) =>
1191
- owned(
1192
- database,
1193
- options,
1194
- mapping.isConstraintConflict,
1195
- Effect.gen(function* () {
1196
- const transaction = yield* CurrentSessionSql;
1197
- const journal = yield* CurrentCommitJournal;
1198
-
1199
- const initial = (yield* transaction
1107
+ ),
1108
+ rotate: <A>(
1109
+ input: Parameters<StatefulSessionPersistence<Claims>["rotate"]>[0],
1110
+ prepare: PrepareSessionCommit<StatefulSessionRecord<Claims>, A>,
1111
+ ) =>
1112
+ owned(
1113
+ database,
1114
+ options,
1115
+ mapping.isConstraintConflict,
1116
+ Effect.gen(function* () {
1117
+ const transaction = yield* CurrentSessionSql;
1118
+ const journal = yield* CurrentCommitJournal;
1119
+ const nativeSessionId = yield* mapping.sessionId.toNative(input.sessionId);
1120
+
1121
+ const initial = (yield* transaction
1122
+ .select()
1123
+ .from(mapping.session.table)
1124
+ .where(eq(c.sessionId, nativeSessionId))
1125
+ .limit(1))[0];
1126
+
1127
+ if (initial === undefined) return yield* SessionConflict.make({});
1128
+ const initialRecord = yield* mapping.session.decode(initial);
1129
+ const nativeSubjectId = yield* mapping.subjectId.toNative(initialRecord.subjectId);
1130
+ const subject = (yield* readSubject(mapping, nativeSubjectId, true))[0];
1131
+
1132
+ const row = (yield* selectRows(
1133
+ transaction
1200
1134
  .select()
1201
1135
  .from(mapping.session.table)
1202
- .where(eq(c.digest, digest))
1203
- .limit(1))[0];
1204
-
1205
- if (initial === undefined) return prepare(false, journal);
1206
- const record = yield* mapping.session.decode(initial);
1207
- const nativeSubjectId = yield* mapping.subjectId.toNative(record.subjectId);
1208
-
1209
- yield* readSubject(mapping, nativeSubjectId, true);
1210
-
1211
- const row = (yield* selectRows(
1212
- transaction
1213
- .select()
1214
- .from(mapping.session.table)
1215
- .where(eq(c.digest, digest))
1216
- .limit(1),
1217
- options.locking,
1218
- ))[0];
1219
-
1220
- const receipt = prepare(row !== undefined, journal);
1221
-
1222
- if (row !== undefined)
1223
- yield* transaction.delete(mapping.session.table).where(eq(c.digest, digest));
1224
-
1225
- return receipt;
1226
- }),
1227
- ),
1228
- revoke: <A>(
1229
- input: Parameters<StatefulSessionPersistence<Claims>["revoke"]>[0],
1230
- prepare: PrepareSessionCommit<void, A>,
1231
- ) =>
1232
- owned(
1233
- database,
1234
- options,
1235
- mapping.isConstraintConflict,
1236
- Effect.gen(function* () {
1237
- const transaction = yield* CurrentSessionSql;
1238
- const journal = yield* CurrentCommitJournal;
1239
- const nativeSubjectId = yield* mapping.subjectId.toNative(input.subjectId);
1240
- const subject = (yield* readSubject(mapping, nativeSubjectId, true))[0];
1136
+ .where(eq(c.sessionId, nativeSessionId))
1137
+ .limit(1),
1138
+ options.locking,
1139
+ ))[0];
1140
+
1141
+ if (row === undefined) return yield* SessionConflict.make({});
1142
+ const record = yield* mapping.session.decode(row);
1143
+
1144
+ const version = yield* allocate(
1145
+ options.mode,
1146
+ mapping.session.allocateVersion,
1147
+ mapping.session.allocateVersionSync,
1148
+ );
1149
+
1150
+ const now = yield* freshNow;
1151
+
1152
+ if (
1153
+ subject === undefined ||
1154
+ !mapping.subject.isActiveStatus(subject[mapping.subject.status]) ||
1155
+ subject[mapping.subject.securityRevision] !== input.expectedSecurityRevision ||
1156
+ record.securityRevision !== input.expectedSecurityRevision ||
1157
+ record.digest !== input.expectedDigest ||
1158
+ record.version !== input.expectedVersion ||
1159
+ DateTime.toEpochMillis(now) >= DateTime.toEpochMillis(record.expiresAt) ||
1160
+ DateTime.toEpochMillis(now) >= DateTime.toEpochMillis(record.absoluteExpiresAt) ||
1161
+ DateTime.toEpochMillis(input.nextExpiresAt) >
1162
+ DateTime.toEpochMillis(record.absoluteExpiresAt) ||
1163
+ DateTime.toEpochMillis(input.nextExpiresAt) <= DateTime.toEpochMillis(now)
1164
+ )
1165
+ return yield* SessionConflict.make({});
1166
+
1167
+ const next = {
1168
+ ...record,
1169
+ digest: input.nextDigest,
1170
+ credentialVersion: input.nextCredentialVersion,
1171
+ version,
1172
+ issuedAt: now,
1173
+ expiresAt: input.nextExpiresAt,
1174
+ };
1175
+
1176
+ const receipt = prepare(next, journal);
1177
+
1178
+ yield* transaction
1179
+ .update(mapping.session.table)
1180
+ .set(mapping.session.encodeRotation(next))
1181
+ .where(
1182
+ and(
1183
+ eq(c.sessionId, nativeSessionId),
1184
+ eq(c.digest, input.expectedDigest),
1185
+ eq(c.version, input.expectedVersion),
1186
+ ),
1187
+ );
1241
1188
 
1242
- if (
1243
- subject === undefined ||
1244
- !mapping.subject.isActiveStatus(subject[mapping.subject.status]) ||
1245
- subject[mapping.subject.securityRevision] !== input.expectedSecurityRevision
1246
- )
1247
- return yield* stale();
1248
- const nativeSessionId = yield* mapping.sessionId.toNative(input.sessionId);
1249
- const receipt = prepare(undefined, journal);
1189
+ return receipt;
1190
+ }),
1191
+ ),
1192
+ revokeDigest: <A>(digest: TokenDigest, prepare: PrepareSessionCommit<boolean, A>) =>
1193
+ owned(
1194
+ database,
1195
+ options,
1196
+ mapping.isConstraintConflict,
1197
+ Effect.gen(function* () {
1198
+ const transaction = yield* CurrentSessionSql;
1199
+ const journal = yield* CurrentCommitJournal;
1200
+
1201
+ const initial = (yield* transaction
1202
+ .select()
1203
+ .from(mapping.session.table)
1204
+ .where(eq(c.digest, digest))
1205
+ .limit(1))[0];
1206
+
1207
+ if (initial === undefined) return prepare(false, journal);
1208
+ const record = yield* mapping.session.decode(initial);
1209
+ const nativeSubjectId = yield* mapping.subjectId.toNative(record.subjectId);
1210
+
1211
+ yield* readSubject(mapping, nativeSubjectId, true);
1212
+
1213
+ const row = (yield* selectRows(
1214
+ transaction.select().from(mapping.session.table).where(eq(c.digest, digest)).limit(1),
1215
+ options.locking,
1216
+ ))[0];
1217
+
1218
+ const receipt = prepare(row !== undefined, journal);
1219
+
1220
+ if (row !== undefined)
1221
+ yield* transaction.delete(mapping.session.table).where(eq(c.digest, digest));
1222
+
1223
+ return receipt;
1224
+ }),
1225
+ ),
1226
+ revoke: <A>(
1227
+ input: Parameters<StatefulSessionPersistence<Claims>["revoke"]>[0],
1228
+ prepare: PrepareSessionCommit<void, A>,
1229
+ ) =>
1230
+ owned(
1231
+ database,
1232
+ options,
1233
+ mapping.isConstraintConflict,
1234
+ Effect.gen(function* () {
1235
+ const transaction = yield* CurrentSessionSql;
1236
+ const journal = yield* CurrentCommitJournal;
1237
+ const nativeSubjectId = yield* mapping.subjectId.toNative(input.subjectId);
1238
+ const subject = (yield* readSubject(mapping, nativeSubjectId, true))[0];
1239
+
1240
+ if (
1241
+ subject === undefined ||
1242
+ !mapping.subject.isActiveStatus(subject[mapping.subject.status]) ||
1243
+ subject[mapping.subject.securityRevision] !== input.expectedSecurityRevision
1244
+ )
1245
+ return yield* stale();
1246
+ const nativeSessionId = yield* mapping.sessionId.toNative(input.sessionId);
1247
+ const receipt = prepare(undefined, journal);
1248
+
1249
+ yield* transaction
1250
+ .delete(mapping.session.table)
1251
+ .where(and(eq(c.subjectId, nativeSubjectId), eq(c.sessionId, nativeSessionId)));
1252
+
1253
+ return receipt;
1254
+ }),
1255
+ ),
1256
+ revokeAll: <A>(
1257
+ input: Parameters<StatefulSessionPersistence<Claims>["revokeAll"]>[0],
1258
+ prepare: PrepareSessionCommit<void, A>,
1259
+ ) =>
1260
+ owned(
1261
+ database,
1262
+ options,
1263
+ mapping.isConstraintConflict,
1264
+ Effect.gen(function* () {
1265
+ const transaction = yield* CurrentSessionSql;
1266
+ const journal = yield* CurrentCommitJournal;
1267
+ const nativeSubjectId = yield* mapping.subjectId.toNative(input.subjectId);
1268
+ const subject = (yield* readSubject(mapping, nativeSubjectId, true))[0];
1269
+
1270
+ if (
1271
+ subject === undefined ||
1272
+ !mapping.subject.isActiveStatus(subject[mapping.subject.status]) ||
1273
+ subject[mapping.subject.securityRevision] !== input.expectedSecurityRevision
1274
+ )
1275
+ return yield* stale();
1276
+ const nextRevisionSync = mapping.subject.nextSecurityRevisionSync;
1277
+
1278
+ const next = yield* allocate(
1279
+ options.mode,
1280
+ mapping.subject.nextSecurityRevision?.(input.expectedSecurityRevision),
1281
+ nextRevisionSync === undefined
1282
+ ? undefined
1283
+ : () => nextRevisionSync(input.expectedSecurityRevision),
1284
+ );
1285
+
1286
+ const receipt = prepare(undefined, journal);
1287
+
1288
+ yield* transaction
1289
+ .update(mapping.subject.table)
1290
+ .set(updateValues([[mapping.subject.securityRevision, next]]))
1291
+ .where(
1292
+ and(
1293
+ eq(authorityColumns(mapping).subjectId, nativeSubjectId),
1294
+ eq(authorityColumns(mapping).subjectRevision, input.expectedSecurityRevision),
1295
+ ),
1296
+ );
1297
+ yield* transaction
1298
+ .delete(mapping.session.table)
1299
+ .where(eq(c.subjectId, nativeSubjectId));
1250
1300
 
1251
- yield* transaction
1252
- .delete(mapping.session.table)
1253
- .where(and(eq(c.subjectId, nativeSubjectId), eq(c.sessionId, nativeSessionId)));
1301
+ return receipt;
1302
+ }),
1303
+ ),
1304
+ };
1254
1305
 
1255
- return receipt;
1256
- }),
1257
- ),
1258
- revokeAll: <A>(
1259
- input: Parameters<StatefulSessionPersistence<Claims>["revokeAll"]>[0],
1260
- prepare: PrepareSessionCommit<void, A>,
1261
- ) =>
1262
- owned(
1306
+ const repository = {
1307
+ list: (input: any) =>
1308
+ safeTransaction(
1309
+ inTransaction(
1263
1310
  database,
1264
- options,
1265
- mapping.isConstraintConflict,
1266
1311
  Effect.gen(function* () {
1267
- const transaction = yield* CurrentSessionSql;
1268
- const journal = yield* CurrentCommitJournal;
1312
+ const read = yield* CurrentSessionSql;
1269
1313
  const nativeSubjectId = yield* mapping.subjectId.toNative(input.subjectId);
1270
- const subject = (yield* readSubject(mapping, nativeSubjectId, true))[0];
1314
+ const subject = (yield* readSubject(mapping, nativeSubjectId, false))[0];
1271
1315
 
1272
1316
  if (
1273
1317
  subject === undefined ||
1274
- !mapping.subject.isActiveStatus(subject[mapping.subject.status]) ||
1275
- subject[mapping.subject.securityRevision] !== input.expectedSecurityRevision
1318
+ !mapping.subject.isActiveStatus(subject[mapping.subject.status])
1276
1319
  )
1277
- return yield* stale();
1278
- const nextRevisionSync = mapping.subject.nextSecurityRevisionSync;
1320
+ return { sessions: [] };
1279
1321
 
1280
- const next = yield* allocate(
1281
- options.mode,
1282
- mapping.subject.nextSecurityRevision?.(input.expectedSecurityRevision),
1283
- nextRevisionSync === undefined
1322
+ const cursor =
1323
+ input.cursor === undefined
1284
1324
  ? undefined
1285
- : () => nextRevisionSync(input.expectedSecurityRevision),
1286
- );
1325
+ : yield* mapping.sessionId.toNative(input.cursor as any);
1287
1326
 
1288
- const receipt = prepare(undefined, journal);
1327
+ const now = yield* freshNow;
1289
1328
 
1290
- yield* transaction
1291
- .update(mapping.subject.table)
1292
- .set(updateValues([[mapping.subject.securityRevision, next]]))
1329
+ const rows = yield* read
1330
+ .select()
1331
+ .from(mapping.session.table)
1293
1332
  .where(
1294
1333
  and(
1295
- eq(authorityColumns(mapping).subjectId, nativeSubjectId),
1296
- eq(authorityColumns(mapping).subjectRevision, input.expectedSecurityRevision),
1334
+ eq(c.subjectId, nativeSubjectId),
1335
+ eq(c.securityRevision, subject[mapping.subject.securityRevision]),
1336
+ gt(c.expiresAt, mapping.session.encodeInstant(now)),
1337
+ gt(c.absoluteExpiresAt, mapping.session.encodeInstant(now)),
1338
+ cursor === undefined ? undefined : gt(c.sessionId, cursor),
1297
1339
  ),
1298
- );
1299
- yield* transaction
1300
- .delete(mapping.session.table)
1301
- .where(eq(c.subjectId, nativeSubjectId));
1302
-
1303
- return receipt;
1304
- }),
1305
- ),
1306
- };
1307
-
1308
- const repository = {
1309
- list: (input: any) =>
1310
- safeTransaction(
1311
- inTransaction(
1312
- database,
1313
- Effect.gen(function* () {
1314
- const read = yield* CurrentSessionSql;
1315
- const nativeSubjectId = yield* mapping.subjectId.toNative(input.subjectId);
1316
- const subject = (yield* readSubject(mapping, nativeSubjectId, false))[0];
1317
-
1318
- if (
1319
- subject === undefined ||
1320
- !mapping.subject.isActiveStatus(subject[mapping.subject.status])
1321
1340
  )
1322
- return { sessions: [] };
1323
-
1324
- const cursor =
1325
- input.cursor === undefined
1326
- ? undefined
1327
- : yield* mapping.sessionId.toNative(input.cursor as any);
1341
+ .orderBy(c.sessionId)
1342
+ .limit(input.limit + 1);
1328
1343
 
1329
- const now = yield* freshNow;
1330
-
1331
- const rows = yield* read
1332
- .select()
1333
- .from(mapping.session.table)
1334
- .where(
1335
- and(
1336
- eq(c.subjectId, nativeSubjectId),
1337
- eq(c.securityRevision, subject[mapping.subject.securityRevision]),
1338
- gt(c.expiresAt, mapping.session.encodeInstant(now)),
1339
- gt(c.absoluteExpiresAt, mapping.session.encodeInstant(now)),
1340
- cursor === undefined ? undefined : gt(c.sessionId, cursor),
1341
- ),
1342
- )
1343
- .orderBy(c.sessionId)
1344
- .limit(input.limit + 1);
1345
-
1346
- const decoded = yield* Effect.forEach(rows, (row: any) =>
1347
- mapping.session.decode(row),
1348
- );
1344
+ const decoded = yield* Effect.forEach(rows, (row: any) =>
1345
+ mapping.session.decode(row),
1346
+ );
1349
1347
 
1350
- const codec = Schema.toCodecJson(Schema.toType(SessionMetadata));
1348
+ const codec = Schema.toCodecJson(Schema.toType(SessionMetadata));
1351
1349
 
1352
- const sessions = yield* Effect.forEach(
1353
- decoded.slice(0, input.limit),
1354
- (record: any) =>
1355
- Schema.encodeEffect(codec)(record).pipe(
1356
- Effect.flatMap(Schema.decodeEffect(codec)),
1357
- ),
1358
- );
1350
+ const sessions = yield* Effect.forEach(decoded.slice(0, input.limit), (record: any) =>
1351
+ Schema.encodeEffect(codec)(record).pipe(Effect.flatMap(Schema.decodeEffect(codec))),
1352
+ );
1359
1353
 
1360
- const extra = decoded[input.limit];
1354
+ const extra = decoded[input.limit];
1361
1355
 
1362
- return {
1363
- sessions,
1364
- ...(extra === undefined ? {} : { nextCursor: sessions.at(-1)?.sessionId }),
1365
- };
1366
- }),
1367
- ),
1356
+ return {
1357
+ sessions,
1358
+ ...(extra === undefined ? {} : { nextCursor: sessions.at(-1)?.sessionId }),
1359
+ };
1360
+ }),
1368
1361
  ),
1369
- };
1370
-
1371
- return {
1372
- statefulSessionPersistence: {
1373
- establish: (input, prepare) =>
1374
- persistence
1375
- .establish(input, prepare)
1376
- .pipe(Effect.provideService(LifecycleHooks, hooks)),
1377
- verify: (input) =>
1378
- persistence.verify(input).pipe(Effect.provideService(LifecycleHooks, hooks)),
1379
- rotate: (input, prepare) =>
1380
- persistence.rotate(input, prepare).pipe(Effect.provideService(LifecycleHooks, hooks)),
1381
- revokeDigest: (digest, prepare) =>
1382
- persistence
1383
- .revokeDigest(digest, prepare)
1384
- .pipe(Effect.provideService(LifecycleHooks, hooks)),
1385
- revoke: (input, prepare) =>
1386
- persistence.revoke(input, prepare).pipe(Effect.provideService(LifecycleHooks, hooks)),
1387
- revokeAll: (input, prepare) =>
1388
- persistence
1389
- .revokeAll(input, prepare)
1390
- .pipe(Effect.provideService(LifecycleHooks, hooks)),
1391
- } satisfies StatefulSessionPersistence<Claims>,
1392
- sessionRepository: {
1393
- list: (input) =>
1394
- repository.list(input).pipe(Effect.provideService(LifecycleHooks, hooks)),
1395
- } satisfies SessionRepository,
1396
- };
1397
- });
1362
+ ),
1363
+ };
1398
1364
 
1399
- const makeSqlSignedValidity = (
1400
- database: Database,
1365
+ return {
1366
+ statefulSessionPersistence: {
1367
+ establish: (input, prepare) =>
1368
+ persistence.establish(input, prepare).pipe(Effect.provideService(LifecycleHooks, hooks)),
1369
+ verify: (input) =>
1370
+ persistence.verify(input).pipe(Effect.provideService(LifecycleHooks, hooks)),
1371
+ rotate: (input, prepare) =>
1372
+ persistence.rotate(input, prepare).pipe(Effect.provideService(LifecycleHooks, hooks)),
1373
+ revokeDigest: (digest, prepare) =>
1374
+ persistence
1375
+ .revokeDigest(digest, prepare)
1376
+ .pipe(Effect.provideService(LifecycleHooks, hooks)),
1377
+ revoke: (input, prepare) =>
1378
+ persistence.revoke(input, prepare).pipe(Effect.provideService(LifecycleHooks, hooks)),
1379
+ revokeAll: (input, prepare) =>
1380
+ persistence.revokeAll(input, prepare).pipe(Effect.provideService(LifecycleHooks, hooks)),
1381
+ } satisfies StatefulSessionPersistence<Claims>,
1382
+ sessionRepository: {
1383
+ list: (input) => repository.list(input).pipe(Effect.provideService(LifecycleHooks, hooks)),
1384
+ } satisfies SessionRepository,
1385
+ };
1386
+ });
1387
+
1388
+ const makeSqlSignedValidity = Effect.fnUntraced(function* (
1401
1389
  mapping: SignedSessionValidityMapping<any, any, any, any>,
1402
1390
  options: SessionSqlOptions,
1403
- ): Effect.Effect<SignedSessionValidity, never, LifecycleHooks> =>
1404
- Effect.map(LifecycleHooks, (hooks) => {
1405
- const subject = subjectColumns(mapping);
1406
-
1407
- const tombstone = {
1408
- subjectId: column(mapping.tombstone.table, mapping.tombstone.subjectId),
1409
- sessionId: column(mapping.tombstone.table, mapping.tombstone.sessionId),
1410
- absoluteExpiresAt: column(mapping.tombstone.table, mapping.tombstone.absoluteExpiresAt),
1411
- };
1412
-
1413
- const service = {
1414
- verify: (session: SessionMetadata, _now: DateTime.Utc) =>
1415
- safeTransaction(
1416
- inTransaction(
1417
- database,
1418
- Effect.gen(function* () {
1419
- const read = yield* CurrentSessionSql;
1420
- const nativeSubjectId = yield* mapping.subjectId.toNative(session.subjectId);
1421
- const nativeSessionId = yield* mapping.sessionId.toNative(session.sessionId);
1422
- const subjects = yield* readSubject(mapping, nativeSubjectId, false);
1423
- const row = subjects[0];
1424
-
1425
- if (
1426
- row === undefined ||
1427
- !mapping.subject.isActiveStatus(row[mapping.subject.status]) ||
1428
- row[mapping.subject.securityRevision] !== session.securityRevision
1429
- )
1430
- return yield* invalidSession();
1431
- const authoritativeNow = yield* freshNow;
1432
-
1433
- if (
1434
- DateTime.toEpochMillis(authoritativeNow) >=
1435
- DateTime.toEpochMillis(session.expiresAt) ||
1436
- DateTime.toEpochMillis(authoritativeNow) >=
1437
- DateTime.toEpochMillis(session.absoluteExpiresAt)
1438
- )
1439
- return yield* invalidSession();
1391
+ ): Effect.fn.Return<SignedSessionValidity, never, LifecycleHooks | CurrentSessionSql> {
1392
+ const database = yield* CurrentSessionSql;
1393
+ const hooks = yield* LifecycleHooks;
1440
1394
 
1441
- const revoked = yield* read
1442
- .select({ sessionId: tombstone.sessionId })
1443
- .from(mapping.tombstone.table)
1444
- .where(
1445
- and(
1446
- eq(tombstone.subjectId, nativeSubjectId),
1447
- eq(tombstone.sessionId, nativeSessionId),
1448
- gt(
1449
- tombstone.absoluteExpiresAt,
1450
- mapping.tombstone.encodeInstant(authoritativeNow),
1451
- ),
1452
- ),
1453
- )
1454
- .limit(1);
1395
+ const subject = subjectColumns(mapping);
1455
1396
 
1456
- if (revoked.length > 0) return yield* invalidSession();
1457
- }),
1458
- ),
1459
- ),
1460
- revoke: <A>(
1461
- input: Parameters<SignedSessionValidity["revoke"]>[0],
1462
- prepare: PrepareSessionCommit<void, A>,
1463
- ) =>
1464
- owned(
1397
+ const tombstone = {
1398
+ subjectId: column(mapping.tombstone.table, mapping.tombstone.subjectId),
1399
+ sessionId: column(mapping.tombstone.table, mapping.tombstone.sessionId),
1400
+ absoluteExpiresAt: column(mapping.tombstone.table, mapping.tombstone.absoluteExpiresAt),
1401
+ };
1402
+
1403
+ const service = {
1404
+ verify: (session: SessionMetadata, _now: DateTime.Utc) =>
1405
+ safeTransaction(
1406
+ inTransaction(
1465
1407
  database,
1466
- options,
1467
- mapping.isConstraintConflict,
1468
1408
  Effect.gen(function* () {
1469
- const transaction = yield* CurrentSessionSql;
1470
- const journal = yield* CurrentCommitJournal;
1471
- const nativeSubjectId = yield* mapping.subjectId.toNative(input.subjectId);
1472
- const row = (yield* readSubject(mapping, nativeSubjectId, true))[0];
1409
+ const read = yield* CurrentSessionSql;
1410
+ const nativeSubjectId = yield* mapping.subjectId.toNative(session.subjectId);
1411
+ const nativeSessionId = yield* mapping.sessionId.toNative(session.sessionId);
1412
+ const subjects = yield* readSubject(mapping, nativeSubjectId, false);
1413
+ const row = subjects[0];
1473
1414
 
1474
1415
  if (
1475
1416
  row === undefined ||
1476
1417
  !mapping.subject.isActiveStatus(row[mapping.subject.status]) ||
1477
- row[mapping.subject.securityRevision] !== input.expectedSecurityRevision
1418
+ row[mapping.subject.securityRevision] !== session.securityRevision
1478
1419
  )
1479
- return yield* stale();
1480
- const nativeSessionId = yield* mapping.sessionId.toNative(input.sessionId);
1420
+ return yield* invalidSession();
1421
+ const authoritativeNow = yield* freshNow;
1481
1422
 
1482
- const existing = yield* transaction
1483
- .select()
1423
+ if (
1424
+ DateTime.toEpochMillis(authoritativeNow) >=
1425
+ DateTime.toEpochMillis(session.expiresAt) ||
1426
+ DateTime.toEpochMillis(authoritativeNow) >=
1427
+ DateTime.toEpochMillis(session.absoluteExpiresAt)
1428
+ )
1429
+ return yield* invalidSession();
1430
+
1431
+ const revoked = yield* read
1432
+ .select({ sessionId: tombstone.sessionId })
1484
1433
  .from(mapping.tombstone.table)
1485
1434
  .where(
1486
1435
  and(
1487
1436
  eq(tombstone.subjectId, nativeSubjectId),
1488
1437
  eq(tombstone.sessionId, nativeSessionId),
1438
+ gt(
1439
+ tombstone.absoluteExpiresAt,
1440
+ mapping.tombstone.encodeInstant(authoritativeNow),
1441
+ ),
1489
1442
  ),
1490
1443
  )
1491
1444
  .limit(1);
1492
1445
 
1493
- const receipt = prepare(undefined, journal);
1494
-
1495
- if (existing[0] === undefined)
1496
- yield* transaction.insert(mapping.tombstone.table).values(
1497
- mapping.tombstone.encodeInsert({
1498
- subjectId: nativeSubjectId,
1499
- sessionId: nativeSessionId,
1500
- absoluteExpiresAt: input.absoluteExpiresAt,
1501
- }),
1502
- );
1503
- else {
1504
- const existingExpiry = yield* mapping.tombstone.decodeInstant(
1505
- existing[0][mapping.tombstone.absoluteExpiresAt],
1506
- );
1507
-
1508
- if (
1509
- DateTime.toEpochMillis(input.absoluteExpiresAt) >
1510
- DateTime.toEpochMillis(existingExpiry)
1511
- )
1512
- yield* transaction
1513
- .update(mapping.tombstone.table)
1514
- .set(
1515
- updateValues([
1516
- [
1517
- mapping.tombstone.absoluteExpiresAt,
1518
- mapping.tombstone.encodeInstant(input.absoluteExpiresAt),
1519
- ],
1520
- ]),
1521
- )
1522
- .where(
1523
- and(
1524
- eq(tombstone.subjectId, nativeSubjectId),
1525
- eq(tombstone.sessionId, nativeSessionId),
1526
- ),
1527
- );
1528
- }
1529
-
1530
- return receipt;
1446
+ if (revoked.length > 0) return yield* invalidSession();
1531
1447
  }),
1532
1448
  ),
1533
- revokeAll: <A>(
1534
- input: Parameters<SignedSessionValidity["revokeAll"]>[0],
1535
- prepare: PrepareSessionCommit<void, A>,
1536
- ) =>
1537
- owned(
1538
- database,
1539
- options,
1540
- mapping.isConstraintConflict,
1541
- Effect.gen(function* () {
1542
- const transaction = yield* CurrentSessionSql;
1543
- const journal = yield* CurrentCommitJournal;
1544
- const nativeSubjectId = yield* mapping.subjectId.toNative(input.subjectId);
1545
- const row = (yield* readSubject(mapping, nativeSubjectId, true))[0];
1546
-
1547
- if (
1548
- row === undefined ||
1549
- !mapping.subject.isActiveStatus(row[mapping.subject.status]) ||
1550
- row[mapping.subject.securityRevision] !== input.expectedSecurityRevision
1449
+ ),
1450
+ revoke: <A>(
1451
+ input: Parameters<SignedSessionValidity["revoke"]>[0],
1452
+ prepare: PrepareSessionCommit<void, A>,
1453
+ ) =>
1454
+ owned(
1455
+ database,
1456
+ options,
1457
+ mapping.isConstraintConflict,
1458
+ Effect.gen(function* () {
1459
+ const transaction = yield* CurrentSessionSql;
1460
+ const journal = yield* CurrentCommitJournal;
1461
+ const nativeSubjectId = yield* mapping.subjectId.toNative(input.subjectId);
1462
+ const row = (yield* readSubject(mapping, nativeSubjectId, true))[0];
1463
+
1464
+ if (
1465
+ row === undefined ||
1466
+ !mapping.subject.isActiveStatus(row[mapping.subject.status]) ||
1467
+ row[mapping.subject.securityRevision] !== input.expectedSecurityRevision
1468
+ )
1469
+ return yield* stale();
1470
+ const nativeSessionId = yield* mapping.sessionId.toNative(input.sessionId);
1471
+
1472
+ const existing = yield* transaction
1473
+ .select()
1474
+ .from(mapping.tombstone.table)
1475
+ .where(
1476
+ and(
1477
+ eq(tombstone.subjectId, nativeSubjectId),
1478
+ eq(tombstone.sessionId, nativeSessionId),
1479
+ ),
1551
1480
  )
1552
- return yield* stale();
1553
- const nextRevisionSync = mapping.subject.nextSecurityRevisionSync;
1481
+ .limit(1);
1554
1482
 
1555
- const next = yield* allocate(
1556
- options.mode,
1557
- mapping.subject.nextSecurityRevision?.(input.expectedSecurityRevision),
1558
- nextRevisionSync === undefined
1559
- ? undefined
1560
- : () => nextRevisionSync(input.expectedSecurityRevision),
1483
+ const receipt = prepare(undefined, journal);
1484
+
1485
+ if (existing[0] === undefined)
1486
+ yield* transaction.insert(mapping.tombstone.table).values(
1487
+ mapping.tombstone.encodeInsert({
1488
+ subjectId: nativeSubjectId,
1489
+ sessionId: nativeSessionId,
1490
+ absoluteExpiresAt: input.absoluteExpiresAt,
1491
+ }),
1492
+ );
1493
+ else {
1494
+ const existingExpiry = yield* mapping.tombstone.decodeInstant(
1495
+ existing[0][mapping.tombstone.absoluteExpiresAt],
1561
1496
  );
1562
1497
 
1563
- const receipt = prepare(undefined, journal);
1498
+ if (
1499
+ DateTime.toEpochMillis(input.absoluteExpiresAt) >
1500
+ DateTime.toEpochMillis(existingExpiry)
1501
+ )
1502
+ yield* transaction
1503
+ .update(mapping.tombstone.table)
1504
+ .set(
1505
+ updateValues([
1506
+ [
1507
+ mapping.tombstone.absoluteExpiresAt,
1508
+ mapping.tombstone.encodeInstant(input.absoluteExpiresAt),
1509
+ ],
1510
+ ]),
1511
+ )
1512
+ .where(
1513
+ and(
1514
+ eq(tombstone.subjectId, nativeSubjectId),
1515
+ eq(tombstone.sessionId, nativeSessionId),
1516
+ ),
1517
+ );
1518
+ }
1564
1519
 
1565
- yield* transaction
1566
- .update(mapping.subject.table)
1567
- .set(updateValues([[mapping.subject.securityRevision, next]]))
1568
- .where(
1569
- and(
1570
- eq(subject.subjectId, nativeSubjectId),
1571
- eq(subject.subjectRevision, input.expectedSecurityRevision),
1572
- ),
1573
- );
1520
+ return receipt;
1521
+ }),
1522
+ ),
1523
+ revokeAll: <A>(
1524
+ input: Parameters<SignedSessionValidity["revokeAll"]>[0],
1525
+ prepare: PrepareSessionCommit<void, A>,
1526
+ ) =>
1527
+ owned(
1528
+ database,
1529
+ options,
1530
+ mapping.isConstraintConflict,
1531
+ Effect.gen(function* () {
1532
+ const transaction = yield* CurrentSessionSql;
1533
+ const journal = yield* CurrentCommitJournal;
1534
+ const nativeSubjectId = yield* mapping.subjectId.toNative(input.subjectId);
1535
+ const row = (yield* readSubject(mapping, nativeSubjectId, true))[0];
1536
+
1537
+ if (
1538
+ row === undefined ||
1539
+ !mapping.subject.isActiveStatus(row[mapping.subject.status]) ||
1540
+ row[mapping.subject.securityRevision] !== input.expectedSecurityRevision
1541
+ )
1542
+ return yield* stale();
1543
+ const nextRevisionSync = mapping.subject.nextSecurityRevisionSync;
1544
+
1545
+ const next = yield* allocate(
1546
+ options.mode,
1547
+ mapping.subject.nextSecurityRevision?.(input.expectedSecurityRevision),
1548
+ nextRevisionSync === undefined
1549
+ ? undefined
1550
+ : () => nextRevisionSync(input.expectedSecurityRevision),
1551
+ );
1552
+
1553
+ const receipt = prepare(undefined, journal);
1554
+
1555
+ yield* transaction
1556
+ .update(mapping.subject.table)
1557
+ .set(updateValues([[mapping.subject.securityRevision, next]]))
1558
+ .where(
1559
+ and(
1560
+ eq(subject.subjectId, nativeSubjectId),
1561
+ eq(subject.subjectRevision, input.expectedSecurityRevision),
1562
+ ),
1563
+ );
1574
1564
 
1575
- return receipt;
1576
- }),
1577
- ),
1578
- };
1565
+ return receipt;
1566
+ }),
1567
+ ),
1568
+ };
1579
1569
 
1580
- return {
1581
- verify: (session, now) =>
1582
- service.verify(session, now).pipe(Effect.provideService(LifecycleHooks, hooks)),
1583
- revoke: (input, prepare) =>
1584
- service.revoke(input, prepare).pipe(Effect.provideService(LifecycleHooks, hooks)),
1585
- revokeAll: (input, prepare) =>
1586
- service.revokeAll(input, prepare).pipe(Effect.provideService(LifecycleHooks, hooks)),
1587
- } satisfies SignedSessionValidity;
1588
- });
1570
+ return {
1571
+ verify: (session, now) =>
1572
+ service.verify(session, now).pipe(Effect.provideService(LifecycleHooks, hooks)),
1573
+ revoke: (input, prepare) =>
1574
+ service.revoke(input, prepare).pipe(Effect.provideService(LifecycleHooks, hooks)),
1575
+ revokeAll: (input, prepare) =>
1576
+ service.revokeAll(input, prepare).pipe(Effect.provideService(LifecycleHooks, hooks)),
1577
+ } satisfies SignedSessionValidity;
1578
+ });
1589
1579
 
1590
1580
  const stepUpColumns = (mapping: any) =>
1591
1581
  Object.fromEntries(
@@ -1787,425 +1777,418 @@ export const makeSessionKernel = (operations: QueryOperations) => {
1787
1777
  );
1788
1778
 
1789
1779
  /** Shared implementation; public driver entrypoints restore concrete tables/IDs. */
1790
- const makeSqlSessionStepUp = <Claims>(
1791
- database: Database,
1780
+ const makeSqlSessionStepUp = Effect.fnUntraced(function* <Claims>(
1792
1781
  mapping: SessionStepUpMapping<Claims, any, any, any, any, any, any, any>,
1793
1782
  options: SessionSqlOptions,
1794
- ): Effect.Effect<SessionStepUpPersistence<Claims>, never, LifecycleHooks> =>
1795
- Effect.map(LifecycleHooks, (hooks) => {
1796
- const m = mapping.intent,
1797
- c = stepUpColumns(mapping);
1798
-
1799
- const read = (digest: TokenDigest) =>
1800
- mapFailureCause(
1801
- safeTransaction(inTransaction(database, readSqlStepUp(mapping, digest, options.locking))),
1802
- (error) => (error._tag === "StaleAuthentication" ? SessionStepUpInvalid.make({}) : error),
1803
- );
1804
-
1805
- const service = {
1806
- create: <A>(
1807
- input: Omit<SessionStepUpIntent, "version">,
1808
- _now: DateTime.Utc,
1809
- prepare: PrepareSessionCommit<SessionStepUpIntent, A>,
1810
- ) =>
1811
- owned(
1812
- database,
1813
- options,
1814
- mapping.isConstraintConflict,
1815
- Effect.gen(function* () {
1816
- const tx = yield* CurrentSessionSql;
1817
- const journal = yield* CurrentCommitJournal;
1818
- const authority = yield* stepUpAuthority(mapping, input.revision, options.locking);
1819
-
1820
- const version = yield* allocate(
1821
- options.mode,
1822
- m.allocateVersion,
1823
- m.allocateVersionSync,
1824
- );
1825
-
1826
- const record: SessionStepUpIntent = { ...input, version };
1827
-
1828
- yield* stepUpSource(
1829
- mapping,
1830
- record,
1831
- authority.nativeSubjectId,
1832
- yield* freshNow,
1833
- options.locking,
1834
- );
1835
-
1836
- const existing = yield* selectRows(
1837
- tx
1838
- .select()
1839
- .from(m.table)
1840
- .where(or(eq(c.digest, input.digest), eq(c.flowId, input.flowId)))
1841
- .limit(1),
1842
- options.locking,
1843
- );
1844
-
1845
- if (existing.length > 0) return yield* SessionConflict.make({});
1846
- const snapshot = yield* encodeStepUpIntent(record);
1847
-
1848
- const values = {
1849
- ...m.encodeInsert(record, { subjectId: authority.nativeSubjectId }),
1850
- ...updateValues([
1851
- [m.digest, record.digest],
1852
- [m.version, version],
1853
- [m.flowId, record.flowId],
1854
- [m.subjectId, authority.nativeSubjectId],
1855
- [m.bindingDigest, record.bindingDigest],
1856
- [m.snapshot, snapshot],
1857
- [m.expiresAt, m.encodeInstant(record.expiresAt)],
1858
- [m.attemptLimit, record.attemptLimit],
1859
- [m.failedAttempts, 0],
1860
- [m.consumed, false],
1861
- ]),
1862
- };
1783
+ ): Effect.fn.Return<SessionStepUpPersistence<Claims>, never, LifecycleHooks | CurrentSessionSql> {
1784
+ const database = yield* CurrentSessionSql;
1785
+ const hooks = yield* LifecycleHooks;
1863
1786
 
1864
- const receipt = prepare(record, journal);
1787
+ const m = mapping.intent,
1788
+ c = stepUpColumns(mapping);
1865
1789
 
1866
- yield* tx.insert(m.table).values(values).pipe(normalizeMutation(mapping));
1790
+ const read = (digest: TokenDigest) =>
1791
+ mapFailureCause(
1792
+ safeTransaction(inTransaction(database, readSqlStepUp(mapping, digest, options.locking))),
1793
+ (error) => (error._tag === "StaleAuthentication" ? SessionStepUpInvalid.make({}) : error),
1794
+ );
1867
1795
 
1868
- const inserted = (yield* tx
1796
+ const service = {
1797
+ create: <A>(
1798
+ input: Omit<SessionStepUpIntent, "version">,
1799
+ _now: DateTime.Utc,
1800
+ prepare: PrepareSessionCommit<SessionStepUpIntent, A>,
1801
+ ) =>
1802
+ owned(
1803
+ database,
1804
+ options,
1805
+ mapping.isConstraintConflict,
1806
+ Effect.gen(function* () {
1807
+ const tx = yield* CurrentSessionSql;
1808
+ const journal = yield* CurrentCommitJournal;
1809
+ const authority = yield* stepUpAuthority(mapping, input.revision, options.locking);
1810
+
1811
+ const version = yield* allocate(options.mode, m.allocateVersion, m.allocateVersionSync);
1812
+
1813
+ const record: SessionStepUpIntent = { ...input, version };
1814
+
1815
+ yield* stepUpSource(
1816
+ mapping,
1817
+ record,
1818
+ authority.nativeSubjectId,
1819
+ yield* freshNow,
1820
+ options.locking,
1821
+ );
1822
+
1823
+ const existing = yield* selectRows(
1824
+ tx
1869
1825
  .select()
1870
1826
  .from(m.table)
1871
- .where(
1872
- and(
1873
- eq(c.digest, record.digest),
1874
- eq(c.version, version),
1875
- eq(c.flowId, record.flowId),
1876
- eq(c.subjectId, authority.nativeSubjectId),
1877
- eq(c.bindingDigest, record.bindingDigest),
1878
- eq(c.snapshot, snapshot),
1879
- eq(c.expiresAt, m.encodeInstant(record.expiresAt)),
1880
- eq(c.attemptLimit, record.attemptLimit),
1881
- eq(c.failedAttempts, 0),
1882
- eq(c.consumed, false),
1883
- ),
1884
- )
1885
- .limit(1))[0];
1827
+ .where(or(eq(c.digest, input.digest), eq(c.flowId, input.flowId)))
1828
+ .limit(1),
1829
+ options.locking,
1830
+ );
1831
+
1832
+ if (existing.length > 0) return yield* SessionConflict.make({});
1833
+ const snapshot = yield* encodeStepUpIntent(record);
1834
+
1835
+ const values = {
1836
+ ...m.encodeInsert(record, { subjectId: authority.nativeSubjectId }),
1837
+ ...updateValues([
1838
+ [m.digest, record.digest],
1839
+ [m.version, version],
1840
+ [m.flowId, record.flowId],
1841
+ [m.subjectId, authority.nativeSubjectId],
1842
+ [m.bindingDigest, record.bindingDigest],
1843
+ [m.snapshot, snapshot],
1844
+ [m.expiresAt, m.encodeInstant(record.expiresAt)],
1845
+ [m.attemptLimit, record.attemptLimit],
1846
+ [m.failedAttempts, 0],
1847
+ [m.consumed, false],
1848
+ ]),
1849
+ };
1850
+
1851
+ const receipt = prepare(record, journal);
1852
+
1853
+ yield* tx.insert(m.table).values(values).pipe(normalizeMutation(mapping));
1854
+
1855
+ const inserted = (yield* tx
1856
+ .select()
1857
+ .from(m.table)
1858
+ .where(
1859
+ and(
1860
+ eq(c.digest, record.digest),
1861
+ eq(c.version, version),
1862
+ eq(c.flowId, record.flowId),
1863
+ eq(c.subjectId, authority.nativeSubjectId),
1864
+ eq(c.bindingDigest, record.bindingDigest),
1865
+ eq(c.snapshot, snapshot),
1866
+ eq(c.expiresAt, m.encodeInstant(record.expiresAt)),
1867
+ eq(c.attemptLimit, record.attemptLimit),
1868
+ eq(c.failedAttempts, 0),
1869
+ eq(c.consumed, false),
1870
+ ),
1871
+ )
1872
+ .limit(1))[0];
1886
1873
 
1887
- if (inserted === undefined) return yield* unavailable();
1874
+ if (inserted === undefined) return yield* unavailable();
1888
1875
 
1889
- return receipt;
1890
- }),
1891
- ).pipe((effect) =>
1892
- mapFailureCause(effect, (error) =>
1893
- error._tag === "SessionStepUpInvalid" ? stale() : error,
1894
- ),
1895
- ),
1896
- context: (input: any) =>
1897
- mapFailureCause(
1898
- read(input.digest).pipe(
1899
- Effect.flatMap(({ intent }) =>
1900
- snapshotPendingAuthenticationContext({
1901
- flowId: intent.flowId,
1902
- bindingDigest: intent.bindingDigest,
1903
- revision: intent.revision,
1904
- expiresAtMillis: DateTime.toEpochMillis(intent.expiresAt),
1905
- }),
1906
- ),
1907
- ),
1908
- (error) => (error._tag === "PendingAuthenticationInvalid" ? unavailable() : error),
1876
+ return receipt;
1877
+ }),
1878
+ ).pipe((effect) =>
1879
+ mapFailureCause(effect, (error) =>
1880
+ error._tag === "SessionStepUpInvalid" ? stale() : error,
1909
1881
  ),
1910
- read: (input: any) =>
1882
+ ),
1883
+ context: (input: any) =>
1884
+ mapFailureCause(
1911
1885
  read(input.digest).pipe(
1912
- Effect.flatMap((value) =>
1913
- value.intent.bindingDigest === input.bindingDigest
1914
- ? Effect.succeed(value.intent)
1915
- : Effect.fail(SessionStepUpInvalid.make({})),
1886
+ Effect.flatMap(({ intent }) =>
1887
+ snapshotPendingAuthenticationContext({
1888
+ flowId: intent.flowId,
1889
+ bindingDigest: intent.bindingDigest,
1890
+ revision: intent.revision,
1891
+ expiresAtMillis: DateTime.toEpochMillis(intent.expiresAt),
1892
+ }),
1916
1893
  ),
1917
1894
  ),
1918
- reject: <A>(
1919
- input: Parameters<SessionStepUpPersistence<Claims>["reject"]>[0],
1920
- prepare: PrepareSessionCommit<{ readonly _tag: "Rejected" }, A>,
1921
- ) =>
1922
- owned(
1923
- database,
1924
- options,
1925
- mapping.isConstraintConflict,
1926
- Effect.gen(function* () {
1927
- const tx = yield* CurrentSessionSql;
1928
- const journal = yield* CurrentCommitJournal;
1929
-
1930
- const selected = yield* recoverSqlStepUpRejectAbsence(
1931
- readSqlStepUp(mapping, input.digest, options.locking).pipe(
1932
- Effect.map((value) => ({ _tag: "Found" as const, value })),
1895
+ (error) => (error._tag === "PendingAuthenticationInvalid" ? unavailable() : error),
1896
+ ),
1897
+ read: (input: any) =>
1898
+ read(input.digest).pipe(
1899
+ Effect.flatMap((value) =>
1900
+ value.intent.bindingDigest === input.bindingDigest
1901
+ ? Effect.succeed(value.intent)
1902
+ : Effect.fail(SessionStepUpInvalid.make({})),
1903
+ ),
1904
+ ),
1905
+ reject: <A>(
1906
+ input: Parameters<SessionStepUpPersistence<Claims>["reject"]>[0],
1907
+ prepare: PrepareSessionCommit<{ readonly _tag: "Rejected" }, A>,
1908
+ ) =>
1909
+ owned(
1910
+ database,
1911
+ options,
1912
+ mapping.isConstraintConflict,
1913
+ Effect.gen(function* () {
1914
+ const tx = yield* CurrentSessionSql;
1915
+ const journal = yield* CurrentCommitJournal;
1916
+
1917
+ const selected = yield* recoverSqlStepUpRejectAbsence(
1918
+ readSqlStepUp(mapping, input.digest, options.locking).pipe(
1919
+ Effect.map((value) => ({ _tag: "Found" as const, value })),
1920
+ ),
1921
+ { _tag: "Rejected" as const },
1922
+ );
1923
+
1924
+ if (selected._tag === "Rejected") return prepare({ _tag: "Rejected" }, journal);
1925
+ const { row, intent } = selected.value;
1926
+ const receipt = prepare({ _tag: "Rejected" }, journal);
1927
+
1928
+ yield* tx
1929
+ .update(m.table)
1930
+ .set(updateValues([[m.failedAttempts, sql`${c.failedAttempts} + 1`]]))
1931
+ .where(
1932
+ and(
1933
+ eq(c.digest, input.digest),
1934
+ eq(c.snapshot, row[m.snapshot]),
1935
+ eq(c.version, intent.version),
1936
+ eq(c.consumed, false),
1937
+ sql`${c.failedAttempts} < ${c.attemptLimit}`,
1933
1938
  ),
1934
- { _tag: "Rejected" as const },
1935
1939
  );
1940
+ const written = yield* stepUpLocator(mapping, input.digest, false);
1941
+
1942
+ if (
1943
+ written === undefined ||
1944
+ written[m.version] !== intent.version ||
1945
+ written[m.snapshot] !== row[m.snapshot] ||
1946
+ written[m.failedAttempts] !== row[m.failedAttempts] + 1
1947
+ )
1948
+ return yield* unavailable();
1949
+
1950
+ return receipt;
1951
+ }),
1952
+ ),
1953
+ complete: <A>(
1954
+ plan: Parameters<SessionStepUpPersistence<Claims>["complete"]>[0],
1955
+ prepare: PrepareSessionCommit<SessionInspection<Claims>, A>,
1956
+ ) =>
1957
+ owned(
1958
+ database,
1959
+ options,
1960
+ mapping.isConstraintConflict,
1961
+ Effect.gen(function* () {
1962
+ const tx = yield* CurrentSessionSql;
1963
+ const journal = yield* CurrentCommitJournal;
1964
+
1965
+ yield* validateStepUpPlan(plan);
1966
+
1967
+ // Lock the entire combined vector before source/intent, including added factors.
1968
+ const current = yield* stepUpAuthority(
1969
+ mapping,
1970
+ plan.evidence.revision,
1971
+ options.locking,
1972
+ );
1973
+
1974
+ const stored = yield* readSqlStepUp(mapping, plan.intent.digest, options.locking);
1975
+
1976
+ if (
1977
+ (yield* encodeStepUpIntent(stored.intent)) !==
1978
+ (yield* encodeStepUpIntent(plan.intent))
1979
+ )
1980
+ return yield* SessionStepUpInvalid.make({});
1981
+
1982
+ const base = yield* assessAuthentication(plan.evidence, current.requirement).pipe(
1983
+ Effect.mapError(() => stale()),
1984
+ );
1985
+
1986
+ const profile = yield* assessAuthentication(
1987
+ plan.evidence,
1988
+ stored.intent.requirement,
1989
+ ).pipe(Effect.mapError(() => stale()));
1990
+
1991
+ if (!base.satisfied || !profile.satisfied) return yield* stale();
1992
+
1993
+ const replacement = plan.replacement,
1994
+ now = yield* freshNow;
1995
+
1996
+ if (
1997
+ DateTime.toEpochMillis(replacement.inspection.session.expiresAt) >
1998
+ DateTime.toEpochMillis(stored.intent.sourceAbsoluteExpiresAt) ||
1999
+ DateTime.toEpochMillis(now) <
2000
+ DateTime.toEpochMillis(replacement.inspection.session.issuedAt) ||
2001
+ DateTime.toEpochMillis(now) >=
2002
+ Math.min(
2003
+ DateTime.toEpochMillis(replacement.inspection.session.expiresAt),
2004
+ DateTime.toEpochMillis(stored.intent.expiresAt),
2005
+ )
2006
+ )
2007
+ return yield* SessionStepUpInvalid.make({});
2008
+ let rotation: any;
1936
2009
 
1937
- if (selected._tag === "Rejected") return prepare({ _tag: "Rejected" }, journal);
1938
- const { row, intent } = selected.value;
1939
- const receipt = prepare({ _tag: "Rejected" }, journal);
1940
-
1941
- yield* tx
1942
- .update(m.table)
1943
- .set(updateValues([[m.failedAttempts, sql`${c.failedAttempts} + 1`]]))
1944
- .where(
1945
- and(
1946
- eq(c.digest, input.digest),
1947
- eq(c.snapshot, row[m.snapshot]),
1948
- eq(c.version, intent.version),
1949
- eq(c.consumed, false),
1950
- sql`${c.failedAttempts} < ${c.attemptLimit}`,
1951
- ),
1952
- );
1953
- const written = yield* stepUpLocator(mapping, input.digest, false);
1954
-
1955
- if (
1956
- written === undefined ||
1957
- written[m.version] !== intent.version ||
1958
- written[m.snapshot] !== row[m.snapshot] ||
1959
- written[m.failedAttempts] !== row[m.failedAttempts] + 1
1960
- )
1961
- return yield* unavailable();
1962
-
1963
- return receipt;
1964
- }),
1965
- ),
1966
- complete: <A>(
1967
- plan: Parameters<SessionStepUpPersistence<Claims>["complete"]>[0],
1968
- prepare: PrepareSessionCommit<SessionInspection<Claims>, A>,
1969
- ) =>
1970
- owned(
1971
- database,
1972
- options,
1973
- mapping.isConstraintConflict,
1974
- Effect.gen(function* () {
1975
- const tx = yield* CurrentSessionSql;
1976
- const journal = yield* CurrentCommitJournal;
1977
-
1978
- yield* validateStepUpPlan(plan);
2010
+ if (replacement._tag === "Stateful") {
2011
+ if (mapping.source.kind !== "Stateful") return yield* SessionStepUpInvalid.make({});
1979
2012
 
1980
- // Lock the entire combined vector before source/intent, including added factors.
1981
- const current = yield* stepUpAuthority(
1982
- mapping,
1983
- plan.evidence.revision,
1984
- options.locking,
1985
- );
1986
-
1987
- const stored = yield* readSqlStepUp(mapping, plan.intent.digest, options.locking);
2013
+ const s = mapping.source.session,
2014
+ row = stored.source.row;
1988
2015
 
1989
2016
  if (
1990
- (yield* encodeStepUpIntent(stored.intent)) !==
1991
- (yield* encodeStepUpIntent(plan.intent))
2017
+ row[s.digest] !== replacement.expectedDigest ||
2018
+ row[s.version] !== replacement.expectedRowVersion
1992
2019
  )
1993
- return yield* SessionStepUpInvalid.make({});
2020
+ return yield* SessionConflict.make({});
1994
2021
 
1995
- const base = yield* assessAuthentication(plan.evidence, current.requirement).pipe(
1996
- Effect.mapError(() => stale()),
2022
+ const version = yield* allocate(
2023
+ options.mode,
2024
+ s.allocateVersion,
2025
+ s.allocateVersionSync,
1997
2026
  );
1998
2027
 
1999
- const profile = yield* assessAuthentication(
2000
- plan.evidence,
2001
- stored.intent.requirement,
2002
- ).pipe(Effect.mapError(() => stale()));
2028
+ const next: StatefulSessionRecord<Claims> = {
2029
+ ...replacement.inspection.session,
2030
+ provenance: replacement.inspection.provenance,
2031
+ credentialVersion: replacement.inspection.credentialVersion,
2032
+ digest: replacement.nextDigest,
2033
+ version,
2034
+ };
2003
2035
 
2004
- if (!base.satisfied || !profile.satisfied) return yield* stale();
2036
+ rotation = {
2037
+ ...s.encodeRotation(next),
2038
+ ...updateValues([
2039
+ [s.version, version],
2040
+ [s.digest, next.digest],
2041
+ [s.credentialVersion, next.credentialVersion],
2042
+ [s.authenticatedAt, s.encodeInstant(next.assurance.authenticatedAt)],
2043
+ [s.issuedAt, s.encodeInstant(next.issuedAt)],
2044
+ [s.expiresAt, s.encodeInstant(next.expiresAt)],
2045
+ [s.absoluteExpiresAt, s.encodeInstant(next.absoluteExpiresAt)],
2046
+ ]),
2047
+ };
2048
+ }
2005
2049
 
2006
- const replacement = plan.replacement,
2007
- now = yield* freshNow;
2050
+ const completionVersion = yield* allocate(
2051
+ options.mode,
2052
+ m.allocateVersion,
2053
+ m.allocateVersionSync,
2054
+ );
2008
2055
 
2009
- if (
2010
- DateTime.toEpochMillis(replacement.inspection.session.expiresAt) >
2011
- DateTime.toEpochMillis(stored.intent.sourceAbsoluteExpiresAt) ||
2012
- DateTime.toEpochMillis(now) <
2013
- DateTime.toEpochMillis(replacement.inspection.session.issuedAt) ||
2014
- DateTime.toEpochMillis(now) >=
2015
- Math.min(
2016
- DateTime.toEpochMillis(replacement.inspection.session.expiresAt),
2017
- DateTime.toEpochMillis(stored.intent.expiresAt),
2018
- )
2019
- )
2020
- return yield* SessionStepUpInvalid.make({});
2021
- let rotation: any;
2056
+ if (completionVersion === stored.intent.version) return yield* unavailable();
2022
2057
 
2023
- if (replacement._tag === "Stateful") {
2024
- if (mapping.source.kind !== "Stateful") return yield* SessionStepUpInvalid.make({});
2058
+ const completionSnapshot = yield* encodeStepUpIntent({
2059
+ ...stored.intent,
2060
+ version: completionVersion,
2061
+ });
2025
2062
 
2026
- const s = mapping.source.session,
2027
- row = stored.source.row;
2063
+ const receipt = prepare(replacement.inspection, journal);
2028
2064
 
2029
- if (
2030
- row[s.digest] !== replacement.expectedDigest ||
2031
- row[s.version] !== replacement.expectedRowVersion
2032
- )
2033
- return yield* SessionConflict.make({});
2065
+ if (replacement._tag === "Stateful" && mapping.source.kind === "Stateful") {
2066
+ const s = mapping.source.session,
2067
+ sc = sessionColumns(mapping.source);
2034
2068
 
2035
- const version = yield* allocate(
2036
- options.mode,
2037
- s.allocateVersion,
2038
- s.allocateVersionSync,
2069
+ yield* tx
2070
+ .update(s.table)
2071
+ .set(rotation)
2072
+ .where(
2073
+ and(
2074
+ eq(sc.sessionId, stored.source.nativeSessionId),
2075
+ eq(sc.digest, replacement.expectedDigest),
2076
+ eq(sc.version, replacement.expectedRowVersion),
2077
+ ),
2039
2078
  );
2040
2079
 
2041
- const next: StatefulSessionRecord<Claims> = {
2042
- ...replacement.inspection.session,
2043
- provenance: replacement.inspection.provenance,
2044
- credentialVersion: replacement.inspection.credentialVersion,
2045
- digest: replacement.nextDigest,
2046
- version,
2047
- };
2048
-
2049
- rotation = {
2050
- ...s.encodeRotation(next),
2051
- ...updateValues([
2052
- [s.version, version],
2053
- [s.digest, next.digest],
2054
- [s.credentialVersion, next.credentialVersion],
2055
- [s.authenticatedAt, s.encodeInstant(next.assurance.authenticatedAt)],
2056
- [s.issuedAt, s.encodeInstant(next.issuedAt)],
2057
- [s.expiresAt, s.encodeInstant(next.expiresAt)],
2058
- [s.absoluteExpiresAt, s.encodeInstant(next.absoluteExpiresAt)],
2059
- ]),
2060
- };
2061
- }
2062
-
2063
- const completionVersion = yield* allocate(
2064
- options.mode,
2065
- m.allocateVersion,
2066
- m.allocateVersionSync,
2067
- );
2068
-
2069
- if (completionVersion === stored.intent.version) return yield* unavailable();
2070
-
2071
- const completionSnapshot = yield* encodeStepUpIntent({
2072
- ...stored.intent,
2073
- version: completionVersion,
2074
- });
2075
-
2076
- const receipt = prepare(replacement.inspection, journal);
2077
-
2078
- if (replacement._tag === "Stateful" && mapping.source.kind === "Stateful") {
2079
- const s = mapping.source.session,
2080
- sc = sessionColumns(mapping.source);
2080
+ const written = (yield* tx
2081
+ .select()
2082
+ .from(s.table)
2083
+ .where(eq(sc.sessionId, stored.source.nativeSessionId))
2084
+ .limit(1))[0];
2081
2085
 
2082
- yield* tx
2083
- .update(s.table)
2084
- .set(rotation)
2085
- .where(
2086
- and(
2087
- eq(sc.sessionId, stored.source.nativeSessionId),
2088
- eq(sc.digest, replacement.expectedDigest),
2089
- eq(sc.version, replacement.expectedRowVersion),
2090
- ),
2091
- );
2086
+ if (written === undefined || written[s.version] !== rotation[s.version])
2087
+ return yield* unavailable();
2088
+ const decoded = yield* s.decode(written);
2092
2089
 
2093
- const written = (yield* tx
2094
- .select()
2095
- .from(s.table)
2096
- .where(eq(sc.sessionId, stored.source.nativeSessionId))
2097
- .limit(1))[0];
2090
+ if (decoded.version !== rotation[s.version] || !stepUpRotationMatches(plan, decoded))
2091
+ return yield* unavailable();
2092
+ } else if (
2093
+ replacement._tag === "StateAssistedSigned" &&
2094
+ mapping.source.kind === "StateAssistedSigned"
2095
+ ) {
2096
+ const t = mapping.source.tombstone;
2098
2097
 
2099
- if (written === undefined || written[s.version] !== rotation[s.version])
2100
- return yield* unavailable();
2101
- const decoded = yield* s.decode(written);
2098
+ const owner = column(t.table, t.subjectId),
2099
+ id = column(t.table, t.sessionId),
2100
+ expiry = column(t.table, t.absoluteExpiresAt);
2102
2101
 
2103
- if (
2104
- decoded.version !== rotation[s.version] ||
2105
- !stepUpRotationMatches(plan, decoded)
2106
- )
2107
- return yield* unavailable();
2108
- } else if (
2109
- replacement._tag === "StateAssistedSigned" &&
2110
- mapping.source.kind === "StateAssistedSigned"
2111
- ) {
2112
- const t = mapping.source.tombstone;
2113
-
2114
- const owner = column(t.table, t.subjectId),
2115
- id = column(t.table, t.sessionId),
2116
- expiry = column(t.table, t.absoluteExpiresAt);
2117
-
2118
- yield* tx
2119
- .delete(t.table)
2120
- .where(
2121
- and(
2122
- eq(owner, stored.nativeSubjectId),
2123
- eq(id, stored.source.nativeSessionId),
2124
- lte(expiry, t.encodeInstant(yield* freshNow)),
2125
- ),
2126
- );
2127
-
2128
- const values = t.encodeInsert({
2129
- subjectId: stored.nativeSubjectId,
2130
- sessionId: stored.source.nativeSessionId,
2131
- absoluteExpiresAt: replacement.tombstoneUntil,
2132
- });
2102
+ yield* tx
2103
+ .delete(t.table)
2104
+ .where(
2105
+ and(
2106
+ eq(owner, stored.nativeSubjectId),
2107
+ eq(id, stored.source.nativeSessionId),
2108
+ lte(expiry, t.encodeInstant(yield* freshNow)),
2109
+ ),
2110
+ );
2133
2111
 
2134
- yield* tx.insert(t.table).values(values).pipe(normalizeMutation(mapping));
2112
+ const values = t.encodeInsert({
2113
+ subjectId: stored.nativeSubjectId,
2114
+ sessionId: stored.source.nativeSessionId,
2115
+ absoluteExpiresAt: replacement.tombstoneUntil,
2116
+ });
2135
2117
 
2136
- const inserted = (yield* tx
2137
- .select()
2138
- .from(t.table)
2139
- .where(
2140
- and(
2141
- eq(owner, stored.nativeSubjectId),
2142
- eq(id, stored.source.nativeSessionId),
2143
- eq(expiry, t.encodeInstant(replacement.tombstoneUntil)),
2144
- ),
2145
- )
2146
- .limit(1))[0];
2118
+ yield* tx.insert(t.table).values(values).pipe(normalizeMutation(mapping));
2147
2119
 
2148
- if (inserted === undefined) return yield* unavailable();
2149
- }
2150
- yield* tx
2151
- .update(m.table)
2152
- .set(
2153
- updateValues([
2154
- [m.consumed, true],
2155
- [m.version, completionVersion],
2156
- [m.snapshot, completionSnapshot],
2157
- ]),
2158
- )
2120
+ const inserted = (yield* tx
2121
+ .select()
2122
+ .from(t.table)
2159
2123
  .where(
2160
2124
  and(
2161
- eq(c.digest, stored.intent.digest),
2162
- eq(c.version, stored.intent.version),
2163
- eq(c.snapshot, stored.row[m.snapshot]),
2164
- eq(c.consumed, false),
2165
- eq(c.failedAttempts, stored.row[m.failedAttempts]),
2125
+ eq(owner, stored.nativeSubjectId),
2126
+ eq(id, stored.source.nativeSessionId),
2127
+ eq(expiry, t.encodeInstant(replacement.tombstoneUntil)),
2166
2128
  ),
2167
- );
2168
- const completed = yield* stepUpLocator(mapping, stored.intent.digest, false);
2129
+ )
2130
+ .limit(1))[0];
2169
2131
 
2170
- if (
2171
- completed === undefined ||
2172
- !completed[m.consumed] ||
2173
- completed[m.version] !== completionVersion ||
2174
- completed[m.snapshot] !== completionSnapshot
2132
+ if (inserted === undefined) return yield* unavailable();
2133
+ }
2134
+ yield* tx
2135
+ .update(m.table)
2136
+ .set(
2137
+ updateValues([
2138
+ [m.consumed, true],
2139
+ [m.version, completionVersion],
2140
+ [m.snapshot, completionSnapshot],
2141
+ ]),
2175
2142
  )
2176
- return yield* unavailable();
2177
- const commitNow = yield* freshNow;
2143
+ .where(
2144
+ and(
2145
+ eq(c.digest, stored.intent.digest),
2146
+ eq(c.version, stored.intent.version),
2147
+ eq(c.snapshot, stored.row[m.snapshot]),
2148
+ eq(c.consumed, false),
2149
+ eq(c.failedAttempts, stored.row[m.failedAttempts]),
2150
+ ),
2151
+ );
2152
+ const completed = yield* stepUpLocator(mapping, stored.intent.digest, false);
2153
+
2154
+ if (
2155
+ completed === undefined ||
2156
+ !completed[m.consumed] ||
2157
+ completed[m.version] !== completionVersion ||
2158
+ completed[m.snapshot] !== completionSnapshot
2159
+ )
2160
+ return yield* unavailable();
2161
+ const commitNow = yield* freshNow;
2162
+
2163
+ if (
2164
+ !stepUpIntentLive(stored.intent, mapping.source.kind, commitNow) ||
2165
+ DateTime.toEpochMillis(commitNow) >=
2166
+ DateTime.toEpochMillis(replacement.inspection.session.expiresAt) ||
2167
+ !(yield* assessAuthentication(plan.evidence, current.requirement).pipe(
2168
+ Effect.mapError(() => stale()),
2169
+ )).satisfied ||
2170
+ !(yield* assessAuthentication(plan.evidence, stored.intent.requirement).pipe(
2171
+ Effect.mapError(() => stale()),
2172
+ )).satisfied
2173
+ )
2174
+ return yield* stale();
2178
2175
 
2179
- if (
2180
- !stepUpIntentLive(stored.intent, mapping.source.kind, commitNow) ||
2181
- DateTime.toEpochMillis(commitNow) >=
2182
- DateTime.toEpochMillis(replacement.inspection.session.expiresAt) ||
2183
- !(yield* assessAuthentication(plan.evidence, current.requirement).pipe(
2184
- Effect.mapError(() => stale()),
2185
- )).satisfied ||
2186
- !(yield* assessAuthentication(plan.evidence, stored.intent.requirement).pipe(
2187
- Effect.mapError(() => stale()),
2188
- )).satisfied
2189
- )
2190
- return yield* stale();
2176
+ return receipt;
2177
+ }),
2178
+ ),
2179
+ };
2191
2180
 
2192
- return receipt;
2193
- }),
2194
- ),
2195
- };
2196
-
2197
- return {
2198
- create: (input, now, prepare) =>
2199
- service.create(input, now, prepare).pipe(Effect.provideService(LifecycleHooks, hooks)),
2200
- context: (input) =>
2201
- service.context(input).pipe(Effect.provideService(LifecycleHooks, hooks)),
2202
- read: (input) => service.read(input).pipe(Effect.provideService(LifecycleHooks, hooks)),
2203
- reject: (input, prepare) =>
2204
- service.reject(input, prepare).pipe(Effect.provideService(LifecycleHooks, hooks)),
2205
- complete: (plan, prepare) =>
2206
- service.complete(plan, prepare).pipe(Effect.provideService(LifecycleHooks, hooks)),
2207
- } satisfies SessionStepUpPersistence<Claims>;
2208
- });
2181
+ return {
2182
+ create: (input, now, prepare) =>
2183
+ service.create(input, now, prepare).pipe(Effect.provideService(LifecycleHooks, hooks)),
2184
+ context: (input) => service.context(input).pipe(Effect.provideService(LifecycleHooks, hooks)),
2185
+ read: (input) => service.read(input).pipe(Effect.provideService(LifecycleHooks, hooks)),
2186
+ reject: (input, prepare) =>
2187
+ service.reject(input, prepare).pipe(Effect.provideService(LifecycleHooks, hooks)),
2188
+ complete: (plan, prepare) =>
2189
+ service.complete(plan, prepare).pipe(Effect.provideService(LifecycleHooks, hooks)),
2190
+ } satisfies SessionStepUpPersistence<Claims>;
2191
+ });
2209
2192
 
2210
2193
  return {
2211
2194
  makeSqlAuthenticationAuthority,