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

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 (130) hide show
  1. package/README.md +11 -7
  2. package/dist/Adapter.d.mts +1 -1
  3. package/dist/Adapter.mjs +8 -8
  4. package/dist/OAuthPersistence.mjs +10 -5
  5. package/dist/OAuthPersistence.mjs.map +1 -1
  6. package/dist/internal/configuration.d.mts +49 -13
  7. package/dist/internal/configuration.mjs.map +1 -1
  8. package/dist/internal/email-native.mjs +10 -2
  9. package/dist/internal/email-native.mjs.map +1 -1
  10. package/dist/internal/email-policy.mjs +3 -2
  11. package/dist/internal/email-policy.mjs.map +1 -1
  12. package/dist/internal/models/oauth-model.d.mts +4 -2
  13. package/dist/internal/models/oauth-model.mjs.map +1 -1
  14. package/dist/internal/models/password-model.d.mts +2 -1
  15. package/dist/internal/models/password-model.mjs.map +1 -1
  16. package/dist/internal/oauth/native-accounts.d.mts +1 -1
  17. package/dist/internal/oauth/native-accounts.mjs +6 -6
  18. package/dist/internal/oauth/native-accounts.mjs.map +1 -1
  19. package/dist/internal/oauth/native-connected.d.mts +1 -1
  20. package/dist/internal/oauth/native-connected.mjs +1 -1
  21. package/dist/internal/oauth/native-registration.mjs +19 -8
  22. package/dist/internal/oauth/native-registration.mjs.map +1 -1
  23. package/dist/internal/oauth/native-sign-in.d.mts +1 -1
  24. package/dist/internal/oauth/native-sign-in.mjs +1 -1
  25. package/dist/internal/oauth/native-state.mjs +21 -11
  26. package/dist/internal/oauth/native-state.mjs.map +1 -1
  27. package/dist/internal/oauth/state.d.mts +1 -1
  28. package/dist/internal/oauth/state.mjs +1 -1
  29. package/dist/internal/oauth-managed.mjs +182 -0
  30. package/dist/internal/oauth-managed.mjs.map +1 -0
  31. package/dist/internal/passkey-managed.mjs +5 -3
  32. package/dist/internal/passkey-managed.mjs.map +1 -1
  33. package/dist/internal/passkey-native-registration.mjs +1 -1
  34. package/dist/internal/passkey-native-state.mjs +33 -23
  35. package/dist/internal/passkey-native-state.mjs.map +1 -1
  36. package/dist/internal/passkey-native-write.mjs +3 -3
  37. package/dist/internal/passkey-native-write.mjs.map +1 -1
  38. package/dist/internal/passkey-native.d.mts +15 -7
  39. package/dist/internal/passkey-native.mjs +2 -1
  40. package/dist/internal/passkey-native.mjs.map +1 -1
  41. package/dist/internal/passkey-policy.mjs +9 -7
  42. package/dist/internal/passkey-policy.mjs.map +1 -1
  43. package/dist/internal/password-native.mjs +22 -5
  44. package/dist/internal/password-native.mjs.map +1 -1
  45. package/dist/internal/password-policy.mjs +7 -6
  46. package/dist/internal/password-policy.mjs.map +1 -1
  47. package/dist/internal/password-registration-native.d.mts +1 -1
  48. package/dist/internal/persistence.d.mts +3 -0
  49. package/dist/internal/persistence.mjs +99 -42
  50. package/dist/internal/persistence.mjs.map +1 -1
  51. package/dist/internal/phone-native.mjs +1 -1
  52. package/dist/internal/phone-native.mjs.map +1 -1
  53. package/dist/internal/registration.mjs +17 -8
  54. package/dist/internal/registration.mjs.map +1 -1
  55. package/dist/internal/session-native-authority.mjs +8 -6
  56. package/dist/internal/session-native-authority.mjs.map +1 -1
  57. package/dist/internal/session-native-login.d.mts +2 -2
  58. package/dist/internal/session-native-login.mjs +4 -2
  59. package/dist/internal/session-native-login.mjs.map +1 -1
  60. package/dist/internal/session-native-pending-read.mjs +26 -20
  61. package/dist/internal/session-native-pending-read.mjs.map +1 -1
  62. package/dist/internal/session-native-record.mjs +6 -4
  63. package/dist/internal/session-native-record.mjs.map +1 -1
  64. package/dist/internal/session-native-state.mjs +24 -8
  65. package/dist/internal/session-native-state.mjs.map +1 -1
  66. package/dist/internal/session-native-stateful.d.mts +2 -2
  67. package/dist/internal/session-native-stateful.mjs +67 -38
  68. package/dist/internal/session-native-stateful.mjs.map +1 -1
  69. package/dist/internal/session-native-step-up.d.mts +2 -2
  70. package/dist/internal/session-native-step-up.mjs +17 -19
  71. package/dist/internal/session-native-step-up.mjs.map +1 -1
  72. package/dist/internal/session-native-validity.mjs +3 -1
  73. package/dist/internal/session-native-validity.mjs.map +1 -1
  74. package/dist/internal/sql-authority.mjs +5 -1
  75. package/dist/internal/sql-authority.mjs.map +1 -1
  76. package/dist/internal/sql-storage-validation.mjs +2 -1
  77. package/dist/internal/sql-storage-validation.mjs.map +1 -1
  78. package/dist/internal/sql-table.d.mts +1 -0
  79. package/dist/internal/sql-table.mjs.map +1 -1
  80. package/dist/internal/sqlite-policy-guard.mjs +23 -0
  81. package/dist/internal/sqlite-policy-guard.mjs.map +1 -0
  82. package/dist/internal/step-up-state.mjs +19 -6
  83. package/dist/internal/step-up-state.mjs.map +1 -1
  84. package/dist/internal/storage-mapping.d.mts +9 -5
  85. package/dist/internal/storage-mapping.mjs +17 -5
  86. package/dist/internal/storage-mapping.mjs.map +1 -1
  87. package/dist/internal/storage-tables.d.mts +237 -0
  88. package/dist/internal/storage-tables.mjs +68 -0
  89. package/dist/internal/storage-tables.mjs.map +1 -1
  90. package/dist/internal/storage-validation.d.mts +3 -1
  91. package/dist/internal/storage-validation.mjs +70 -17
  92. package/dist/internal/storage-validation.mjs.map +1 -1
  93. package/dist/internal/totp-native.mjs +1 -1
  94. package/package.json +1 -1
  95. package/src/OAuthPersistence.ts +7 -2
  96. package/src/internal/configuration.ts +72 -15
  97. package/src/internal/email-native.ts +16 -6
  98. package/src/internal/email-policy.ts +12 -3
  99. package/src/internal/models/oauth-model.ts +7 -1
  100. package/src/internal/models/password-model.ts +2 -1
  101. package/src/internal/oauth/native-accounts.ts +3 -3
  102. package/src/internal/oauth/native-registration.ts +17 -6
  103. package/src/internal/oauth/native-state.ts +38 -18
  104. package/src/internal/oauth-managed.ts +277 -0
  105. package/src/internal/passkey-managed.ts +8 -2
  106. package/src/internal/passkey-native-state.ts +59 -52
  107. package/src/internal/passkey-native-write.ts +2 -2
  108. package/src/internal/passkey-native.ts +4 -2
  109. package/src/internal/passkey-policy.ts +13 -9
  110. package/src/internal/password-native.ts +45 -11
  111. package/src/internal/password-policy.ts +16 -8
  112. package/src/internal/persistence.ts +360 -204
  113. package/src/internal/phone-native.ts +1 -1
  114. package/src/internal/registration.ts +53 -40
  115. package/src/internal/session-native-authority.ts +26 -8
  116. package/src/internal/session-native-login.ts +9 -4
  117. package/src/internal/session-native-pending-read.ts +46 -33
  118. package/src/internal/session-native-record.ts +6 -4
  119. package/src/internal/session-native-state.ts +32 -10
  120. package/src/internal/session-native-stateful.ts +147 -52
  121. package/src/internal/session-native-step-up.ts +31 -24
  122. package/src/internal/session-native-validity.ts +3 -1
  123. package/src/internal/sql-authority.ts +5 -1
  124. package/src/internal/sql-storage-validation.ts +53 -46
  125. package/src/internal/sql-table.ts +1 -0
  126. package/src/internal/sqlite-policy-guard.ts +34 -0
  127. package/src/internal/step-up-state.ts +43 -6
  128. package/src/internal/storage-mapping.ts +40 -4
  129. package/src/internal/storage-tables.ts +76 -0
  130. package/src/internal/storage-validation.ts +127 -27
@@ -1,36 +1,42 @@
1
1
  import { exactSqlText } from "./sql-change.mjs";
2
- import { makeNativeSessionAuthorityState } from "./session-native-state.mjs";
2
+ import { makeNativeSessionAuthorityState, sessionInvariant, sessionUnavailable } from "./session-native-state.mjs";
3
+ import { canJoinTextColumns } from "./storage-validation.mjs";
3
4
  import { makeNativeSessionPending } from "./session-native-pending.mjs";
4
5
  import { Effect } from "effect";
5
6
  //#region src/internal/session-native-pending-read.ts
6
- /** Both semantic pending ports use the same kind-bound physical snapshot.
7
- * Distinct native ID encodings use their own bound reads; joined rows still
8
- * validate each decoded owner before returning current policy. */
7
+ /** Both ports re-read the pending row with current authority in one snapshot.
8
+ * Discovery only selects the independently bound owner for incompatible IDs. */
9
9
  const makeNativeSessionPendingReader = Effect.fnUntraced(function* (tables, mapping, batch = false) {
10
10
  const state = yield* makeNativeSessionAuthorityState(tables, mapping, batch);
11
11
  const pending = yield* makeNativeSessionPending(tables, mapping, batch);
12
+ const joinedIds = yield* canJoinTextColumns([
13
+ pending.table.unencodedTextColumn?.(pending.p.subjectId),
14
+ state.subject.unencodedTextColumn?.(mapping.subject.id),
15
+ state.credential.unencodedTextColumn?.(mapping.credential.subjectId)
16
+ ]).pipe(Effect.mapError(sessionUnavailable));
12
17
  return {
13
18
  state,
14
19
  pending,
15
20
  read: Effect.fnUntraced(function* (kind, digest) {
16
21
  const { sql, now } = state, { p, table } = pending;
17
- if (table.unencodedTextColumn?.(p.subjectId) !== void 0 && state.subject.unencodedTextColumn?.(mapping.subject.id) !== void 0 && state.credential.unencodedTextColumn?.(mapping.credential.subjectId) !== void 0) {
18
- const pt = table.as("authentication_pending"), st = state.joinedSubject, ct = state.joinedCredential, s = mapping.subject, c = mapping.credential;
19
- const rows = yield* sql`select ${pt.fields("p_")}, ${st.fields("s_")}, ${ct.fields("c_")}, ${now} as engine_now from ${pt.name} join ${st.name} on ${exactSqlText(sql, st.column(s.id), pt.column(p.subjectId))} and ${state.activeSubject(st)} left join ${ct.name} on ${exactSqlText(sql, ct.column(c.subjectId), pt.column(p.subjectId))} and ${state.activeCredential(ct)} where ${pending.predicate(pt, kind, digest)} limit 65`;
20
- if (rows[0] === void 0) return void 0;
21
- const record = yield* pending.decode(kind, pt.decode(rows[0], "p_"));
22
- const owner = yield* mapping.subjectId.toNative(record.subjectId);
23
- const authority = yield* state.decode(record.subjectId, owner, st.decode(rows[0], "s_"), rows.map((row) => ct.decode(row, "c_")), rows[0].engine_now);
24
- return authority === void 0 ? void 0 : {
25
- record,
26
- authority
27
- };
22
+ let discoveredSubjectId;
23
+ let nativeId;
24
+ if (!joinedIds) {
25
+ const selected = yield* pending.read(kind, digest);
26
+ if (selected === void 0) return void 0;
27
+ discoveredSubjectId = selected.record.subjectId;
28
+ nativeId = yield* mapping.subjectId.toNative(discoveredSubjectId);
28
29
  }
29
- const selected = yield* pending.read(kind, digest);
30
- if (selected === void 0) return void 0;
31
- const authority = yield* state.read(selected.record.subjectId);
32
- return authority === void 0 || authority.now >= selected.record.expiresAtMillis ? void 0 : {
33
- record: selected.record,
30
+ const pt = table.as("authentication_pending"), st = state.joinedSubject, ct = state.joinedCredential, s = mapping.subject, c = mapping.credential;
31
+ const bindOwner = (table, key) => joinedIds ? exactSqlText(sql, table.column(key), pt.column(p.subjectId)) : state.id(table, key, nativeId);
32
+ const rows = yield* sql`select ${pt.fields("p_")}, ${st.fields("s_")}, ${ct.fields("c_")}, ${now} as engine_now from ${pt.name} join ${st.name} on ${bindOwner(st, s.id)} and ${state.activeSubject(st)} left join ${ct.name} on ${bindOwner(ct, c.subjectId)} and ${state.activeCredential(ct)} where ${pending.predicate(pt, kind, digest)}${joinedIds ? sql`` : sql` and ${bindOwner(pt, p.subjectId)}`} limit 65`;
33
+ if (rows[0] === void 0) return void 0;
34
+ const record = yield* pending.decode(kind, pt.decode(rows[0], "p_"));
35
+ sessionInvariant(joinedIds || record.subjectId === discoveredSubjectId);
36
+ const owner = yield* mapping.subjectId.toNative(record.subjectId);
37
+ const authority = yield* state.decode(record.subjectId, owner, rows[0], rows.map((row) => ct.decode(row, "c_")), rows[0].engine_now);
38
+ return authority === void 0 || authority.now >= record.expiresAtMillis ? void 0 : {
39
+ record,
34
40
  authority
35
41
  };
36
42
  })
@@ -1 +1 @@
1
- {"version":3,"file":"session-native-pending-read.mjs","names":[],"sources":["../../src/internal/session-native-pending-read.ts"],"sourcesContent":["import type { TokenDigest } from \"@yielded/auth/Schema\";\nimport type { PendingAuthenticationKind } from \"@yielded/auth/Sessions\";\nimport { Effect } from \"effect\";\n\nimport type { NativeSqlTables } from \"./native-sql-table\";\nimport { makeNativeSessionPending } from \"./session-native-pending\";\nimport {\n makeNativeSessionAuthorityState,\n type NativeSessionAuthorityMapping,\n} from \"./session-native-state\";\nimport { exactSqlText } from \"./sql-change\";\n\n/** Both semantic pending ports use the same kind-bound physical snapshot.\n * Distinct native ID encodings use their own bound reads; joined rows still\n * validate each decoded owner before returning current policy. */\nexport const makeNativeSessionPendingReader = Effect.fnUntraced(function* (\n tables: NativeSqlTables,\n mapping: NativeSessionAuthorityMapping & Parameters<typeof makeNativeSessionPending>[1],\n batch = false,\n) {\n const state = yield* makeNativeSessionAuthorityState(tables, mapping, batch);\n const pending = yield* makeNativeSessionPending(tables, mapping, batch);\n\n const read = Effect.fnUntraced(function* (kind: PendingAuthenticationKind, digest: TokenDigest) {\n const { sql, now } = state,\n { p, table } = pending;\n\n const compatible =\n table.unencodedTextColumn?.(p.subjectId) !== undefined &&\n state.subject.unencodedTextColumn?.(mapping.subject.id) !== undefined &&\n state.credential.unencodedTextColumn?.(mapping.credential.subjectId) !== undefined;\n\n if (compatible) {\n const pt = table.as(\"authentication_pending\"),\n st = state.joinedSubject,\n ct = state.joinedCredential,\n s = mapping.subject,\n c = mapping.credential;\n\n const rows =\n yield* sql`select ${pt.fields(\"p_\")}, ${st.fields(\"s_\")}, ${ct.fields(\"c_\")}, ${now} as engine_now from ${pt.name} join ${st.name} on ${exactSqlText(sql, st.column(s.id), pt.column(p.subjectId))} and ${state.activeSubject(st)} left join ${ct.name} on ${exactSqlText(sql, ct.column(c.subjectId), pt.column(p.subjectId))} and ${state.activeCredential(ct)} where ${pending.predicate(pt, kind, digest)} limit 65`;\n\n if (rows[0] === undefined) return undefined;\n const record = yield* pending.decode(kind, pt.decode(rows[0], \"p_\"));\n const owner = yield* mapping.subjectId.toNative(record.subjectId);\n\n const authority = yield* state.decode(\n record.subjectId,\n owner,\n st.decode(rows[0], \"s_\"),\n rows.map((row) => ct.decode(row, \"c_\")),\n rows[0].engine_now,\n );\n\n return authority === undefined ? undefined : { record, authority };\n }\n const selected = yield* pending.read(kind, digest);\n\n if (selected === undefined) return undefined;\n const authority = yield* state.read(selected.record.subjectId);\n\n return authority === undefined || authority.now >= selected.record.expiresAtMillis\n ? undefined\n : { record: selected.record, authority };\n });\n\n return { state, pending, read };\n});\n"],"mappings":";;;;;;;;AAeA,MAAa,iCAAiC,OAAO,WAAW,WAC9D,QACA,SACA,QAAQ,OACR;CACA,MAAM,QAAQ,OAAO,gCAAgC,QAAQ,SAAS,KAAK;CAC3E,MAAM,UAAU,OAAO,yBAAyB,QAAQ,SAAS,KAAK;CA6CtE,OAAO;EAAE;EAAO;EAAS,MA3CZ,OAAO,WAAW,WAAW,MAAiC,QAAqB;GAC9F,MAAM,EAAE,KAAK,QAAQ,OACnB,EAAE,GAAG,UAAU;GAOjB,IAJE,MAAM,sBAAsB,EAAE,SAAS,MAAM,KAAA,KAC7C,MAAM,QAAQ,sBAAsB,QAAQ,QAAQ,EAAE,MAAM,KAAA,KAC5D,MAAM,WAAW,sBAAsB,QAAQ,WAAW,SAAS,MAAM,KAAA,GAE3D;IACd,MAAM,KAAK,MAAM,GAAG,wBAAwB,GAC1C,KAAK,MAAM,eACX,KAAK,MAAM,kBACX,IAAI,QAAQ,SACZ,IAAI,QAAQ;IAEd,MAAM,OACJ,OAAO,GAAG,UAAU,GAAG,OAAO,IAAI,EAAE,IAAI,GAAG,OAAO,IAAI,EAAE,IAAI,GAAG,OAAO,IAAI,EAAE,IAAI,IAAI,sBAAsB,GAAG,KAAK,QAAQ,GAAG,KAAK,MAAM,aAAa,KAAK,GAAG,OAAO,EAAE,EAAE,GAAG,GAAG,OAAO,EAAE,SAAS,CAAC,EAAE,OAAO,MAAM,cAAc,EAAE,EAAE,aAAa,GAAG,KAAK,MAAM,aAAa,KAAK,GAAG,OAAO,EAAE,SAAS,GAAG,GAAG,OAAO,EAAE,SAAS,CAAC,EAAE,OAAO,MAAM,iBAAiB,EAAE,EAAE,SAAS,QAAQ,UAAU,IAAI,MAAM,MAAM,EAAE;IAEhZ,IAAI,KAAK,OAAO,KAAA,GAAW,OAAO,KAAA;IAClC,MAAM,SAAS,OAAO,QAAQ,OAAO,MAAM,GAAG,OAAO,KAAK,IAAI,IAAI,CAAC;IACnE,MAAM,QAAQ,OAAO,QAAQ,UAAU,SAAS,OAAO,SAAS;IAEhE,MAAM,YAAY,OAAO,MAAM,OAC7B,OAAO,WACP,OACA,GAAG,OAAO,KAAK,IAAI,IAAI,GACvB,KAAK,KAAK,QAAQ,GAAG,OAAO,KAAK,IAAI,CAAC,GACtC,KAAK,EAAE,CAAC,UACV;IAEA,OAAO,cAAc,KAAA,IAAY,KAAA,IAAY;KAAE;KAAQ;IAAU;GACnE;GACA,MAAM,WAAW,OAAO,QAAQ,KAAK,MAAM,MAAM;GAEjD,IAAI,aAAa,KAAA,GAAW,OAAO,KAAA;GACnC,MAAM,YAAY,OAAO,MAAM,KAAK,SAAS,OAAO,SAAS;GAE7D,OAAO,cAAc,KAAA,KAAa,UAAU,OAAO,SAAS,OAAO,kBAC/D,KAAA,IACA;IAAE,QAAQ,SAAS;IAAQ;GAAU;EAC3C,CAE4B;CAAE;AAChC,CAAC"}
1
+ {"version":3,"file":"session-native-pending-read.mjs","names":[],"sources":["../../src/internal/session-native-pending-read.ts"],"sourcesContent":["import type { SubjectId, TokenDigest } from \"@yielded/auth/Schema\";\nimport type { PendingAuthenticationKind } from \"@yielded/auth/Sessions\";\nimport { Effect } from \"effect\";\n\nimport type { NativeSqlTables } from \"./native-sql-table\";\nimport { makeNativeSessionPending } from \"./session-native-pending\";\nimport {\n makeNativeSessionAuthorityState,\n sessionInvariant,\n sessionUnavailable,\n type NativeSessionAuthorityMapping,\n} from \"./session-native-state\";\nimport { exactSqlText } from \"./sql-change\";\nimport { canJoinTextColumns } from \"./storage-validation\";\n\n/** Both ports re-read the pending row with current authority in one snapshot.\n * Discovery only selects the independently bound owner for incompatible IDs. */\nexport const makeNativeSessionPendingReader = Effect.fnUntraced(function* (\n tables: NativeSqlTables,\n mapping: NativeSessionAuthorityMapping & Parameters<typeof makeNativeSessionPending>[1],\n batch = false,\n) {\n const state = yield* makeNativeSessionAuthorityState(tables, mapping, batch);\n const pending = yield* makeNativeSessionPending(tables, mapping, batch);\n\n const joinedIds = yield* canJoinTextColumns([\n pending.table.unencodedTextColumn?.(pending.p.subjectId),\n state.subject.unencodedTextColumn?.(mapping.subject.id),\n state.credential.unencodedTextColumn?.(mapping.credential.subjectId),\n ]).pipe(Effect.mapError(sessionUnavailable));\n\n const read = Effect.fnUntraced(function* (kind: PendingAuthenticationKind, digest: TokenDigest) {\n const { sql, now } = state,\n { p, table } = pending;\n\n let discoveredSubjectId: SubjectId | undefined;\n let nativeId: unknown;\n\n if (!joinedIds) {\n const selected = yield* pending.read(kind, digest);\n\n if (selected === undefined) return undefined;\n discoveredSubjectId = selected.record.subjectId;\n nativeId = yield* mapping.subjectId.toNative(discoveredSubjectId);\n }\n\n const pt = table.as(\"authentication_pending\"),\n st = state.joinedSubject,\n ct = state.joinedCredential,\n s = mapping.subject,\n c = mapping.credential;\n\n const bindOwner = (table: typeof pt, key: string) =>\n joinedIds\n ? exactSqlText(sql, table.column(key), pt.column(p.subjectId))\n : state.id(table, key, nativeId);\n\n const rows =\n yield* sql`select ${pt.fields(\"p_\")}, ${st.fields(\"s_\")}, ${ct.fields(\"c_\")}, ${now} as engine_now from ${pt.name} join ${st.name} on ${bindOwner(st, s.id)} and ${state.activeSubject(st)} left join ${ct.name} on ${bindOwner(ct, c.subjectId)} and ${state.activeCredential(ct)} where ${pending.predicate(pt, kind, digest)}${joinedIds ? sql`` : sql` and ${bindOwner(pt, p.subjectId)}`} limit 65`;\n\n if (rows[0] === undefined) return undefined;\n const record = yield* pending.decode(kind, pt.decode(rows[0], \"p_\"));\n\n sessionInvariant(joinedIds || record.subjectId === discoveredSubjectId);\n const owner = yield* mapping.subjectId.toNative(record.subjectId);\n\n const authority = yield* state.decode(\n record.subjectId,\n owner,\n rows[0],\n rows.map((row) => ct.decode(row, \"c_\")),\n rows[0].engine_now,\n );\n\n return authority === undefined || authority.now >= record.expiresAtMillis\n ? undefined\n : { record, authority };\n });\n\n return { state, pending, read };\n});\n"],"mappings":";;;;;;;;AAiBA,MAAa,iCAAiC,OAAO,WAAW,WAC9D,QACA,SACA,QAAQ,OACR;CACA,MAAM,QAAQ,OAAO,gCAAgC,QAAQ,SAAS,KAAK;CAC3E,MAAM,UAAU,OAAO,yBAAyB,QAAQ,SAAS,KAAK;CAEtE,MAAM,YAAY,OAAO,mBAAmB;EAC1C,QAAQ,MAAM,sBAAsB,QAAQ,EAAE,SAAS;EACvD,MAAM,QAAQ,sBAAsB,QAAQ,QAAQ,EAAE;EACtD,MAAM,WAAW,sBAAsB,QAAQ,WAAW,SAAS;CACrE,CAAC,CAAC,CAAC,KAAK,OAAO,SAAS,kBAAkB,CAAC;CAkD3C,OAAO;EAAE;EAAO;EAAS,MAhDZ,OAAO,WAAW,WAAW,MAAiC,QAAqB;GAC9F,MAAM,EAAE,KAAK,QAAQ,OACnB,EAAE,GAAG,UAAU;GAEjB,IAAI;GACJ,IAAI;GAEJ,IAAI,CAAC,WAAW;IACd,MAAM,WAAW,OAAO,QAAQ,KAAK,MAAM,MAAM;IAEjD,IAAI,aAAa,KAAA,GAAW,OAAO,KAAA;IACnC,sBAAsB,SAAS,OAAO;IACtC,WAAW,OAAO,QAAQ,UAAU,SAAS,mBAAmB;GAClE;GAEA,MAAM,KAAK,MAAM,GAAG,wBAAwB,GAC1C,KAAK,MAAM,eACX,KAAK,MAAM,kBACX,IAAI,QAAQ,SACZ,IAAI,QAAQ;GAEd,MAAM,aAAa,OAAkB,QACnC,YACI,aAAa,KAAK,MAAM,OAAO,GAAG,GAAG,GAAG,OAAO,EAAE,SAAS,CAAC,IAC3D,MAAM,GAAG,OAAO,KAAK,QAAQ;GAEnC,MAAM,OACJ,OAAO,GAAG,UAAU,GAAG,OAAO,IAAI,EAAE,IAAI,GAAG,OAAO,IAAI,EAAE,IAAI,GAAG,OAAO,IAAI,EAAE,IAAI,IAAI,sBAAsB,GAAG,KAAK,QAAQ,GAAG,KAAK,MAAM,UAAU,IAAI,EAAE,EAAE,EAAE,OAAO,MAAM,cAAc,EAAE,EAAE,aAAa,GAAG,KAAK,MAAM,UAAU,IAAI,EAAE,SAAS,EAAE,OAAO,MAAM,iBAAiB,EAAE,EAAE,SAAS,QAAQ,UAAU,IAAI,MAAM,MAAM,IAAI,YAAY,GAAG,KAAK,GAAG,QAAQ,UAAU,IAAI,EAAE,SAAS,IAAI;GAEhY,IAAI,KAAK,OAAO,KAAA,GAAW,OAAO,KAAA;GAClC,MAAM,SAAS,OAAO,QAAQ,OAAO,MAAM,GAAG,OAAO,KAAK,IAAI,IAAI,CAAC;GAEnE,iBAAiB,aAAa,OAAO,cAAc,mBAAmB;GACtE,MAAM,QAAQ,OAAO,QAAQ,UAAU,SAAS,OAAO,SAAS;GAEhE,MAAM,YAAY,OAAO,MAAM,OAC7B,OAAO,WACP,OACA,KAAK,IACL,KAAK,KAAK,QAAQ,GAAG,OAAO,KAAK,IAAI,CAAC,GACtC,KAAK,EAAE,CAAC,UACV;GAEA,OAAO,cAAc,KAAA,KAAa,UAAU,OAAO,OAAO,kBACtD,KAAA,IACA;IAAE;IAAQ;GAAU;EAC1B,CAE4B;CAAE;AAChC,CAAC"}
@@ -3,6 +3,7 @@ import { sessionInvariant } from "./session-native-state.mjs";
3
3
  import { DateTime, Effect, Schema } from "effect";
4
4
  import { SecurityRevision, SessionAuthenticationProvenance, SessionCredentialVersion, SessionId, SessionMetadata } from "@yielded/auth/Sessions";
5
5
  import { SqlClient } from "effect/sql/SqlClient";
6
+ import { AuthenticationClock } from "@yielded/auth/Operations";
6
7
  import { TokenDigest } from "@yielded/auth/Schema";
7
8
  //#region src/internal/session-native-record.ts
8
9
  const metadataJson = Schema.encodeSync(Schema.fromJsonString(SessionMetadata));
@@ -16,16 +17,17 @@ const makeConditionalSqlInsert = Effect.fnUntraced(function* () {
16
17
  return sql`insert into ${table.name} (${sql.join(", ", false)(fields.map(([key]) => table.columnName(key)))}) select ${sql.join(", ", false)(fields.map(([key, value]) => table.value(key, value)))} where ${condition}`;
17
18
  };
18
19
  });
19
- /** Security columns identify the row and own its live time bounds. In particular
20
- * issuedAt may be assigned by UPDATE's database clock without rewriting a JSON
21
- * copy of prepared metadata. Claims/provenance decoding remains consumer-owned. */
20
+ /** Security columns own live time bounds and commit-time issuedAt adjustments,
21
+ * independently of prepared JSON metadata. Claims/provenance decoding remains consumer-owned. */
22
22
  const makeNativeSessionRecords = Effect.fnUntraced(function* (tables, mapping) {
23
23
  const sql = (yield* SqlClient).withoutTransforms();
24
+ const { futureToleranceMillis } = yield* AuthenticationClock;
24
25
  const s = mapping.session, table = tables(s.table), now = tables.expression(mapping.clock.engineNowMillis);
25
26
  const exact = (t, key, value) => exactSqlText(sql, t.column(key), t.value(key, value));
26
27
  const millis = (t, key) => tables.expression(mapping.clock.toMillis(t.column(key)));
27
28
  const live = (t) => sql.and([
28
- sql`${millis(t, s.issuedAt)} <= ${now}`,
29
+ sql`${millis(t, s.issuedAt)} <= ${now} + ${futureToleranceMillis}`,
30
+ sql`${millis(t, s.issuedAt)} < ${millis(t, s.expiresAt)}`,
29
31
  sql`${millis(t, s.expiresAt)} > ${now}`,
30
32
  sql`${millis(t, s.absoluteExpiresAt)} >= ${millis(t, s.expiresAt)}`
31
33
  ]);
@@ -1 +1 @@
1
- {"version":3,"file":"session-native-record.mjs","names":[],"sources":["../../src/internal/session-native-record.ts"],"sourcesContent":["import { TokenDigest } from \"@yielded/auth/Schema\";\nimport {\n SessionId,\n SessionMetadata,\n SessionAuthenticationProvenance,\n SessionCredentialVersion,\n SecurityRevision,\n type StatefulSessionRecord,\n} from \"@yielded/auth/Sessions\";\nimport { DateTime, Effect, Schema } from \"effect\";\nimport { SqlClient } from \"effect/sql/SqlClient\";\nimport type { Fragment } from \"effect/sql/Statement\";\n\nimport type { SubjectIdCodec } from \"./models/common\";\nimport type { StatefulSessionTables, SessionExecution } from \"./models/session-model\";\nimport type { NativeSqlTables, SqlTable } from \"./native-sql-table\";\nimport { sessionInvariant } from \"./session-native-state\";\nimport { exactSqlText } from \"./sql-change\";\nimport type { AnyTableModel } from \"./table-model\";\n\nexport type NativeSessionRecordMapping<Claims> = StatefulSessionTables<\n Claims,\n AnyTableModel,\n unknown,\n unknown\n> &\n Pick<SessionExecution, \"clock\"> & { readonly subjectId: SubjectIdCodec<unknown> };\n\nconst metadataJson = Schema.encodeSync(Schema.fromJsonString(SessionMetadata));\nconst provenanceJson = Schema.encodeSync(Schema.fromJsonString(SessionAuthenticationProvenance));\n\nexport const sameSessionRecord = <Claims>(\n left: StatefulSessionRecord<Claims>,\n right: StatefulSessionRecord<Claims>,\n) =>\n left.digest === right.digest &&\n left.credentialVersion === right.credentialVersion &&\n metadataJson(left) === metadataJson(right) &&\n provenanceJson(left.provenance) === provenanceJson(right.provenance);\n\nexport const makeConditionalSqlInsert = Effect.fnUntraced(function* () {\n const sql = (yield* SqlClient).withoutTransforms();\n\n return (table: SqlTable, values: Readonly<Record<string, unknown>>, condition: Fragment) => {\n const fields = Object.entries(values).filter(([, value]) => value !== undefined);\n\n sessionInvariant(fields.length > 0);\n\n return sql`insert into ${table.name} (${sql.join(\", \", false)(fields.map(([key]) => table.columnName(key)))}) select ${sql.join(\", \", false)(fields.map(([key, value]) => table.value(key, value)))} where ${condition}`;\n };\n});\n\n/** Security columns identify the row and own its live time bounds. In particular\n * issuedAt may be assigned by UPDATE's database clock without rewriting a JSON\n * copy of prepared metadata. Claims/provenance decoding remains consumer-owned. */\nexport const makeNativeSessionRecords = Effect.fnUntraced(function* <Claims>(\n tables: NativeSqlTables,\n mapping: NativeSessionRecordMapping<Claims>,\n) {\n const sql = (yield* SqlClient).withoutTransforms();\n\n const s = mapping.session,\n table = tables(s.table),\n now = tables.expression(mapping.clock.engineNowMillis);\n\n const exact = (t: SqlTable, key: string, value: unknown) =>\n exactSqlText(sql, t.column(key), t.value(key, value));\n\n const millis = (t: SqlTable, key: string) =>\n tables.expression(mapping.clock.toMillis(t.column(key)));\n\n const live = (t: SqlTable) =>\n sql.and([\n sql`${millis(t, s.issuedAt)} <= ${now}`,\n sql`${millis(t, s.expiresAt)} > ${now}`,\n sql`${millis(t, s.absoluteExpiresAt)} >= ${millis(t, s.expiresAt)}`,\n ]);\n\n const decode = Effect.fnUntraced(function* (row: Record<string, unknown>) {\n const decoded = yield* s.decode(row);\n const subjectId = yield* mapping.subjectId.toSubject(row[s.subjectId]);\n const sessionId = yield* mapping.sessionId.toSession(row[s.sessionId]);\n const digest = yield* Schema.decodeUnknownEffect(TokenDigest)(row[s.digest]);\n const revision = yield* Schema.decodeUnknownEffect(SecurityRevision)(row[s.securityRevision]);\n const issuedAt = DateTime.makeUnsafe(mapping.clock.decodeInstant(row[s.issuedAt]));\n const expiresAt = mapping.clock.decodeInstant(row[s.expiresAt]);\n const absolute = mapping.clock.decodeInstant(row[s.absoluteExpiresAt]);\n\n sessionInvariant(\n decoded.subjectId === subjectId &&\n decoded.sessionId === sessionId &&\n decoded.digest === digest &&\n decoded.securityRevision === revision &&\n DateTime.toEpochMillis(decoded.expiresAt) === expiresAt &&\n DateTime.toEpochMillis(decoded.absoluteExpiresAt) === absolute,\n );\n yield* Schema.decodeEffect(SessionId)(sessionId);\n yield* Schema.decodeEffect(SessionCredentialVersion)(decoded.credentialVersion);\n\n return { ...decoded, issuedAt };\n });\n\n const owner = Effect.fnUntraced(function* (\n record: Pick<StatefulSessionRecord<Claims>, \"subjectId\" | \"sessionId\" | \"digest\">,\n ) {\n const nativeSubject = yield* mapping.subjectId.toNative(record.subjectId),\n nativeSession = yield* mapping.sessionId.toNative(record.sessionId);\n\n return sql.and([\n sql`${table.column(s.subjectId)} = ${table.value(s.subjectId, nativeSubject)}`,\n sql`${table.column(s.sessionId)} = ${table.value(s.sessionId, nativeSession)}`,\n exact(table, s.digest, record.digest),\n ]);\n });\n\n // These are the semantic values supplied by the session encoder for this\n // write, not an observed whole-row snapshot or unrelated application columns.\n const encodedCondition = (values: Readonly<Record<string, unknown>>) =>\n sql.and(\n Object.entries(values)\n .filter(([, value]) => value !== undefined)\n .map(([key, value]) =>\n value === null\n ? sql`${table.column(key)} is null`\n : typeof value === \"string\"\n ? exact(table, key, value)\n : sql`${table.column(key)} = ${table.value(key, value)}`,\n ),\n );\n\n const read = Effect.fnUntraced(function* (digest: TokenDigest) {\n const rows =\n yield* sql`select ${table.fields(\"session_\")} from ${table.name} where ${exact(table, s.digest, digest)} and ${live(table)} limit 2`;\n\n sessionInvariant(rows.length <= 1);\n\n return rows[0] === undefined ? undefined : yield* decode(table.decode(rows[0], \"session_\"));\n });\n\n return { sql, s, table, now, exact, millis, live, decode, owner, read, encodedCondition };\n});\n"],"mappings":";;;;;;;AA4BA,MAAM,eAAe,OAAO,WAAW,OAAO,eAAe,eAAe,CAAC;AAC7E,MAAM,iBAAiB,OAAO,WAAW,OAAO,eAAe,+BAA+B,CAAC;AAE/F,MAAa,qBACX,MACA,UAEA,KAAK,WAAW,MAAM,UACtB,KAAK,sBAAsB,MAAM,qBACjC,aAAa,IAAI,MAAM,aAAa,KAAK,KACzC,eAAe,KAAK,UAAU,MAAM,eAAe,MAAM,UAAU;AAErE,MAAa,2BAA2B,OAAO,WAAW,aAAa;CACrE,MAAM,OAAO,OAAO,UAAA,CAAW,kBAAkB;CAEjD,QAAQ,OAAiB,QAA2C,cAAwB;EAC1F,MAAM,SAAS,OAAO,QAAQ,MAAM,CAAC,CAAC,QAAQ,GAAG,WAAW,UAAU,KAAA,CAAS;EAE/E,iBAAiB,OAAO,SAAS,CAAC;EAElC,OAAO,GAAG,eAAe,MAAM,KAAK,IAAI,IAAI,KAAK,MAAM,KAAK,CAAC,CAAC,OAAO,KAAK,CAAC,SAAS,MAAM,WAAW,GAAG,CAAC,CAAC,EAAE,WAAW,IAAI,KAAK,MAAM,KAAK,CAAC,CAAC,OAAO,KAAK,CAAC,KAAK,WAAW,MAAM,MAAM,KAAK,KAAK,CAAC,CAAC,EAAE,SAAS;CAC/M;AACF,CAAC;;;;AAKD,MAAa,2BAA2B,OAAO,WAAW,WACxD,QACA,SACA;CACA,MAAM,OAAO,OAAO,UAAA,CAAW,kBAAkB;CAEjD,MAAM,IAAI,QAAQ,SAChB,QAAQ,OAAO,EAAE,KAAK,GACtB,MAAM,OAAO,WAAW,QAAQ,MAAM,eAAe;CAEvD,MAAM,SAAS,GAAa,KAAa,UACvC,aAAa,KAAK,EAAE,OAAO,GAAG,GAAG,EAAE,MAAM,KAAK,KAAK,CAAC;CAEtD,MAAM,UAAU,GAAa,QAC3B,OAAO,WAAW,QAAQ,MAAM,SAAS,EAAE,OAAO,GAAG,CAAC,CAAC;CAEzD,MAAM,QAAQ,MACZ,IAAI,IAAI;EACN,GAAG,GAAG,OAAO,GAAG,EAAE,QAAQ,EAAE,MAAM;EAClC,GAAG,GAAG,OAAO,GAAG,EAAE,SAAS,EAAE,KAAK;EAClC,GAAG,GAAG,OAAO,GAAG,EAAE,iBAAiB,EAAE,MAAM,OAAO,GAAG,EAAE,SAAS;CAClE,CAAC;CAEH,MAAM,SAAS,OAAO,WAAW,WAAW,KAA8B;EACxE,MAAM,UAAU,OAAO,EAAE,OAAO,GAAG;EACnC,MAAM,YAAY,OAAO,QAAQ,UAAU,UAAU,IAAI,EAAE,UAAU;EACrE,MAAM,YAAY,OAAO,QAAQ,UAAU,UAAU,IAAI,EAAE,UAAU;EACrE,MAAM,SAAS,OAAO,OAAO,oBAAoB,WAAW,CAAC,CAAC,IAAI,EAAE,OAAO;EAC3E,MAAM,WAAW,OAAO,OAAO,oBAAoB,gBAAgB,CAAC,CAAC,IAAI,EAAE,iBAAiB;EAC5F,MAAM,WAAW,SAAS,WAAW,QAAQ,MAAM,cAAc,IAAI,EAAE,SAAS,CAAC;EACjF,MAAM,YAAY,QAAQ,MAAM,cAAc,IAAI,EAAE,UAAU;EAC9D,MAAM,WAAW,QAAQ,MAAM,cAAc,IAAI,EAAE,kBAAkB;EAErE,iBACE,QAAQ,cAAc,aACpB,QAAQ,cAAc,aACtB,QAAQ,WAAW,UACnB,QAAQ,qBAAqB,YAC7B,SAAS,cAAc,QAAQ,SAAS,MAAM,aAC9C,SAAS,cAAc,QAAQ,iBAAiB,MAAM,QAC1D;EACA,OAAO,OAAO,aAAa,SAAS,CAAC,CAAC,SAAS;EAC/C,OAAO,OAAO,aAAa,wBAAwB,CAAC,CAAC,QAAQ,iBAAiB;EAE9E,OAAO;GAAE,GAAG;GAAS;EAAS;CAChC,CAAC;CAED,MAAM,QAAQ,OAAO,WAAW,WAC9B,QACA;EACA,MAAM,gBAAgB,OAAO,QAAQ,UAAU,SAAS,OAAO,SAAS,GACtE,gBAAgB,OAAO,QAAQ,UAAU,SAAS,OAAO,SAAS;EAEpE,OAAO,IAAI,IAAI;GACb,GAAG,GAAG,MAAM,OAAO,EAAE,SAAS,EAAE,KAAK,MAAM,MAAM,EAAE,WAAW,aAAa;GAC3E,GAAG,GAAG,MAAM,OAAO,EAAE,SAAS,EAAE,KAAK,MAAM,MAAM,EAAE,WAAW,aAAa;GAC3E,MAAM,OAAO,EAAE,QAAQ,OAAO,MAAM;EACtC,CAAC;CACH,CAAC;CAID,MAAM,oBAAoB,WACxB,IAAI,IACF,OAAO,QAAQ,MAAM,CAAC,CACnB,QAAQ,GAAG,WAAW,UAAU,KAAA,CAAS,CAAC,CAC1C,KAAK,CAAC,KAAK,WACV,UAAU,OACN,GAAG,GAAG,MAAM,OAAO,GAAG,EAAE,YACxB,OAAO,UAAU,WACf,MAAM,OAAO,KAAK,KAAK,IACvB,GAAG,GAAG,MAAM,OAAO,GAAG,EAAE,KAAK,MAAM,MAAM,KAAK,KAAK,GAC3D,CACJ;CAWF,OAAO;EAAE;EAAK;EAAG;EAAO;EAAK;EAAO;EAAQ;EAAM;EAAQ;EAAO,MATpD,OAAO,WAAW,WAAW,QAAqB;GAC7D,MAAM,OACJ,OAAO,GAAG,UAAU,MAAM,OAAO,UAAU,EAAE,QAAQ,MAAM,KAAK,SAAS,MAAM,OAAO,EAAE,QAAQ,MAAM,EAAE,OAAO,KAAK,KAAK,EAAE;GAE7H,iBAAiB,KAAK,UAAU,CAAC;GAEjC,OAAO,KAAK,OAAO,KAAA,IAAY,KAAA,IAAY,OAAO,OAAO,MAAM,OAAO,KAAK,IAAI,UAAU,CAAC;EAC5F,CAEoE;EAAG;CAAiB;AAC1F,CAAC"}
1
+ {"version":3,"file":"session-native-record.mjs","names":[],"sources":["../../src/internal/session-native-record.ts"],"sourcesContent":["import { AuthenticationClock } from \"@yielded/auth/Operations\";\nimport { TokenDigest } from \"@yielded/auth/Schema\";\nimport {\n SessionId,\n SessionMetadata,\n SessionAuthenticationProvenance,\n SessionCredentialVersion,\n SecurityRevision,\n type StatefulSessionRecord,\n} from \"@yielded/auth/Sessions\";\nimport { DateTime, Effect, Schema } from \"effect\";\nimport { SqlClient } from \"effect/sql/SqlClient\";\nimport type { Fragment } from \"effect/sql/Statement\";\n\nimport type { SubjectIdCodec } from \"./models/common\";\nimport type { StatefulSessionTables, SessionExecution } from \"./models/session-model\";\nimport type { NativeSqlTables, SqlTable } from \"./native-sql-table\";\nimport { sessionInvariant } from \"./session-native-state\";\nimport { exactSqlText } from \"./sql-change\";\nimport type { AnyTableModel } from \"./table-model\";\n\nexport type NativeSessionRecordMapping<Claims> = StatefulSessionTables<\n Claims,\n AnyTableModel,\n unknown,\n unknown\n> &\n Pick<SessionExecution, \"clock\"> & { readonly subjectId: SubjectIdCodec<unknown> };\n\nconst metadataJson = Schema.encodeSync(Schema.fromJsonString(SessionMetadata));\nconst provenanceJson = Schema.encodeSync(Schema.fromJsonString(SessionAuthenticationProvenance));\n\nexport const sameSessionRecord = <Claims>(\n left: StatefulSessionRecord<Claims>,\n right: StatefulSessionRecord<Claims>,\n) =>\n left.digest === right.digest &&\n left.credentialVersion === right.credentialVersion &&\n metadataJson(left) === metadataJson(right) &&\n provenanceJson(left.provenance) === provenanceJson(right.provenance);\n\nexport const makeConditionalSqlInsert = Effect.fnUntraced(function* () {\n const sql = (yield* SqlClient).withoutTransforms();\n\n return (table: SqlTable, values: Readonly<Record<string, unknown>>, condition: Fragment) => {\n const fields = Object.entries(values).filter(([, value]) => value !== undefined);\n\n sessionInvariant(fields.length > 0);\n\n return sql`insert into ${table.name} (${sql.join(\", \", false)(fields.map(([key]) => table.columnName(key)))}) select ${sql.join(\", \", false)(fields.map(([key, value]) => table.value(key, value)))} where ${condition}`;\n };\n});\n\n/** Security columns own live time bounds and commit-time issuedAt adjustments,\n * independently of prepared JSON metadata. Claims/provenance decoding remains consumer-owned. */\nexport const makeNativeSessionRecords = Effect.fnUntraced(function* <Claims>(\n tables: NativeSqlTables,\n mapping: NativeSessionRecordMapping<Claims>,\n) {\n const sql = (yield* SqlClient).withoutTransforms();\n const { futureToleranceMillis } = yield* AuthenticationClock;\n\n const s = mapping.session,\n table = tables(s.table),\n now = tables.expression(mapping.clock.engineNowMillis);\n\n const exact = (t: SqlTable, key: string, value: unknown) =>\n exactSqlText(sql, t.column(key), t.value(key, value));\n\n const millis = (t: SqlTable, key: string) =>\n tables.expression(mapping.clock.toMillis(t.column(key)));\n\n const live = (t: SqlTable) =>\n sql.and([\n sql`${millis(t, s.issuedAt)} <= ${now} + ${futureToleranceMillis}`,\n sql`${millis(t, s.issuedAt)} < ${millis(t, s.expiresAt)}`,\n sql`${millis(t, s.expiresAt)} > ${now}`,\n sql`${millis(t, s.absoluteExpiresAt)} >= ${millis(t, s.expiresAt)}`,\n ]);\n\n const decode = Effect.fnUntraced(function* (row: Record<string, unknown>) {\n const decoded = yield* s.decode(row);\n const subjectId = yield* mapping.subjectId.toSubject(row[s.subjectId]);\n const sessionId = yield* mapping.sessionId.toSession(row[s.sessionId]);\n const digest = yield* Schema.decodeUnknownEffect(TokenDigest)(row[s.digest]);\n const revision = yield* Schema.decodeUnknownEffect(SecurityRevision)(row[s.securityRevision]);\n const issuedAt = DateTime.makeUnsafe(mapping.clock.decodeInstant(row[s.issuedAt]));\n const expiresAt = mapping.clock.decodeInstant(row[s.expiresAt]);\n const absolute = mapping.clock.decodeInstant(row[s.absoluteExpiresAt]);\n\n sessionInvariant(\n decoded.subjectId === subjectId &&\n decoded.sessionId === sessionId &&\n decoded.digest === digest &&\n decoded.securityRevision === revision &&\n DateTime.toEpochMillis(decoded.expiresAt) === expiresAt &&\n DateTime.toEpochMillis(decoded.absoluteExpiresAt) === absolute,\n );\n yield* Schema.decodeEffect(SessionId)(sessionId);\n yield* Schema.decodeEffect(SessionCredentialVersion)(decoded.credentialVersion);\n\n return { ...decoded, issuedAt };\n });\n\n const owner = Effect.fnUntraced(function* (\n record: Pick<StatefulSessionRecord<Claims>, \"subjectId\" | \"sessionId\" | \"digest\">,\n ) {\n const nativeSubject = yield* mapping.subjectId.toNative(record.subjectId),\n nativeSession = yield* mapping.sessionId.toNative(record.sessionId);\n\n return sql.and([\n sql`${table.column(s.subjectId)} = ${table.value(s.subjectId, nativeSubject)}`,\n sql`${table.column(s.sessionId)} = ${table.value(s.sessionId, nativeSession)}`,\n exact(table, s.digest, record.digest),\n ]);\n });\n\n // These are the semantic values supplied by the session encoder for this\n // write, not an observed whole-row snapshot or unrelated application columns.\n const encodedCondition = (values: Readonly<Record<string, unknown>>) =>\n sql.and(\n Object.entries(values)\n .filter(([, value]) => value !== undefined)\n .map(([key, value]) =>\n value === null\n ? sql`${table.column(key)} is null`\n : typeof value === \"string\"\n ? exact(table, key, value)\n : sql`${table.column(key)} = ${table.value(key, value)}`,\n ),\n );\n\n const read = Effect.fnUntraced(function* (digest: TokenDigest) {\n const rows =\n yield* sql`select ${table.fields(\"session_\")} from ${table.name} where ${exact(table, s.digest, digest)} and ${live(table)} limit 2`;\n\n sessionInvariant(rows.length <= 1);\n\n return rows[0] === undefined ? undefined : yield* decode(table.decode(rows[0], \"session_\"));\n });\n\n return { sql, s, table, now, exact, millis, live, decode, owner, read, encodedCondition };\n});\n"],"mappings":"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"}
@@ -1,9 +1,13 @@
1
1
  import { PersistenceMappingError } from "./mapping-error.mjs";
2
+ import { nativeSqlAlias } from "./native-sql-table.mjs";
2
3
  import { anySqlCondition, exactSqlText } from "./sql-change.mjs";
4
+ import { guardSqlitePolicy } from "./sqlite-policy-guard.mjs";
3
5
  import { Cause, DateTime, Effect, Schema } from "effect";
6
+ import { fragment, parameter } from "effect/sql/Statement";
4
7
  import { reportAuthFailure } from "@yielded/auth/Persistence";
5
8
  import { AuthenticationRevision, PendingAuthenticationInvalid, SessionConflict, SessionInvalid, SessionStepUpInvalid, SessionUnavailable, StaleAuthentication, assessAuthentication } from "@yielded/auth/Sessions";
6
9
  import { SqlClient } from "effect/sql/SqlClient";
10
+ import { AuthenticationClock } from "@yielded/auth/Operations";
7
11
  //#region src/internal/session-native-state.ts
8
12
  const sessionUnavailable = () => SessionUnavailable.make({});
9
13
  const sessionInvariant = (condition) => {
@@ -22,6 +26,7 @@ const sameSessionRevision = (left, right) => left.subjectId === right.subjectId
22
26
  * precede every credential lock, including inside an application coordinator. */
23
27
  const makeNativeSessionAuthorityState = Effect.fnUntraced(function* (tables, mapping, batch = false) {
24
28
  const sql = (yield* SqlClient).withoutTransforms();
29
+ const clockPolicy = yield* AuthenticationClock;
25
30
  const s = mapping.subject, c = mapping.credential;
26
31
  if (batch) sessionInvariant(s.requirementColumns !== void 0);
27
32
  const subject = tables(s.table), credential = tables(c.table);
@@ -44,7 +49,8 @@ const makeNativeSessionAuthorityState = Effect.fnUntraced(function* (tables, map
44
49
  return id(table, c.status, c.activeStatusValue);
45
50
  };
46
51
  const requested = (table, ids) => anySqlCondition(ids.map((value) => exact(table, c.credentialId, value)));
47
- const decode = Effect.fnUntraced(function* (subjectId, native, row, factors, instant) {
52
+ const decode = Effect.fnUntraced(function* (subjectId, native, projected, factors, instant) {
53
+ const row = subject.decode(projected, "s_");
48
54
  sessionInvariant((yield* mapping.subjectId.toSubject(row[s.id])) === subjectId);
49
55
  if (!s.isActiveStatus(row[s.status])) return void 0;
50
56
  const credentials = [];
@@ -66,6 +72,10 @@ const makeNativeSessionAuthorityState = Effect.fnUntraced(function* (tables, map
66
72
  return {
67
73
  native,
68
74
  row,
75
+ policyValues: batch ? (s.requirementColumns ?? []).map((key) => ({
76
+ column: subject.selectedColumn(key),
77
+ value: fragment([parameter(projected[nativeSqlAlias("s_", subject.keys.indexOf(key))])])
78
+ })) : [],
69
79
  revision: yield* Schema.decodeUnknownEffect(AuthenticationRevision)({
70
80
  subjectId,
71
81
  securityRevision: row[s.securityRevision],
@@ -83,17 +93,22 @@ const makeNativeSessionAuthorityState = Effect.fnUntraced(function* (tables, map
83
93
  sessionInvariant(owners.length <= 1);
84
94
  if (owners[0] === void 0) return void 0;
85
95
  const factors = yield* sql`select ${credential.fields("c_")} from ${credential.name} where ${id(credential, c.subjectId, native)} and ${activeCredential(credential)} order by ${credential.column(c.credentialId)} limit 65 ${lock}`;
86
- return yield* decode(subjectId, native, subject.decode(owners[0], "s_"), factors.map((row) => credential.decode(row, "c_")), owners[0].engine_now);
96
+ return yield* decode(subjectId, native, owners[0], factors.map((row) => credential.decode(row, "c_")), owners[0].engine_now);
87
97
  }
88
98
  const rows = yield* sql`select ${joinedSubject.fields("s_")}, ${joinedCredential.fields("c_")}, ${now} as engine_now from ${joinedSubject.name} left join ${joinedCredential.name} on ${id(joinedCredential, c.subjectId, native)} and ${activeCredential(joinedCredential)} where ${id(joinedSubject, s.id, native)} and ${activeSubject(joinedSubject)} limit 65`;
89
99
  if (rows[0] === void 0) return void 0;
90
- return yield* decode(subjectId, native, joinedSubject.decode(rows[0], "s_"), rows.map((row) => joinedCredential.decode(row, "c_")), rows[0].engine_now);
100
+ return yield* decode(subjectId, native, rows[0], rows.map((row) => joinedCredential.decode(row, "c_")), rows[0].engine_now);
91
101
  });
92
- const policyCondition = (row, native) => {
93
- if (!batch) return sql`1 = 1`;
102
+ const guardPolicy = Effect.fnUntraced(function* (current) {
103
+ if (!batch) return;
94
104
  sessionInvariant(s.requirementColumns !== void 0);
95
- return sql`exists(select 1 from ${subject.name} where ${id(subject, s.id, native)} and ${sql.and(s.requirementColumns.map((key) => row[key] === null ? sql`${subject.column(key)} is null` : typeof row[key] === "string" ? exact(subject, key, row[key]) : id(subject, key, row[key])))})`;
96
- };
105
+ yield* guardSqlitePolicy({
106
+ name: "session-subject-policy",
107
+ table: subject.name,
108
+ owner: id(subject, s.id, current.native),
109
+ values: current.policyValues
110
+ });
111
+ });
97
112
  const authorityCondition = (revision, native) => sql.and([
98
113
  sql`exists(select 1 from ${subject.name} where ${id(subject, s.id, native)} and ${exact(subject, s.securityRevision, revision.securityRevision)} and ${activeSubject(subject)})`,
99
114
  sql`(select count(*) from ${credential.name} where ${id(credential, c.subjectId, native)} and ${activeCredential(credential)}) = ${revision.credentials.length}`,
@@ -101,6 +116,7 @@ const makeNativeSessionAuthorityState = Effect.fnUntraced(function* (tables, map
101
116
  ]);
102
117
  return {
103
118
  sql,
119
+ clockPolicy,
104
120
  subject,
105
121
  credential,
106
122
  joinedSubject,
@@ -116,7 +132,7 @@ const makeNativeSessionAuthorityState = Effect.fnUntraced(function* (tables, map
116
132
  decode,
117
133
  read,
118
134
  authorityCondition,
119
- policyCondition
135
+ guardPolicy
120
136
  };
121
137
  });
122
138
  /** Evaluate freshness at the database sample. The final write also enforces the
@@ -1 +1 @@
1
- {"version":3,"file":"session-native-state.mjs","names":[],"sources":["../../src/internal/session-native-state.ts"],"sourcesContent":["import { reportAuthFailure } from \"@yielded/auth/Persistence\";\nimport type { SubjectId } from \"@yielded/auth/Schema\";\nimport {\n type AuthenticationRequirement,\n AuthenticationRevision,\n SessionConflict,\n SessionInvalid,\n SessionUnavailable,\n SessionStepUpInvalid,\n PendingAuthenticationInvalid,\n StaleAuthentication,\n assessAuthentication,\n type AuthenticationEvidence,\n} from \"@yielded/auth/Sessions\";\nimport { Cause, Effect, Schema, DateTime } from \"effect\";\nimport { SqlClient } from \"effect/sql/SqlClient\";\nimport type { Fragment } from \"effect/sql/Statement\";\n\nimport { PersistenceMappingError } from \"./mapping-error\";\nimport type { SessionAuthorityTables, SessionExecution } from \"./models/session-model\";\nimport type { NativeSqlTables, SqlTable } from \"./native-sql-table\";\nimport { anySqlCondition, exactSqlText } from \"./sql-change\";\nimport type { AnyTableModel } from \"./table-model\";\n\nexport const sessionUnavailable = () => SessionUnavailable.make({});\n\nexport const sessionInvariant: (condition: unknown) => asserts condition = (condition) => {\n if (!condition)\n throw PersistenceMappingError.make({ operation: \"decode\", cause: \"Invalid session authority\" });\n};\n\nexport type SessionDomainFailure =\n | SessionConflict\n | SessionInvalid\n | SessionUnavailable\n | SessionStepUpInvalid\n | PendingAuthenticationInvalid\n | StaleAuthentication;\n\nconst isDomainFailure = (error: unknown): error is SessionDomainFailure =>\n Schema.is(SessionConflict)(error) ||\n Schema.is(SessionInvalid)(error) ||\n Schema.is(SessionUnavailable)(error) ||\n Schema.is(SessionStepUpInvalid)(error) ||\n Schema.is(PendingAuthenticationInvalid)(error) ||\n Schema.is(StaleAuthentication)(error);\n\n/** Only schema-checked domain failures cross the owner boundary; SQL/mapping\n * failures remain Unavailable. The executor preserves every mixed cause reason. */\nexport const sessionFailure = <E>(\n error: E,\n): Extract<E, SessionDomainFailure> | SessionUnavailable =>\n isDomainFailure(error) ? (error as Extract<E, SessionDomainFailure>) : sessionUnavailable();\n\nexport const normalizeSessionOperation = <A, E, R>(effect: Effect.Effect<A, E, R>) =>\n effect.pipe(\n Effect.tapError((error) =>\n isDomainFailure(error)\n ? Effect.void\n : reportAuthFailure(\"auth-persistence\", Cause.fail(error)),\n ),\n Effect.mapError(sessionFailure),\n );\n\nexport type NativeSessionAuthorityMapping = SessionAuthorityTables<\n AnyTableModel,\n AnyTableModel,\n unknown\n> &\n SessionExecution;\n\nexport interface NativeSessionAuthority {\n readonly native: unknown;\n readonly row: Record<string, unknown>;\n readonly revision: AuthenticationRevision;\n readonly requirement: AuthenticationRequirement;\n readonly now: number;\n}\n\nexport const sameSessionRevision = (left: AuthenticationRevision, right: AuthenticationRevision) =>\n left.subjectId === right.subjectId &&\n left.securityRevision === right.securityRevision &&\n left.credentials.length === right.credentials.length &&\n new Set(left.credentials.map((f) => f.credentialId)).size === left.credentials.length &&\n left.credentials.every((f) =>\n right.credentials.some(\n (other) => other.credentialId === f.credentialId && other.revision === f.revision,\n ),\n );\n\n/** Known-subject joins bind each physical ID column independently. Subject locks\n * precede every credential lock, including inside an application coordinator. */\nexport const makeNativeSessionAuthorityState = Effect.fnUntraced(function* (\n tables: NativeSqlTables,\n mapping: NativeSessionAuthorityMapping,\n batch = false,\n) {\n const sql = (yield* SqlClient).withoutTransforms();\n\n const s = mapping.subject,\n c = mapping.credential;\n\n if (batch) sessionInvariant(s.requirementColumns !== undefined);\n\n const subject = tables(s.table),\n credential = tables(c.table);\n\n const joinedSubject = subject.as(\"session_subject\"),\n joinedCredential = credential.as(\"session_factor\");\n\n const now = tables.expression(mapping.clock.engineNowMillis);\n const lock = sql.onDialectOrElse({ sqlite: () => sql``, orElse: () => sql`for update` });\n const nativeLocks = !batch && sql.onDialectOrElse({ sqlite: () => false, orElse: () => true });\n\n const exact = (table: SqlTable, key: string, value: unknown) =>\n exactSqlText(sql, table.column(key), table.value(key, value));\n\n const id = (table: SqlTable, key: string, value: unknown) =>\n sql`${table.column(key)} = ${table.value(key, value)}`;\n\n const activeSubject = (table: SqlTable) => id(table, s.status, s.activeStatusValue);\n\n const activeCredential = (table: SqlTable) => {\n if (c.status === undefined) return sql`1 = 1`;\n sessionInvariant(c.activeStatusValue !== undefined);\n\n return id(table, c.status, c.activeStatusValue);\n };\n\n const requested = (table: SqlTable, ids: ReadonlyArray<string>) =>\n anySqlCondition(ids.map((value) => exact(table, c.credentialId, value)));\n\n const decode = Effect.fnUntraced(function* (\n subjectId: SubjectId,\n native: unknown,\n row: Record<string, unknown>,\n factors: ReadonlyArray<Record<string, unknown>>,\n instant: unknown,\n ): Effect.fn.Return<\n NativeSessionAuthority | undefined,\n PersistenceMappingError | Schema.SchemaError\n > {\n sessionInvariant((yield* mapping.subjectId.toSubject(row[s.id])) === subjectId);\n if (!s.isActiveStatus(row[s.status])) return undefined;\n const credentials: { credentialId: string; revision: string }[] = [];\n\n for (const factor of factors) {\n if (factor[c.credentialId] === null || factor[c.credentialId] === undefined) continue;\n sessionInvariant((yield* mapping.subjectId.toSubject(factor[c.subjectId])) === subjectId);\n if (c.status !== undefined) sessionInvariant(c.isActiveStatus?.(factor[c.status]) === true);\n\n const value = yield* Schema.decodeUnknownEffect(\n Schema.Struct({ credentialId: Schema.String, revision: Schema.String }),\n )({ credentialId: factor[c.credentialId], revision: factor[c.revision] });\n\n sessionInvariant(!credentials.some((other) => other.credentialId === value.credentialId));\n credentials.push(value);\n }\n sessionInvariant(credentials.length <= 64);\n\n return {\n native,\n row,\n revision: yield* Schema.decodeUnknownEffect(AuthenticationRevision)({\n subjectId,\n securityRevision: row[s.securityRevision],\n credentials: credentials.sort((a, b) => a.credentialId.localeCompare(b.credentialId)),\n }),\n requirement: yield* s.decodeRequirement(row),\n now: yield* Schema.decodeEffect(Schema.Int)(Number(instant)),\n };\n });\n\n const read = Effect.fnUntraced(function* (subjectId: SubjectId, locking = false) {\n const native = yield* mapping.subjectId.toNative(subjectId);\n\n sessionInvariant((yield* mapping.subjectId.toSubject(native)) === subjectId);\n if (locking && nativeLocks) {\n const owners =\n yield* sql`select ${subject.fields(\"s_\")}, ${now} as engine_now from ${subject.name} where ${id(subject, s.id, native)} and ${activeSubject(subject)} limit 2 ${lock}`;\n\n sessionInvariant(owners.length <= 1);\n if (owners[0] === undefined) return undefined;\n\n const factors =\n yield* sql`select ${credential.fields(\"c_\")} from ${credential.name} where ${id(credential, c.subjectId, native)} and ${activeCredential(credential)} order by ${credential.column(c.credentialId)} limit 65 ${lock}`;\n\n return yield* decode(\n subjectId,\n native,\n subject.decode(owners[0], \"s_\"),\n factors.map((row) => credential.decode(row, \"c_\")),\n owners[0].engine_now,\n );\n }\n\n const rows =\n yield* sql`select ${joinedSubject.fields(\"s_\")}, ${joinedCredential.fields(\"c_\")}, ${now} as engine_now from ${joinedSubject.name} left join ${joinedCredential.name} on ${id(joinedCredential, c.subjectId, native)} and ${activeCredential(joinedCredential)} where ${id(joinedSubject, s.id, native)} and ${activeSubject(joinedSubject)} limit 65`;\n\n if (rows[0] === undefined) return undefined;\n\n return yield* decode(\n subjectId,\n native,\n joinedSubject.decode(rows[0], \"s_\"),\n rows.map((row) => joinedCredential.decode(row, \"c_\")),\n rows[0].engine_now,\n );\n });\n\n const policyCondition = (row: Record<string, unknown>, native: unknown): Fragment => {\n if (!batch) return sql`1 = 1`;\n sessionInvariant(s.requirementColumns !== undefined);\n\n return sql`exists(select 1 from ${subject.name} where ${id(subject, s.id, native)} and ${sql.and(s.requirementColumns.map((key) => (row[key] === null ? sql`${subject.column(key)} is null` : typeof row[key] === \"string\" ? exact(subject, key, row[key]) : id(subject, key, row[key]))))})`;\n };\n\n const authorityCondition = (revision: AuthenticationRevision, native: unknown): Fragment =>\n sql.and([\n sql`exists(select 1 from ${subject.name} where ${id(subject, s.id, native)} and ${exact(subject, s.securityRevision, revision.securityRevision)} and ${activeSubject(subject)})`,\n sql`(select count(*) from ${credential.name} where ${id(credential, c.subjectId, native)} and ${activeCredential(credential)}) = ${revision.credentials.length}`,\n ...revision.credentials.map(\n (factor) =>\n sql`exists(select 1 from ${credential.name} where ${id(credential, c.subjectId, native)} and ${exact(credential, c.credentialId, factor.credentialId)} and ${exact(credential, c.revision, factor.revision)} and ${activeCredential(credential)})`,\n ),\n ]);\n\n return {\n sql,\n subject,\n credential,\n joinedSubject,\n joinedCredential,\n now,\n lock,\n nativeLocks,\n exact,\n id,\n activeSubject,\n activeCredential,\n requested,\n decode,\n read,\n authorityCondition,\n policyCondition,\n };\n});\n\n/** Evaluate freshness at the database sample. The final write also enforces the\n * earliest future boundary at which the same evidence would cease to satisfy. */\nexport const assessSessionAt = Effect.fnUntraced(function* (\n evidence: AuthenticationEvidence,\n requirement: AuthenticationRequirement,\n now: number,\n) {\n const result = yield* assessAuthentication(evidence, requirement, DateTime.makeUnsafe(now));\n\n return result;\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"session-native-state.mjs","names":[],"sources":["../../src/internal/session-native-state.ts"],"sourcesContent":["import { AuthenticationClock } from \"@yielded/auth/Operations\";\nimport { reportAuthFailure } from \"@yielded/auth/Persistence\";\nimport type { SubjectId } from \"@yielded/auth/Schema\";\nimport {\n type AuthenticationRequirement,\n AuthenticationRevision,\n SessionConflict,\n SessionInvalid,\n SessionUnavailable,\n SessionStepUpInvalid,\n PendingAuthenticationInvalid,\n StaleAuthentication,\n assessAuthentication,\n type AuthenticationEvidence,\n} from \"@yielded/auth/Sessions\";\nimport { Cause, Effect, Schema, DateTime } from \"effect\";\nimport { SqlClient } from \"effect/sql/SqlClient\";\nimport { fragment, parameter, type Fragment } from \"effect/sql/Statement\";\n\nimport { PersistenceMappingError } from \"./mapping-error\";\nimport type { SessionAuthorityTables, SessionExecution } from \"./models/session-model\";\nimport { nativeSqlAlias, type NativeSqlTables, type SqlTable } from \"./native-sql-table\";\nimport { anySqlCondition, exactSqlText } from \"./sql-change\";\nimport { guardSqlitePolicy, type SqlitePolicyValue } from \"./sqlite-policy-guard\";\nimport type { AnyTableModel } from \"./table-model\";\n\nexport const sessionUnavailable = () => SessionUnavailable.make({});\n\nexport const sessionInvariant: (condition: unknown) => asserts condition = (condition) => {\n if (!condition)\n throw PersistenceMappingError.make({ operation: \"decode\", cause: \"Invalid session authority\" });\n};\n\nexport type SessionDomainFailure =\n | SessionConflict\n | SessionInvalid\n | SessionUnavailable\n | SessionStepUpInvalid\n | PendingAuthenticationInvalid\n | StaleAuthentication;\n\nconst isDomainFailure = (error: unknown): error is SessionDomainFailure =>\n Schema.is(SessionConflict)(error) ||\n Schema.is(SessionInvalid)(error) ||\n Schema.is(SessionUnavailable)(error) ||\n Schema.is(SessionStepUpInvalid)(error) ||\n Schema.is(PendingAuthenticationInvalid)(error) ||\n Schema.is(StaleAuthentication)(error);\n\n/** Only schema-checked domain failures cross the owner boundary; SQL/mapping\n * failures remain Unavailable. The executor preserves every mixed cause reason. */\nexport const sessionFailure = <E>(\n error: E,\n): Extract<E, SessionDomainFailure> | SessionUnavailable =>\n isDomainFailure(error) ? (error as Extract<E, SessionDomainFailure>) : sessionUnavailable();\n\nexport const normalizeSessionOperation = <A, E, R>(effect: Effect.Effect<A, E, R>) =>\n effect.pipe(\n Effect.tapError((error) =>\n isDomainFailure(error)\n ? Effect.void\n : reportAuthFailure(\"auth-persistence\", Cause.fail(error)),\n ),\n Effect.mapError(sessionFailure),\n );\n\nexport type NativeSessionAuthorityMapping = SessionAuthorityTables<\n AnyTableModel,\n AnyTableModel,\n unknown\n> &\n SessionExecution;\n\nexport interface NativeSessionAuthority {\n readonly native: unknown;\n readonly row: Record<string, unknown>;\n readonly policyValues: ReadonlyArray<SqlitePolicyValue>;\n readonly revision: AuthenticationRevision;\n readonly requirement: AuthenticationRequirement;\n readonly now: number;\n}\n\nexport const sameSessionRevision = (left: AuthenticationRevision, right: AuthenticationRevision) =>\n left.subjectId === right.subjectId &&\n left.securityRevision === right.securityRevision &&\n left.credentials.length === right.credentials.length &&\n new Set(left.credentials.map((f) => f.credentialId)).size === left.credentials.length &&\n left.credentials.every((f) =>\n right.credentials.some(\n (other) => other.credentialId === f.credentialId && other.revision === f.revision,\n ),\n );\n\n/** Known-subject joins bind each physical ID column independently. Subject locks\n * precede every credential lock, including inside an application coordinator. */\nexport const makeNativeSessionAuthorityState = Effect.fnUntraced(function* (\n tables: NativeSqlTables,\n mapping: NativeSessionAuthorityMapping,\n batch = false,\n) {\n const sql = (yield* SqlClient).withoutTransforms();\n const clockPolicy = yield* AuthenticationClock;\n\n const s = mapping.subject,\n c = mapping.credential;\n\n if (batch) sessionInvariant(s.requirementColumns !== undefined);\n\n const subject = tables(s.table),\n credential = tables(c.table);\n\n const joinedSubject = subject.as(\"session_subject\"),\n joinedCredential = credential.as(\"session_factor\");\n\n const now = tables.expression(mapping.clock.engineNowMillis);\n const lock = sql.onDialectOrElse({ sqlite: () => sql``, orElse: () => sql`for update` });\n const nativeLocks = !batch && sql.onDialectOrElse({ sqlite: () => false, orElse: () => true });\n\n const exact = (table: SqlTable, key: string, value: unknown) =>\n exactSqlText(sql, table.column(key), table.value(key, value));\n\n const id = (table: SqlTable, key: string, value: unknown) =>\n sql`${table.column(key)} = ${table.value(key, value)}`;\n\n const activeSubject = (table: SqlTable) => id(table, s.status, s.activeStatusValue);\n\n const activeCredential = (table: SqlTable) => {\n if (c.status === undefined) return sql`1 = 1`;\n sessionInvariant(c.activeStatusValue !== undefined);\n\n return id(table, c.status, c.activeStatusValue);\n };\n\n const requested = (table: SqlTable, ids: ReadonlyArray<string>) =>\n anySqlCondition(ids.map((value) => exact(table, c.credentialId, value)));\n\n const decode = Effect.fnUntraced(function* (\n subjectId: SubjectId,\n native: unknown,\n projected: Record<string, unknown>,\n factors: ReadonlyArray<Record<string, unknown>>,\n instant: unknown,\n ): Effect.fn.Return<\n NativeSessionAuthority | undefined,\n PersistenceMappingError | Schema.SchemaError\n > {\n const row = subject.decode(projected, \"s_\");\n\n sessionInvariant((yield* mapping.subjectId.toSubject(row[s.id])) === subjectId);\n if (!s.isActiveStatus(row[s.status])) return undefined;\n const credentials: { credentialId: string; revision: string }[] = [];\n\n for (const factor of factors) {\n if (factor[c.credentialId] === null || factor[c.credentialId] === undefined) continue;\n sessionInvariant((yield* mapping.subjectId.toSubject(factor[c.subjectId])) === subjectId);\n if (c.status !== undefined) sessionInvariant(c.isActiveStatus?.(factor[c.status]) === true);\n\n const value = yield* Schema.decodeUnknownEffect(\n Schema.Struct({ credentialId: Schema.String, revision: Schema.String }),\n )({ credentialId: factor[c.credentialId], revision: factor[c.revision] });\n\n sessionInvariant(!credentials.some((other) => other.credentialId === value.credentialId));\n credentials.push(value);\n }\n sessionInvariant(credentials.length <= 64);\n\n return {\n native,\n row,\n // Compare the selected SQL representation, without round-tripping through\n // application codecs (for example, JSON whitespace or date precision).\n policyValues: batch\n ? (s.requirementColumns ?? []).map((key) => ({\n column: subject.selectedColumn(key),\n value: fragment([\n parameter(projected[nativeSqlAlias(\"s_\", subject.keys.indexOf(key))]),\n ]),\n }))\n : [],\n revision: yield* Schema.decodeUnknownEffect(AuthenticationRevision)({\n subjectId,\n securityRevision: row[s.securityRevision],\n credentials: credentials.sort((a, b) => a.credentialId.localeCompare(b.credentialId)),\n }),\n requirement: yield* s.decodeRequirement(row),\n now: yield* Schema.decodeEffect(Schema.Int)(Number(instant)),\n };\n });\n\n const read = Effect.fnUntraced(function* (subjectId: SubjectId, locking = false) {\n const native = yield* mapping.subjectId.toNative(subjectId);\n\n sessionInvariant((yield* mapping.subjectId.toSubject(native)) === subjectId);\n if (locking && nativeLocks) {\n const owners =\n yield* sql`select ${subject.fields(\"s_\")}, ${now} as engine_now from ${subject.name} where ${id(subject, s.id, native)} and ${activeSubject(subject)} limit 2 ${lock}`;\n\n sessionInvariant(owners.length <= 1);\n if (owners[0] === undefined) return undefined;\n\n const factors =\n yield* sql`select ${credential.fields(\"c_\")} from ${credential.name} where ${id(credential, c.subjectId, native)} and ${activeCredential(credential)} order by ${credential.column(c.credentialId)} limit 65 ${lock}`;\n\n return yield* decode(\n subjectId,\n native,\n owners[0],\n factors.map((row) => credential.decode(row, \"c_\")),\n owners[0].engine_now,\n );\n }\n\n const rows =\n yield* sql`select ${joinedSubject.fields(\"s_\")}, ${joinedCredential.fields(\"c_\")}, ${now} as engine_now from ${joinedSubject.name} left join ${joinedCredential.name} on ${id(joinedCredential, c.subjectId, native)} and ${activeCredential(joinedCredential)} where ${id(joinedSubject, s.id, native)} and ${activeSubject(joinedSubject)} limit 65`;\n\n if (rows[0] === undefined) return undefined;\n\n return yield* decode(\n subjectId,\n native,\n rows[0],\n rows.map((row) => joinedCredential.decode(row, \"c_\")),\n rows[0].engine_now,\n );\n });\n\n const guardPolicy = Effect.fnUntraced(function* (current: NativeSessionAuthority) {\n if (!batch) return;\n sessionInvariant(s.requirementColumns !== undefined);\n\n yield* guardSqlitePolicy({\n name: \"session-subject-policy\",\n table: subject.name,\n owner: id(subject, s.id, current.native),\n values: current.policyValues,\n });\n });\n\n const authorityCondition = (revision: AuthenticationRevision, native: unknown): Fragment =>\n sql.and([\n sql`exists(select 1 from ${subject.name} where ${id(subject, s.id, native)} and ${exact(subject, s.securityRevision, revision.securityRevision)} and ${activeSubject(subject)})`,\n sql`(select count(*) from ${credential.name} where ${id(credential, c.subjectId, native)} and ${activeCredential(credential)}) = ${revision.credentials.length}`,\n ...revision.credentials.map(\n (factor) =>\n sql`exists(select 1 from ${credential.name} where ${id(credential, c.subjectId, native)} and ${exact(credential, c.credentialId, factor.credentialId)} and ${exact(credential, c.revision, factor.revision)} and ${activeCredential(credential)})`,\n ),\n ]);\n\n return {\n sql,\n clockPolicy,\n subject,\n credential,\n joinedSubject,\n joinedCredential,\n now,\n lock,\n nativeLocks,\n exact,\n id,\n activeSubject,\n activeCredential,\n requested,\n decode,\n read,\n authorityCondition,\n guardPolicy,\n };\n});\n\n/** Evaluate freshness at the database sample. The final write also enforces the\n * earliest future boundary at which the same evidence would cease to satisfy. */\nexport const assessSessionAt = Effect.fnUntraced(function* (\n evidence: AuthenticationEvidence,\n requirement: AuthenticationRequirement,\n now: number,\n) {\n const result = yield* assessAuthentication(evidence, requirement, DateTime.makeUnsafe(now));\n\n return result;\n});\n"],"mappings":"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"}
@@ -4,13 +4,13 @@ import { NativeSqlTables } from "./native-sql-table.mjs";
4
4
  import { SqlBatchCommit } from "./sql-commit.mjs";
5
5
  import { Effect } from "effect";
6
6
  import { LifecycleHooks } from "@yielded/auth/Hooks";
7
- import { SessionRepository, StatefulSessionPersistence } from "@yielded/auth/Sessions";
7
+ import { SessionRepository, SessionUnavailable, StatefulSessionPersistence } from "@yielded/auth/Sessions";
8
8
  import { SqlClient } from "effect/sql/SqlClient";
9
9
  //#region src/internal/session-native-stateful.d.ts
10
10
  export type NativeStatefulSessionMapping<Claims> = StatefulSessionMapping<Claims, AnyTableModel, AnyTableModel, AnyTableModel, AnyTableModel, unknown, unknown>;
11
11
  export declare const makeNativeStatefulSessionServices: <Claims>(tables: NativeSqlTables, mapping: NativeStatefulSessionMapping<Claims>) => Effect.Effect<{
12
12
  readonly statefulSessionPersistence: StatefulSessionPersistence<Claims>;
13
13
  readonly sessionRepository: SessionRepository;
14
- }, never, LifecycleHooks | SqlBatchCommit | SqlClient>;
14
+ }, SessionUnavailable, LifecycleHooks | SqlBatchCommit | SqlClient>;
15
15
  //#endregion
16
16
  //# sourceMappingURL=session-native-stateful.d.mts.map
@@ -4,11 +4,13 @@ import { exactSqlText, executeSqlChange } from "./sql-change.mjs";
4
4
  import { CurrentSqlCommit, SqlBatchCommit, appendSqlBatchStatement, makeSqlCommitExecutor, registerSqlBatchPostcondition, registerSqlPostcondition } from "./sql-commit.mjs";
5
5
  import { makeNativeSessionAuthorityState, normalizeSessionOperation, sameSessionRevision, sessionInvariant, sessionUnavailable } from "./session-native-state.mjs";
6
6
  import { makeConditionalSqlInsert, makeNativeSessionRecords, sameSessionRecord } from "./session-native-record.mjs";
7
+ import { canJoinTextColumns, withStorageValidation } from "./storage-validation.mjs";
7
8
  import { makeNativeSessionPending, prepareNativeSession } from "./session-native-pending.mjs";
8
9
  import { allocateSessionValue, preservesRevision } from "./session-policy.mjs";
9
10
  import { sessionEvidenceDeadline } from "./session-native-authority.mjs";
10
11
  import { DateTime, Effect, Option, Schema } from "effect";
11
12
  import { PendingAuthenticationInvalid, SessionConflict, SessionId, SessionInvalid, SessionMetadata, StaleAuthentication, snapshotSessionAuthenticationProvenance } from "@yielded/auth/Sessions";
13
+ import { AuthenticationClock } from "@yielded/auth/Operations";
12
14
  //#region src/internal/session-native-stateful.ts
13
15
  const makeNativeStatefulSessionServices = Effect.fnUntraced(function* (tables, mapping) {
14
16
  const batch = yield* SqlBatchCommit;
@@ -22,7 +24,7 @@ const makeNativeStatefulSessionServices = Effect.fnUntraced(function* (tables, m
22
24
  ...mapping.pending
23
25
  }, batch !== void 0);
24
26
  const { sql, s, table, now, exact, live } = records;
25
- const native = (work, mode = "transaction") => batch === void 0 ? executor.operation(normalizeSessionOperation(work), mode) : executor.operationBatch(normalizeSessionOperation(work)).pipe(Effect.provideService(SqlBatchCommit, batch));
27
+ const native = (work, mode = "transaction") => (batch === void 0 ? executor.operation(normalizeSessionOperation(work), mode) : executor.operationBatch(normalizeSessionOperation(work)).pipe(Effect.provideService(SqlBatchCommit, batch))).pipe(Effect.provideService(AuthenticationClock, state.clockPolicy));
26
28
  const stage = Effect.fnUntraced(function* (statement, expected) {
27
29
  if (batch !== void 0) {
28
30
  yield* appendSqlBatchStatement(sql`${statement}`);
@@ -41,7 +43,7 @@ const makeNativeStatefulSessionServices = Effect.fnUntraced(function* (tables, m
41
43
  check: Effect.gen(function* () {
42
44
  const rows = yield* sql`select 1 where ${condition}`;
43
45
  sessionInvariant(rows.length === 1);
44
- }).pipe(Effect.mapError((cause) => PersistenceMappingError.make({
46
+ }).pipe(Effect.provideService(AuthenticationClock, state.clockPolicy), Effect.mapError((cause) => PersistenceMappingError.make({
45
47
  operation: "decode",
46
48
  cause
47
49
  })))
@@ -51,25 +53,26 @@ const makeNativeStatefulSessionServices = Effect.fnUntraced(function* (tables, m
51
53
  return sql`exists(select 1 from ${table.name} where ${yield* records.owner(record)} and ${exact(table, s.securityRevision, record.securityRevision)} and ${records.millis(table, s.expiresAt)} = ${DateTime.toEpochMillis(record.expiresAt)} and ${records.millis(table, s.absoluteExpiresAt)} = ${DateTime.toEpochMillis(record.absoluteExpiresAt)} and ${records.encodedCondition(values)} and ${live(table)})`;
52
54
  });
53
55
  const classify = (effect) => effect.pipe(Effect.mapError((error) => isMappedConstraintConflict(mapping.isConstraintConflict, error) ? SessionConflict.make({}) : error));
56
+ const [joinedIds, joinedRevisions] = yield* Effect.forEach([[table.unencodedTextColumn?.(s.subjectId), state.subject.unencodedTextColumn?.(mapping.subject.id)], [table.unencodedTextColumn?.(s.securityRevision), state.subject.unencodedTextColumn?.(mapping.subject.securityRevision)]], canJoinTextColumns).pipe(withStorageValidation, Effect.mapError(sessionUnavailable));
54
57
  const verify = Effect.fnUntraced(function* (digest) {
55
58
  const subject = state.subject.as("verified_subject"), session = table.as("verified_session"), owner = mapping.subject;
56
- if (table.unencodedTextColumn?.(s.subjectId) !== void 0 && state.subject.unencodedTextColumn?.(owner.id) !== void 0) {
57
- const rows = yield* sql`select ${session.fields("session_")}, ${subject.fields("subject_")} from ${session.name} join ${subject.name} on ${exactSqlText(sql, subject.column(owner.id), session.column(s.subjectId))} and ${state.activeSubject(subject)} and ${exactSqlText(sql, subject.column(owner.securityRevision), session.column(s.securityRevision))} where ${exact(session, s.digest, digest)} and ${live(session)} limit 2`;
58
- sessionInvariant(rows.length <= 1);
59
- if (rows[0] === void 0) return void 0;
60
- const record = yield* records.decode(session.decode(rows[0], "session_"));
61
- const decoded = subject.decode(rows[0], "subject_");
62
- sessionInvariant((yield* mapping.subjectId.toSubject(decoded[owner.id])) === record.subjectId && decoded[owner.securityRevision] === record.securityRevision && owner.isActiveStatus(decoded[owner.status]));
63
- return record;
59
+ let discoveredSubjectId;
60
+ let nativeSubject;
61
+ if (!joinedIds) {
62
+ const discovered = yield* records.read(digest);
63
+ if (discovered === void 0) return void 0;
64
+ discoveredSubjectId = discovered.subjectId;
65
+ nativeSubject = yield* mapping.subjectId.toNative(discoveredSubjectId);
64
66
  }
65
- const record = yield* records.read(digest);
66
- if (record === void 0) return void 0;
67
- const nativeSubject = yield* mapping.subjectId.toNative(record.subjectId);
68
- const rows = yield* sql`select ${subject.fields("subject_")} from ${subject.name} where ${state.id(subject, owner.id, nativeSubject)} and ${state.activeSubject(subject)} and ${state.exact(subject, owner.securityRevision, record.securityRevision)} and ${now} < ${DateTime.toEpochMillis(record.expiresAt)} limit 2`;
67
+ const owns = joinedIds ? exactSqlText(sql, subject.column(owner.id), session.column(s.subjectId)) : sql`${state.id(subject, owner.id, nativeSubject)} and ${state.id(session, s.subjectId, nativeSubject)}`;
68
+ const revision = joinedRevisions ? exactSqlText(sql, subject.column(owner.securityRevision), session.column(s.securityRevision)) : sql`true`;
69
+ const rows = yield* sql`select ${session.fields("session_")}, ${subject.fields("subject_")} from ${session.name} join ${subject.name} on ${owns} and ${state.activeSubject(subject)} and ${revision} where ${exact(session, s.digest, digest)} and ${live(session)} limit 2`;
69
70
  sessionInvariant(rows.length <= 1);
70
71
  if (rows[0] === void 0) return void 0;
71
- sessionInvariant((yield* mapping.subjectId.toSubject(subject.decode(rows[0], "subject_")[owner.id])) === record.subjectId);
72
- return record;
72
+ const record = yield* records.decode(session.decode(rows[0], "session_"));
73
+ const decoded = subject.decode(rows[0], "subject_");
74
+ sessionInvariant((joinedIds || record.subjectId === discoveredSubjectId) && (yield* mapping.subjectId.toSubject(decoded[owner.id])) === record.subjectId && owner.isActiveStatus(decoded[owner.status]));
75
+ return decoded[owner.securityRevision] === record.securityRevision ? record : void 0;
73
76
  });
74
77
  return {
75
78
  statefulSessionPersistence: {
@@ -77,8 +80,8 @@ const makeNativeStatefulSessionServices = Effect.fnUntraced(function* (tables, m
77
80
  const current = yield* state.read(input.evidence.revision.subjectId, true);
78
81
  if (current === void 0 || !sameSessionRevision(current.revision, input.evidence.revision)) return yield* StaleAuthentication.make({});
79
82
  const assessment = yield* sessionEvidenceDeadline(input.evidence, current.requirement, current.now);
80
- const expires = DateTime.toEpochMillis(input.session.expiresAt), absolute = DateTime.toEpochMillis(input.session.absoluteExpiresAt);
81
- if (current.now >= expires || expires > absolute) return yield* StaleAuthentication.make({});
83
+ const issued = Math.max(current.now, DateTime.toEpochMillis(input.now), DateTime.toEpochMillis(input.session.issuedAt), ...input.evidence.proofs.map((proof) => DateTime.toEpochMillis(proof.verifiedAt))), expires = DateTime.toEpochMillis(input.session.expiresAt), absolute = DateTime.toEpochMillis(input.session.absoluteExpiresAt);
84
+ if (issued - current.now > state.clockPolicy.futureToleranceMillis || issued >= expires || current.now >= expires || expires > absolute) return yield* StaleAuthentication.make({});
82
85
  let pendingExpiry = Infinity;
83
86
  if (input.pending !== void 0) {
84
87
  if (pending === void 0 || mapping.pending === void 0) return yield* PendingAuthenticationInvalid.make({});
@@ -97,12 +100,12 @@ const makeNativeStatefulSessionServices = Effect.fnUntraced(function* (tables, m
97
100
  sessionId,
98
101
  subjectId: current.revision.subjectId,
99
102
  securityRevision: current.revision.securityRevision,
100
- assurance: input.handoffSourceSessionId === void 0 ? assessment.assessed.assurance : input.session.assurance,
101
103
  provenance: yield* snapshotSessionAuthenticationProvenance({ evidence: input.evidence }),
102
- issuedAt: DateTime.makeUnsafe(current.now)
104
+ issuedAt: DateTime.makeUnsafe(issued)
103
105
  };
104
106
  const deadline = Math.min(expires, assessment.validUntil, pendingExpiry);
105
- const condition = sql`${state.authorityCondition(current.revision, current.native)} and ${state.policyCondition(current.row, current.native)} and ${now} >= ${current.now} and ${now} < ${deadline}`;
107
+ yield* state.guardPolicy(current);
108
+ const condition = sql`${state.authorityCondition(current.revision, current.native)} and ${now} >= ${current.now} and ${now} < ${deadline}`;
106
109
  const values = {
107
110
  ...s.encodeInsert(record, {
108
111
  subjectId: current.native,
@@ -127,7 +130,7 @@ const makeNativeStatefulSessionServices = Effect.fnUntraced(function* (tables, m
127
130
  const authority = yield* state.read(record.subjectId);
128
131
  sessionInvariant(authority !== void 0 && sameSessionRevision(authority.revision, input.evidence.revision));
129
132
  yield* sessionEvidenceDeadline(input.evidence, authority.requirement, authority.now);
130
- }).pipe(Effect.mapError((cause) => PersistenceMappingError.make({
133
+ }).pipe(Effect.provideService(AuthenticationClock, state.clockPolicy), Effect.mapError((cause) => PersistenceMappingError.make({
131
134
  operation: "decode",
132
135
  cause
133
136
  })))
@@ -136,8 +139,8 @@ const makeNativeStatefulSessionServices = Effect.fnUntraced(function* (tables, m
136
139
  }))),
137
140
  verify: ({ digest }) => executor.verify(verify(digest)).pipe(Effect.flatMap((record) => record === void 0 ? Effect.fail(SessionInvalid.make({})) : Effect.succeed(record))),
138
141
  rotate: (input, project) => native(classify(Effect.gen(function* () {
139
- const absolute = DateTime.toEpochMillis(input.record.absoluteExpiresAt), expires = DateTime.toEpochMillis(input.nextExpiresAt);
140
- if (expires > absolute || input.nextDigest === input.record.digest || input.nextCredentialVersion === input.record.credentialVersion) return yield* SessionConflict.make({});
142
+ const absolute = DateTime.toEpochMillis(input.record.absoluteExpiresAt), expires = DateTime.toEpochMillis(input.nextExpiresAt), preparedAt = Math.max(DateTime.toEpochMillis(input.now), DateTime.toEpochMillis(input.record.issuedAt));
143
+ if (expires > absolute || preparedAt >= expires || input.nextDigest === input.record.digest || input.nextCredentialVersion === input.record.credentialVersion) return yield* SessionConflict.make({});
141
144
  const needsSample = batch !== void 0 || sql.onDialectOrElse({
142
145
  mysql: () => true,
143
146
  orElse: () => false
@@ -149,15 +152,15 @@ const makeNativeStatefulSessionServices = Effect.fnUntraced(function* (tables, m
149
152
  digest: input.nextDigest,
150
153
  credentialVersion: input.nextCredentialVersion,
151
154
  expiresAt: input.nextExpiresAt,
152
- issuedAt: DateTime.makeUnsafe(sampled)
155
+ issuedAt: DateTime.makeUnsafe(Math.max(sampled, preparedAt))
153
156
  };
154
157
  const values = {
155
158
  ...s.encodeRotation(prepared),
156
159
  [s.digest]: prepared.digest,
157
- [s.issuedAt]: tables.expression(mapping.clock.fromMillis(now)),
160
+ [s.issuedAt]: tables.expression(mapping.clock.fromMillis(sql`case when ${now} >= ${preparedAt} then ${now} else ${preparedAt} end`)),
158
161
  [s.expiresAt]: s.encodeInstant(prepared.expiresAt)
159
162
  };
160
- const condition = sql`${yield* records.owner(input.record)} and ${live(table)} and ${now} < ${expires} and ${records.millis(table, s.absoluteExpiresAt)} = ${absolute}`;
163
+ const condition = sql`${yield* records.owner(input.record)} and ${live(table)} and ${preparedAt} <= ${now} + ${state.clockPolicy.futureToleranceMillis} and ${now} < ${expires} and ${records.millis(table, s.absoluteExpiresAt)} = ${absolute}`;
161
164
  const update = sql`${table.update(values)} where ${condition}`;
162
165
  let record = prepared;
163
166
  if (needsSample) {
@@ -205,17 +208,43 @@ const makeNativeStatefulSessionServices = Effect.fnUntraced(function* (tables, m
205
208
  sessionInvariant(Number.isSafeInteger(input.limit) && input.limit > 0 && input.limit <= 1e3);
206
209
  const nativeSubject = yield* mapping.subjectId.toNative(input.subjectId), st = state.subject.as("listed_subject"), list = table.as("listed_session"), owner = mapping.subject;
207
210
  const cursor = input.cursor === void 0 ? void 0 : yield* mapping.sessionId.toNative(yield* Schema.decodeEffect(SessionId)(input.cursor));
208
- const rows = yield* sql`select ${list.fields("session_")}, ${st.fields("subject_")} from ${list.name} join ${st.name} on ${state.id(st, owner.id, nativeSubject)} and ${state.activeSubject(st)} and ${exactSqlText(sql, st.column(owner.securityRevision), list.column(s.securityRevision))} where ${state.id(list, s.subjectId, nativeSubject)} and ${live(list)}${cursor === void 0 ? sql`` : sql` and ${list.column(s.sessionId)} > ${list.value(s.sessionId, cursor)}`} order by ${list.column(s.sessionId)} limit ${input.limit + 1}`;
209
- const decoded = yield* Effect.forEach(rows.slice(0, input.limit), (row) => Effect.gen(function* () {
210
- const record = yield* records.decode(list.decode(row, "session_"));
211
- sessionInvariant(record.subjectId === input.subjectId && (yield* mapping.subjectId.toSubject(st.decode(row, "subject_")[owner.id])) === input.subjectId);
212
- return yield* Schema.decodeEffect(Schema.toType(SessionMetadata))(record);
213
- }));
214
- const nextCursor = rows.length > input.limit ? decoded.at(-1)?.sessionId : void 0;
215
- return {
216
- sessions: decoded,
217
- ...nextCursor === void 0 ? {} : { nextCursor }
218
- };
211
+ let revision;
212
+ if (!joinedRevisions) {
213
+ const subjects = yield* sql`select ${st.fields("subject_")} from ${st.name} where ${state.id(st, owner.id, nativeSubject)} and ${state.activeSubject(st)} limit 2`;
214
+ sessionInvariant(subjects.length <= 1);
215
+ if (subjects[0] === void 0) return { sessions: [] };
216
+ const subject = st.decode(subjects[0], "subject_");
217
+ sessionInvariant((yield* mapping.subjectId.toSubject(subject[owner.id])) === input.subjectId);
218
+ revision = yield* Schema.decodeUnknownEffect(SessionMetadata.fields.securityRevision)(subject[owner.securityRevision]);
219
+ }
220
+ for (let attempt = 0; attempt < 2; attempt++) {
221
+ const currentRevision = joinedRevisions ? exactSqlText(sql, st.column(owner.securityRevision), list.column(s.securityRevision)) : sql`${state.exact(st, owner.securityRevision, revision)} and ${exact(list, s.securityRevision, revision)}`;
222
+ const rows = yield* sql`select ${list.fields("session_")}, ${st.fields("subject_")}, ${list.column(s.subjectId)} as session_owner from ${st.name} left join ${list.name} on ${state.id(list, s.subjectId, nativeSubject)} and ${currentRevision} and ${live(list)}${cursor === void 0 ? sql`` : sql` and ${list.column(s.sessionId)} > ${list.value(s.sessionId, cursor)}`} where ${state.id(st, owner.id, nativeSubject)} and ${state.activeSubject(st)} order by ${list.column(s.sessionId)} limit ${input.limit + 1}`;
223
+ const first = rows[0];
224
+ if (first === void 0) return { sessions: [] };
225
+ if (!joinedRevisions) {
226
+ const subject = st.decode(first, "subject_");
227
+ sessionInvariant((yield* mapping.subjectId.toSubject(subject[owner.id])) === input.subjectId);
228
+ const current = yield* Schema.decodeUnknownEffect(SessionMetadata.fields.securityRevision)(subject[owner.securityRevision]);
229
+ if (current !== revision) {
230
+ revision = current;
231
+ continue;
232
+ }
233
+ }
234
+ if (first.session_owner === null) return { sessions: [] };
235
+ const decoded = yield* Effect.forEach(rows.slice(0, input.limit), (row) => Effect.gen(function* () {
236
+ const record = yield* records.decode(list.decode(row, "session_"));
237
+ const subject = st.decode(row, "subject_");
238
+ sessionInvariant(record.subjectId === input.subjectId && (yield* mapping.subjectId.toSubject(subject[owner.id])) === input.subjectId && subject[owner.securityRevision] === record.securityRevision);
239
+ return yield* Schema.decodeEffect(Schema.toType(SessionMetadata))(record);
240
+ }));
241
+ const nextCursor = rows.length > input.limit ? decoded.at(-1)?.sessionId : void 0;
242
+ return {
243
+ sessions: decoded,
244
+ ...nextCursor === void 0 ? {} : { nextCursor }
245
+ };
246
+ }
247
+ return yield* sessionUnavailable();
219
248
  })) }
220
249
  };
221
250
  });