@yielded/auth-persistence 0.1.0-beta.25 → 0.1.0-beta.27

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 (481) hide show
  1. package/README.md +6 -8
  2. package/dist/Adapter.d.mts +47 -33
  3. package/dist/Adapter.mjs +36 -23
  4. package/dist/OAuthPersistence.d.mts +35 -51
  5. package/dist/OAuthPersistence.mjs +60 -68
  6. package/dist/OAuthPersistence.mjs.map +1 -1
  7. package/dist/index.d.mts +2 -2
  8. package/dist/index.mjs +2 -2
  9. package/dist/internal/browser-login.d.mts +1 -1
  10. package/dist/internal/browser-login.mjs +29 -9
  11. package/dist/internal/browser-login.mjs.map +1 -1
  12. package/dist/internal/configuration.d.mts +13 -8
  13. package/dist/internal/configuration.mjs.map +1 -1
  14. package/dist/internal/crypto.d.mts +1 -0
  15. package/dist/internal/crypto.mjs +1 -1
  16. package/dist/internal/d1-planning.mjs +8 -0
  17. package/dist/internal/d1-planning.mjs.map +1 -0
  18. package/dist/internal/email-native-state.mjs +135 -0
  19. package/dist/internal/email-native-state.mjs.map +1 -0
  20. package/dist/internal/email-native.d.mts +17 -0
  21. package/dist/internal/email-native.mjs +245 -0
  22. package/dist/internal/email-native.mjs.map +1 -0
  23. package/dist/internal/email-policy.mjs +92 -0
  24. package/dist/internal/email-policy.mjs.map +1 -0
  25. package/dist/internal/email-registration-native.d.mts +38 -0
  26. package/dist/internal/email-registration-native.mjs +171 -0
  27. package/dist/internal/email-registration-native.mjs.map +1 -0
  28. package/dist/internal/models/email-model.d.mts +40 -104
  29. package/dist/internal/models/email-model.mjs +3 -5
  30. package/dist/internal/models/email-model.mjs.map +1 -1
  31. package/dist/internal/models/oauth-connected-model.d.mts +44 -213
  32. package/dist/internal/models/oauth-connected-model.mjs +6 -14
  33. package/dist/internal/models/oauth-connected-model.mjs.map +1 -1
  34. package/dist/internal/models/oauth-model.d.mts +95 -244
  35. package/dist/internal/models/oauth-model.mjs +28 -22
  36. package/dist/internal/models/oauth-model.mjs.map +1 -1
  37. package/dist/internal/models/passkey-model.d.mts +11 -147
  38. package/dist/internal/models/passkey-model.mjs +5 -24
  39. package/dist/internal/models/passkey-model.mjs.map +1 -1
  40. package/dist/internal/models/passkey-registration-ceremony-model.d.mts +5 -39
  41. package/dist/internal/models/passkey-write-model.d.mts +17 -75
  42. package/dist/internal/models/passkey-write-model.mjs +1 -7
  43. package/dist/internal/models/passkey-write-model.mjs.map +1 -1
  44. package/dist/internal/models/password-model.d.mts +14 -81
  45. package/dist/internal/models/password-model.mjs +1 -4
  46. package/dist/internal/models/password-model.mjs.map +1 -1
  47. package/dist/internal/models/phone-model.d.mts +86 -0
  48. package/dist/internal/models/phone-model.mjs +10 -0
  49. package/dist/internal/models/phone-model.mjs.map +1 -0
  50. package/dist/internal/models/proof-model.d.mts +43 -272
  51. package/dist/internal/models/proof-model.mjs +2 -10
  52. package/dist/internal/models/proof-model.mjs.map +1 -1
  53. package/dist/internal/models/session-model.d.mts +74 -93
  54. package/dist/internal/models/session-model.mjs +3 -11
  55. package/dist/internal/models/session-model.mjs.map +1 -1
  56. package/dist/internal/models/step-up-model.d.mts +12 -50
  57. package/dist/internal/models/step-up-model.mjs +2 -4
  58. package/dist/internal/models/step-up-model.mjs.map +1 -1
  59. package/dist/internal/models/totp-model.d.mts +76 -0
  60. package/dist/internal/models/totp-model.mjs +10 -0
  61. package/dist/internal/models/totp-model.mjs.map +1 -0
  62. package/dist/internal/native-sql-table.d.mts +21 -4
  63. package/dist/internal/native-sql-table.mjs +9 -5
  64. package/dist/internal/native-sql-table.mjs.map +1 -1
  65. package/dist/internal/oauth/native-accounts.d.mts +17 -0
  66. package/dist/internal/oauth/native-accounts.mjs +290 -0
  67. package/dist/internal/oauth/native-accounts.mjs.map +1 -0
  68. package/dist/internal/oauth/native-connected-state.d.mts +11 -0
  69. package/dist/internal/oauth/native-connected-state.mjs +146 -0
  70. package/dist/internal/oauth/native-connected-state.mjs.map +1 -0
  71. package/dist/internal/oauth/native-connected.d.mts +15 -0
  72. package/dist/internal/oauth/native-connected.mjs +462 -0
  73. package/dist/internal/oauth/native-connected.mjs.map +1 -0
  74. package/dist/internal/oauth/native-flow.mjs +138 -0
  75. package/dist/internal/oauth/native-flow.mjs.map +1 -0
  76. package/dist/internal/oauth/native-layers.d.mts +24 -0
  77. package/dist/internal/oauth/native-layers.mjs +13 -0
  78. package/dist/internal/oauth/native-layers.mjs.map +1 -0
  79. package/dist/internal/oauth/native-mutation.mjs +122 -0
  80. package/dist/internal/oauth/native-mutation.mjs.map +1 -0
  81. package/dist/internal/oauth/native-registration.d.mts +24 -0
  82. package/dist/internal/oauth/native-registration.mjs +328 -0
  83. package/dist/internal/oauth/native-registration.mjs.map +1 -0
  84. package/dist/internal/oauth/native-revocations.d.mts +17 -0
  85. package/dist/internal/oauth/native-revocations.mjs +106 -0
  86. package/dist/internal/oauth/native-revocations.mjs.map +1 -0
  87. package/dist/internal/oauth/native-sign-in.d.mts +17 -0
  88. package/dist/internal/oauth/native-sign-in.mjs +50 -0
  89. package/dist/internal/oauth/native-sign-in.mjs.map +1 -0
  90. package/dist/internal/oauth/native-state.d.mts +10 -0
  91. package/dist/internal/oauth/native-state.mjs +214 -0
  92. package/dist/internal/oauth/native-state.mjs.map +1 -0
  93. package/dist/internal/oauth/state.d.mts +10 -0
  94. package/dist/internal/oauth/state.mjs +3 -27
  95. package/dist/internal/oauth/state.mjs.map +1 -1
  96. package/dist/internal/passkey-actions.mjs +55 -0
  97. package/dist/internal/passkey-actions.mjs.map +1 -0
  98. package/dist/internal/passkey-inputs.mjs +73 -0
  99. package/dist/internal/passkey-inputs.mjs.map +1 -0
  100. package/dist/internal/passkey-managed.mjs +224 -0
  101. package/dist/internal/passkey-managed.mjs.map +1 -0
  102. package/dist/internal/passkey-native-ceremony.d.mts +35 -0
  103. package/dist/internal/passkey-native-ceremony.mjs +38 -0
  104. package/dist/internal/passkey-native-ceremony.mjs.map +1 -0
  105. package/dist/internal/passkey-native-flow.mjs +91 -0
  106. package/dist/internal/passkey-native-flow.mjs.map +1 -0
  107. package/dist/internal/passkey-native-registration.d.mts +10 -0
  108. package/dist/internal/passkey-native-registration.mjs +114 -0
  109. package/dist/internal/passkey-native-registration.mjs.map +1 -0
  110. package/dist/internal/passkey-native-state.d.mts +15 -0
  111. package/dist/internal/passkey-native-state.mjs +186 -0
  112. package/dist/internal/passkey-native-state.mjs.map +1 -0
  113. package/dist/internal/passkey-native-write.d.mts +82 -0
  114. package/dist/internal/passkey-native-write.mjs +330 -0
  115. package/dist/internal/passkey-native-write.mjs.map +1 -0
  116. package/dist/internal/passkey-native.d.mts +835 -0
  117. package/dist/internal/passkey-native.mjs +175 -0
  118. package/dist/internal/passkey-native.mjs.map +1 -0
  119. package/dist/internal/passkey-policy.mjs +115 -0
  120. package/dist/internal/passkey-policy.mjs.map +1 -0
  121. package/dist/internal/password-credentials.mjs +38 -9
  122. package/dist/internal/password-credentials.mjs.map +1 -1
  123. package/dist/internal/password-native.d.mts +14 -0
  124. package/dist/internal/password-native.mjs +292 -0
  125. package/dist/internal/password-native.mjs.map +1 -0
  126. package/dist/internal/password-policy.mjs +93 -0
  127. package/dist/internal/password-policy.mjs.map +1 -0
  128. package/dist/internal/password-registration-native.d.mts +22 -0
  129. package/dist/internal/password-registration-native.mjs +142 -0
  130. package/dist/internal/password-registration-native.mjs.map +1 -0
  131. package/dist/internal/persistence.d.mts +6 -19
  132. package/dist/internal/persistence.mjs +199 -166
  133. package/dist/internal/persistence.mjs.map +1 -1
  134. package/dist/internal/phone-composed.mjs +79 -0
  135. package/dist/internal/phone-composed.mjs.map +1 -0
  136. package/dist/internal/phone-native.d.mts +15 -0
  137. package/dist/internal/phone-native.mjs +354 -0
  138. package/dist/internal/phone-native.mjs.map +1 -0
  139. package/dist/internal/proof-native.d.mts +15 -0
  140. package/dist/internal/proof-native.mjs +289 -0
  141. package/dist/internal/proof-native.mjs.map +1 -0
  142. package/dist/internal/registration.mjs +50 -60
  143. package/dist/internal/registration.mjs.map +1 -1
  144. package/dist/internal/session-native-authority.d.mts +15 -0
  145. package/dist/internal/session-native-authority.mjs +91 -0
  146. package/dist/internal/session-native-authority.mjs.map +1 -0
  147. package/dist/internal/session-native-cleanup.d.mts +14 -0
  148. package/dist/internal/session-native-cleanup.mjs +48 -0
  149. package/dist/internal/session-native-cleanup.mjs.map +1 -0
  150. package/dist/internal/session-native-login.d.mts +15 -0
  151. package/dist/internal/session-native-login.mjs +87 -0
  152. package/dist/internal/session-native-login.mjs.map +1 -0
  153. package/dist/internal/session-native-pending-read.mjs +42 -0
  154. package/dist/internal/session-native-pending-read.mjs.map +1 -0
  155. package/dist/internal/session-native-pending.mjs +134 -0
  156. package/dist/internal/session-native-pending.mjs.map +1 -0
  157. package/dist/internal/session-native-record.mjs +79 -0
  158. package/dist/internal/session-native-record.mjs.map +1 -0
  159. package/dist/internal/session-native-state.mjs +130 -0
  160. package/dist/internal/session-native-state.mjs.map +1 -0
  161. package/dist/internal/session-native-stateful.d.mts +16 -0
  162. package/dist/internal/session-native-stateful.mjs +225 -0
  163. package/dist/internal/session-native-stateful.mjs.map +1 -0
  164. package/dist/internal/session-native-step-up.d.mts +15 -0
  165. package/dist/internal/session-native-step-up.mjs +212 -0
  166. package/dist/internal/session-native-step-up.mjs.map +1 -0
  167. package/dist/internal/session-native-validity.d.mts +15 -0
  168. package/dist/internal/session-native-validity.mjs +97 -0
  169. package/dist/internal/session-native-validity.mjs.map +1 -0
  170. package/dist/internal/session-policy.mjs +29 -0
  171. package/dist/internal/session-policy.mjs.map +1 -0
  172. package/dist/internal/sql-authority.d.mts +16 -0
  173. package/dist/internal/sql-authority.mjs +32 -0
  174. package/dist/internal/sql-authority.mjs.map +1 -0
  175. package/dist/internal/sql-change.mjs +21 -0
  176. package/dist/internal/sql-change.mjs.map +1 -0
  177. package/dist/internal/sql-cleanup.mjs +35 -0
  178. package/dist/internal/sql-cleanup.mjs.map +1 -0
  179. package/dist/internal/sql-commit.d.mts +72 -0
  180. package/dist/internal/sql-commit.mjs +176 -0
  181. package/dist/internal/sql-commit.mjs.map +1 -0
  182. package/dist/internal/sql-expression.d.mts +17 -0
  183. package/dist/internal/sql-expression.mjs +49 -0
  184. package/dist/internal/sql-expression.mjs.map +1 -0
  185. package/dist/internal/sql-oauth-connected-model.d.mts +7 -23
  186. package/dist/internal/sql-oauth-model.d.mts +27 -52
  187. package/dist/internal/sql-oauth-model.mjs +3 -4
  188. package/dist/internal/sql-oauth-model.mjs.map +1 -1
  189. package/dist/internal/sql-persistence.mjs +5 -9
  190. package/dist/internal/sql-persistence.mjs.map +1 -1
  191. package/dist/internal/sql-storage-validation.mjs +1 -2
  192. package/dist/internal/sql-storage-validation.mjs.map +1 -1
  193. package/dist/internal/sql-table.d.mts +3 -16
  194. package/dist/internal/sql-table.mjs +21 -42
  195. package/dist/internal/sql-table.mjs.map +1 -1
  196. package/dist/internal/standalone.mjs +2 -3
  197. package/dist/internal/standalone.mjs.map +1 -1
  198. package/dist/internal/step-up-state.mjs +5 -8
  199. package/dist/internal/step-up-state.mjs.map +1 -1
  200. package/dist/internal/storage-clock.mjs +44 -0
  201. package/dist/internal/storage-clock.mjs.map +1 -0
  202. package/dist/internal/storage-mapping.d.mts +11 -8
  203. package/dist/internal/storage-mapping.mjs +88 -302
  204. package/dist/internal/storage-mapping.mjs.map +1 -1
  205. package/dist/internal/storage-plans.mjs +15 -73
  206. package/dist/internal/storage-plans.mjs.map +1 -1
  207. package/dist/internal/storage-tables.d.mts +37 -520
  208. package/dist/internal/storage-tables.mjs +31 -211
  209. package/dist/internal/storage-tables.mjs.map +1 -1
  210. package/dist/internal/storage-validation.d.mts +1 -6
  211. package/dist/internal/storage-validation.mjs +1 -42
  212. package/dist/internal/storage-validation.mjs.map +1 -1
  213. package/dist/internal/table-model.d.mts +21 -0
  214. package/dist/internal/totp-native.d.mts +15 -0
  215. package/dist/internal/totp-native.mjs +401 -0
  216. package/dist/internal/totp-native.mjs.map +1 -0
  217. package/package.json +1 -1
  218. package/src/Adapter.ts +143 -199
  219. package/src/OAuthPersistence.ts +128 -271
  220. package/src/index.ts +1 -1
  221. package/src/internal/browser-login.ts +54 -24
  222. package/src/internal/configuration.ts +16 -31
  223. package/src/internal/d1-planning.ts +7 -0
  224. package/src/internal/email-native-state.ts +247 -0
  225. package/src/internal/email-native.ts +514 -0
  226. package/src/internal/email-policy.ts +235 -0
  227. package/src/internal/email-registration-native.ts +386 -0
  228. package/src/internal/models/email-model.ts +76 -214
  229. package/src/internal/models/oauth-connected-model.ts +50 -288
  230. package/src/internal/models/oauth-model.ts +147 -368
  231. package/src/internal/models/passkey-model.ts +11 -208
  232. package/src/internal/models/passkey-registration-ceremony-model.ts +6 -60
  233. package/src/internal/models/passkey-write-model.ts +14 -95
  234. package/src/internal/models/password-model.ts +26 -147
  235. package/src/internal/models/phone-model.ts +110 -0
  236. package/src/internal/models/proof-model.ts +48 -404
  237. package/src/internal/models/session-model.ts +102 -200
  238. package/src/internal/models/step-up-model.ts +17 -96
  239. package/src/internal/models/totp-model.ts +82 -0
  240. package/src/internal/native-sql-table.ts +32 -11
  241. package/src/internal/oauth/native-accounts.ts +576 -0
  242. package/src/internal/oauth/native-connected-state.ts +288 -0
  243. package/src/internal/oauth/native-connected.ts +888 -0
  244. package/src/internal/oauth/native-flow.ts +253 -0
  245. package/src/internal/oauth/native-layers.ts +83 -0
  246. package/src/internal/oauth/native-mutation.ts +222 -0
  247. package/src/internal/oauth/native-registration.ts +545 -0
  248. package/src/internal/oauth/native-revocations.ts +190 -0
  249. package/src/internal/oauth/native-sign-in.ts +111 -0
  250. package/src/internal/oauth/native-state.ts +346 -0
  251. package/src/internal/oauth/state.ts +2 -70
  252. package/src/internal/passkey-actions.ts +70 -0
  253. package/src/internal/passkey-inputs.ts +56 -0
  254. package/src/internal/passkey-managed.ts +360 -0
  255. package/src/internal/passkey-native-ceremony.ts +66 -0
  256. package/src/internal/passkey-native-flow.ts +140 -0
  257. package/src/internal/passkey-native-registration.ts +221 -0
  258. package/src/internal/passkey-native-state.ts +335 -0
  259. package/src/internal/passkey-native-write.ts +611 -0
  260. package/src/internal/passkey-native.ts +372 -0
  261. package/src/internal/passkey-policy.ts +246 -0
  262. package/src/internal/password-credentials.ts +52 -10
  263. package/src/internal/password-native.ts +571 -0
  264. package/src/internal/password-policy.ts +307 -0
  265. package/src/internal/password-registration-native.ts +322 -0
  266. package/src/internal/persistence.ts +336 -290
  267. package/src/internal/phone-composed.ts +86 -0
  268. package/src/internal/phone-native.ts +659 -0
  269. package/src/internal/proof-native.ts +448 -0
  270. package/src/internal/registration.ts +96 -143
  271. package/src/internal/session-native-authority.ts +247 -0
  272. package/src/internal/session-native-cleanup.ts +65 -0
  273. package/src/internal/session-native-login.ts +205 -0
  274. package/src/internal/session-native-pending-read.ts +68 -0
  275. package/src/internal/session-native-pending.ts +223 -0
  276. package/src/internal/session-native-record.ts +141 -0
  277. package/src/internal/session-native-state.ts +259 -0
  278. package/src/internal/session-native-stateful.ts +512 -0
  279. package/src/internal/session-native-step-up.ts +450 -0
  280. package/src/internal/session-native-validity.ts +208 -0
  281. package/src/internal/session-policy.ts +59 -0
  282. package/src/internal/sql-authority.ts +56 -0
  283. package/src/internal/sql-change.ts +38 -0
  284. package/src/internal/sql-cleanup.ts +56 -0
  285. package/src/internal/sql-commit.ts +435 -0
  286. package/src/internal/sql-expression.ts +94 -0
  287. package/src/internal/sql-metadata.ts +100 -0
  288. package/src/internal/sql-oauth-connected-model.ts +6 -95
  289. package/src/internal/sql-oauth-model.ts +31 -134
  290. package/src/internal/sql-persistence.ts +2 -6
  291. package/src/internal/sql-storage-validation.ts +1 -2
  292. package/src/internal/sql-table.ts +19 -52
  293. package/src/internal/standalone.ts +2 -11
  294. package/src/internal/step-up-state.ts +4 -17
  295. package/src/internal/storage-clock.ts +58 -0
  296. package/src/internal/storage-mapping.ts +125 -340
  297. package/src/internal/storage-plans.ts +14 -80
  298. package/src/internal/storage-tables.ts +28 -212
  299. package/src/internal/storage-validation.ts +0 -58
  300. package/src/internal/table-model.ts +24 -0
  301. package/src/internal/totp-native.ts +701 -0
  302. package/dist/internal/PasswordPreparedPostconditions.d.mts +0 -22
  303. package/dist/internal/PasswordPreparedPostconditions.mjs +0 -12
  304. package/dist/internal/PasswordPreparedPostconditions.mjs.map +0 -1
  305. package/dist/internal/authority-snapshot.mjs +0 -53
  306. package/dist/internal/authority-snapshot.mjs.map +0 -1
  307. package/dist/internal/email-kernel.d.mts +0 -276
  308. package/dist/internal/email-kernel.mjs +0 -497
  309. package/dist/internal/email-kernel.mjs.map +0 -1
  310. package/dist/internal/models/passkey-registration-ceremony-model.mjs +0 -18
  311. package/dist/internal/models/passkey-registration-ceremony-model.mjs.map +0 -1
  312. package/dist/internal/oauth/accounts.d.mts +0 -8
  313. package/dist/internal/oauth/accounts.mjs +0 -379
  314. package/dist/internal/oauth/accounts.mjs.map +0 -1
  315. package/dist/internal/oauth/connected-access.d.mts +0 -9
  316. package/dist/internal/oauth/connected-access.mjs +0 -229
  317. package/dist/internal/oauth/connected-access.mjs.map +0 -1
  318. package/dist/internal/oauth/connected-collection.d.mts +0 -7
  319. package/dist/internal/oauth/connected-collection.mjs +0 -214
  320. package/dist/internal/oauth/connected-collection.mjs.map +0 -1
  321. package/dist/internal/oauth/connected-custody.d.mts +0 -6
  322. package/dist/internal/oauth/connected-custody.mjs +0 -165
  323. package/dist/internal/oauth/connected-custody.mjs.map +0 -1
  324. package/dist/internal/oauth/connected-flow.d.mts +0 -7
  325. package/dist/internal/oauth/connected-flow.mjs +0 -324
  326. package/dist/internal/oauth/connected-flow.mjs.map +0 -1
  327. package/dist/internal/oauth/connected-input.mjs +0 -122
  328. package/dist/internal/oauth/connected-input.mjs.map +0 -1
  329. package/dist/internal/oauth/connected-keys.d.mts +0 -2
  330. package/dist/internal/oauth/connected-keys.mjs +0 -22
  331. package/dist/internal/oauth/connected-keys.mjs.map +0 -1
  332. package/dist/internal/oauth/connected-maintenance.d.mts +0 -9
  333. package/dist/internal/oauth/connected-maintenance.mjs +0 -226
  334. package/dist/internal/oauth/connected-maintenance.mjs.map +0 -1
  335. package/dist/internal/oauth/connected-management.d.mts +0 -9
  336. package/dist/internal/oauth/connected-management.mjs +0 -243
  337. package/dist/internal/oauth/connected-management.mjs.map +0 -1
  338. package/dist/internal/oauth/connected-reference.mjs +0 -63
  339. package/dist/internal/oauth/connected-reference.mjs.map +0 -1
  340. package/dist/internal/oauth/connected-settlement.d.mts +0 -8
  341. package/dist/internal/oauth/connected-settlement.mjs +0 -141
  342. package/dist/internal/oauth/connected-settlement.mjs.map +0 -1
  343. package/dist/internal/oauth/connected-sign-in.d.mts +0 -10
  344. package/dist/internal/oauth/connected-sign-in.mjs +0 -281
  345. package/dist/internal/oauth/connected-sign-in.mjs.map +0 -1
  346. package/dist/internal/oauth/connected-state.d.mts +0 -14
  347. package/dist/internal/oauth/connected-state.mjs +0 -469
  348. package/dist/internal/oauth/connected-state.mjs.map +0 -1
  349. package/dist/internal/oauth/connected-target.d.mts +0 -21
  350. package/dist/internal/oauth/connected-target.mjs +0 -117
  351. package/dist/internal/oauth/connected-target.mjs.map +0 -1
  352. package/dist/internal/oauth/flow.d.mts +0 -5
  353. package/dist/internal/oauth/flow.mjs +0 -279
  354. package/dist/internal/oauth/flow.mjs.map +0 -1
  355. package/dist/internal/oauth/input.mjs +0 -127
  356. package/dist/internal/oauth/input.mjs.map +0 -1
  357. package/dist/internal/oauth/owner.d.mts +0 -10
  358. package/dist/internal/oauth/owner.mjs +0 -10
  359. package/dist/internal/oauth/owner.mjs.map +0 -1
  360. package/dist/internal/oauth/query-compiler.d.mts +0 -7
  361. package/dist/internal/oauth/query-compiler.mjs +0 -9
  362. package/dist/internal/oauth/query-compiler.mjs.map +0 -1
  363. package/dist/internal/oauth/registration.d.mts +0 -7
  364. package/dist/internal/oauth/registration.mjs +0 -393
  365. package/dist/internal/oauth/registration.mjs.map +0 -1
  366. package/dist/internal/oauth/target.d.mts +0 -31
  367. package/dist/internal/oauth/target.mjs +0 -273
  368. package/dist/internal/oauth/target.mjs.map +0 -1
  369. package/dist/internal/oauth-kernel.d.mts +0 -1423
  370. package/dist/internal/oauth-kernel.mjs +0 -187
  371. package/dist/internal/oauth-kernel.mjs.map +0 -1
  372. package/dist/internal/passkey/admission.d.mts +0 -6
  373. package/dist/internal/passkey/admission.mjs +0 -220
  374. package/dist/internal/passkey/admission.mjs.map +0 -1
  375. package/dist/internal/passkey/credentials.d.mts +0 -14
  376. package/dist/internal/passkey/credentials.mjs +0 -249
  377. package/dist/internal/passkey/credentials.mjs.map +0 -1
  378. package/dist/internal/passkey/enrollment.mjs +0 -138
  379. package/dist/internal/passkey/enrollment.mjs.map +0 -1
  380. package/dist/internal/passkey/flow.d.mts +0 -21
  381. package/dist/internal/passkey/flow.mjs +0 -452
  382. package/dist/internal/passkey/flow.mjs.map +0 -1
  383. package/dist/internal/passkey/management.mjs +0 -231
  384. package/dist/internal/passkey/management.mjs.map +0 -1
  385. package/dist/internal/passkey/registration-ceremony.d.mts +0 -9
  386. package/dist/internal/passkey/registration-ceremony.mjs +0 -129
  387. package/dist/internal/passkey/registration-ceremony.mjs.map +0 -1
  388. package/dist/internal/passkey/registration-custody.d.mts +0 -2
  389. package/dist/internal/passkey/registration-custody.mjs +0 -52
  390. package/dist/internal/passkey/registration-custody.mjs.map +0 -1
  391. package/dist/internal/passkey/registration-write.mjs +0 -211
  392. package/dist/internal/passkey/registration-write.mjs.map +0 -1
  393. package/dist/internal/passkey/state.d.mts +0 -10
  394. package/dist/internal/passkey/state.mjs +0 -234
  395. package/dist/internal/passkey/state.mjs.map +0 -1
  396. package/dist/internal/passkey/target.d.mts +0 -20
  397. package/dist/internal/passkey/target.mjs +0 -200
  398. package/dist/internal/passkey/target.mjs.map +0 -1
  399. package/dist/internal/passkey/write-state.d.mts +0 -17
  400. package/dist/internal/passkey/write-state.mjs +0 -354
  401. package/dist/internal/passkey/write-state.mjs.map +0 -1
  402. package/dist/internal/passkey/write-target.mjs +0 -192
  403. package/dist/internal/passkey/write-target.mjs.map +0 -1
  404. package/dist/internal/passkey-kernel.d.mts +0 -6045
  405. package/dist/internal/passkey-kernel.mjs +0 -49
  406. package/dist/internal/passkey-kernel.mjs.map +0 -1
  407. package/dist/internal/passkeys.d.mts +0 -7
  408. package/dist/internal/passkeys.mjs +0 -387
  409. package/dist/internal/passkeys.mjs.map +0 -1
  410. package/dist/internal/password-credentials.d.mts +0 -11
  411. package/dist/internal/password-kernel.d.mts +0 -278
  412. package/dist/internal/password-kernel.mjs +0 -508
  413. package/dist/internal/password-kernel.mjs.map +0 -1
  414. package/dist/internal/phone-kernel.d.mts +0 -162
  415. package/dist/internal/phone-kernel.mjs +0 -437
  416. package/dist/internal/phone-kernel.mjs.map +0 -1
  417. package/dist/internal/proof-kernel.d.mts +0 -94
  418. package/dist/internal/proof-kernel.mjs +0 -624
  419. package/dist/internal/proof-kernel.mjs.map +0 -1
  420. package/dist/internal/query-operations.d.mts +0 -68
  421. package/dist/internal/session-kernel.d.mts +0 -62
  422. package/dist/internal/session-kernel.mjs +0 -820
  423. package/dist/internal/session-kernel.mjs.map +0 -1
  424. package/dist/internal/sql-oauth-connected-model.mjs +0 -7
  425. package/dist/internal/sql-oauth-connected-model.mjs.map +0 -1
  426. package/dist/internal/sql-query.d.mts +0 -12
  427. package/dist/internal/sql-query.mjs +0 -206
  428. package/dist/internal/sql-query.mjs.map +0 -1
  429. package/dist/internal/step-up-state.d.mts +0 -49
  430. package/dist/internal/transaction-execution-kernel.d.mts +0 -37
  431. package/dist/internal/transaction-execution-kernel.mjs +0 -120
  432. package/dist/internal/transaction-execution-kernel.mjs.map +0 -1
  433. package/dist/internal/transaction-kernel.d.mts +0 -81
  434. package/dist/internal/transaction-kernel.mjs +0 -264
  435. package/dist/internal/transaction-kernel.mjs.map +0 -1
  436. package/src/internal/PasswordPreparedPostconditions.ts +0 -30
  437. package/src/internal/authority-snapshot.ts +0 -91
  438. package/src/internal/email-kernel.ts +0 -1556
  439. package/src/internal/oauth/accounts.ts +0 -839
  440. package/src/internal/oauth/connected-access.ts +0 -388
  441. package/src/internal/oauth/connected-collection.ts +0 -345
  442. package/src/internal/oauth/connected-custody.ts +0 -308
  443. package/src/internal/oauth/connected-flow.ts +0 -562
  444. package/src/internal/oauth/connected-input.ts +0 -147
  445. package/src/internal/oauth/connected-keys.ts +0 -31
  446. package/src/internal/oauth/connected-maintenance.ts +0 -379
  447. package/src/internal/oauth/connected-management.ts +0 -391
  448. package/src/internal/oauth/connected-reference.ts +0 -135
  449. package/src/internal/oauth/connected-settlement.ts +0 -278
  450. package/src/internal/oauth/connected-sign-in.ts +0 -472
  451. package/src/internal/oauth/connected-state.ts +0 -924
  452. package/src/internal/oauth/connected-target.ts +0 -307
  453. package/src/internal/oauth/flow.ts +0 -505
  454. package/src/internal/oauth/input.ts +0 -201
  455. package/src/internal/oauth/owner.ts +0 -22
  456. package/src/internal/oauth/query-compiler.ts +0 -15
  457. package/src/internal/oauth/registration.ts +0 -676
  458. package/src/internal/oauth/target.ts +0 -803
  459. package/src/internal/oauth-kernel.ts +0 -294
  460. package/src/internal/passkey/admission.ts +0 -368
  461. package/src/internal/passkey/credentials.ts +0 -511
  462. package/src/internal/passkey/enrollment.ts +0 -326
  463. package/src/internal/passkey/flow.ts +0 -1062
  464. package/src/internal/passkey/management.ts +0 -426
  465. package/src/internal/passkey/registration-ceremony.ts +0 -283
  466. package/src/internal/passkey/registration-custody.ts +0 -91
  467. package/src/internal/passkey/registration-write.ts +0 -426
  468. package/src/internal/passkey/state.ts +0 -351
  469. package/src/internal/passkey/target.ts +0 -598
  470. package/src/internal/passkey/write-state.ts +0 -692
  471. package/src/internal/passkey/write-target.ts +0 -450
  472. package/src/internal/passkey-kernel.ts +0 -112
  473. package/src/internal/passkeys.ts +0 -652
  474. package/src/internal/password-kernel.ts +0 -1523
  475. package/src/internal/phone-kernel.ts +0 -872
  476. package/src/internal/proof-kernel.ts +0 -1992
  477. package/src/internal/query-operations.ts +0 -91
  478. package/src/internal/session-kernel.ts +0 -2406
  479. package/src/internal/sql-query.ts +0 -304
  480. package/src/internal/transaction-execution-kernel.ts +0 -335
  481. package/src/internal/transaction-kernel.ts +0 -603
@@ -1,2406 +0,0 @@
1
- import {
2
- coordinateCommit,
3
- CurrentCommitJournal,
4
- hasCommitScope,
5
- LifecycleHooks,
6
- } from "@yielded/auth/Hooks";
7
- import { reportPersistenceFailure } from "@yielded/auth/Persistence";
8
- import type { SubjectId, TokenDigest } from "@yielded/auth/Schema";
9
- import {
10
- assessAuthentication,
11
- AuthenticationCapture,
12
- snapshotSessionAuthenticationProvenance,
13
- PendingAuthenticationInvalid,
14
- SessionConflict,
15
- SessionInvalid,
16
- SessionStepUpInvalid,
17
- SessionUnavailable,
18
- StaleAuthentication,
19
- type AuthenticationEvidence,
20
- type AuthenticationRevision,
21
- type PendingConsumption,
22
- type SecurityRevision,
23
- type SessionInspection,
24
- SessionMetadata,
25
- pendingAuthenticationContext,
26
- snapshotPendingAuthenticationContext,
27
- type PendingAuthentication,
28
- type PendingAuthenticationRecord,
29
- type PendingAuthenticationState,
30
- type AuthenticationAuthority,
31
- type PrepareSessionCommit,
32
- type SessionRepository,
33
- type SignedSessionValidity,
34
- type StatefulSessionPersistence,
35
- type StatefulSessionRecord,
36
- type SessionStepUpIntent,
37
- type SessionStepUpPersistence,
38
- } from "@yielded/auth/Sessions";
39
- import { Cause, Context, DateTime, Effect, Option, Schema } from "effect";
40
- import type { SqlClient } from "effect/sql/SqlClient";
41
- import type * as SqlError from "effect/sql/SqlError";
42
-
43
- import { makeAuthoritySnapshot } from "./authority-snapshot";
44
- import { PersistenceMappingError, isMappedConstraintConflict } from "./mapping-error";
45
- import type {
46
- AuthenticationAuthorityMapping,
47
- PendingAuthenticationTables,
48
- PendingAuthenticationMapping,
49
- SessionAuthorityTables,
50
- SessionFlowTables,
51
- SessionSubjectTables,
52
- SignedSessionValidityMapping,
53
- StatefulSessionMapping,
54
- } from "./models/session-model";
55
- import type { SessionStepUpMapping } from "./models/step-up-model";
56
- import { makeNativeSqlTables, sqlMapping, type NativeSqlTables } from "./native-sql-table";
57
- /* oxlint-disable no-explicit-any -- existing storage kernels erase foreign table shapes; domain errors remain typed. */
58
- import type { QueryFailure } from "./query-operations";
59
- import type { QueryOperations } from "./query-operations";
60
- import {
61
- decodeStepUpIntent,
62
- encodeStepUpIntent,
63
- sameStepUpRevision,
64
- stepUpIntentLive,
65
- validateStepUpPlan,
66
- stepUpRotationMatches,
67
- } from "./step-up-state";
68
- import { canJoinTextColumns } from "./storage-validation";
69
- import { NativeDatabase } from "./transaction-kernel";
70
-
71
- const decodeRevocationFlag = Schema.decodeUnknownEffect(Schema.Literals([0, 1]));
72
-
73
- /** Reads need no transaction of their own. A caller-owned transaction still
74
- * needs a savepoint so catching an SQL failure does not leave it aborted. */
75
- const withVerificationSavepoint = Effect.fnUntraced(function* <A, E, R>(
76
- effect: Effect.Effect<A, E, R>,
77
- ): Effect.fn.Return<A, E | SqlError.SqlError, R | NativeDatabase> {
78
- const { $client } = yield* NativeDatabase;
79
- const transaction = yield* Effect.serviceOption($client.transactionService);
80
-
81
- return yield* Option.isSome(transaction) ? $client.withTransaction(effect) : effect;
82
- });
83
-
84
- type CommitMode = "interactive" | "synchronous";
85
-
86
- type AnySubjectMapping = SessionSubjectTables<any, any>;
87
-
88
- type AnyAuthorityMapping = SessionAuthorityTables<any, any, any>;
89
-
90
- type AnyFlowMapping = SessionFlowTables<any, any>;
91
-
92
- type AnyPendingMapping = AnyAuthorityMapping &
93
- AnyFlowMapping &
94
- PendingAuthenticationTables<any, any, any, any>;
95
-
96
- type AnyStepUpMapping = SessionStepUpMapping<any, any, any, any, any, any, any, any>;
97
-
98
- interface SessionSqlQuery<A = ReadonlyArray<any>> extends Effect.Effect<
99
- A,
100
- QueryFailure | SqlError.SqlError
101
- > {
102
- readonly from: (...args: ReadonlyArray<any>) => SessionSqlQuery<A>;
103
- readonly where: (...args: ReadonlyArray<any>) => SessionSqlQuery<A>;
104
- readonly limit: (...args: ReadonlyArray<any>) => SessionSqlQuery<A>;
105
- readonly orderBy: (...args: ReadonlyArray<any>) => SessionSqlQuery<A>;
106
- readonly for: (...args: ReadonlyArray<any>) => SessionSqlQuery<A>;
107
- readonly set: (...args: ReadonlyArray<any>) => SessionSqlQuery<A>;
108
- readonly values: (...args: ReadonlyArray<any>) => SessionSqlQuery<A>;
109
- }
110
-
111
- export interface SessionSqlDatabase {
112
- readonly select: (...args: ReadonlyArray<any>) => SessionSqlQuery;
113
- readonly insert: (...args: ReadonlyArray<any>) => SessionSqlQuery;
114
- readonly update: (...args: ReadonlyArray<any>) => SessionSqlQuery;
115
- readonly delete: (...args: ReadonlyArray<any>) => SessionSqlQuery;
116
- readonly transaction: <A, E, R>(
117
- body: (transaction: SessionSqlDatabase) => Effect.Effect<A, E, R>,
118
- ) => Effect.Effect<A, E | SqlError.SqlError, R>;
119
- }
120
-
121
- type Database = SessionSqlDatabase;
122
-
123
- /** Native SQL authority for the current transaction or savepoint. */
124
- export class CurrentSessionSql extends Context.Service<CurrentSessionSql, SessionSqlDatabase>()(
125
- "effect-auth/drizzle/CurrentSessionSql",
126
- ) {}
127
-
128
- type Guard = Effect.Effect<void, SessionUnavailable>;
129
-
130
- export interface SessionSqlOptions {
131
- readonly coordinated?: boolean;
132
- readonly mode: CommitMode;
133
- readonly locking: boolean;
134
- /** Target-specific ambient transaction preflight. It runs before IDs or writes. */
135
- readonly standaloneGuard: Guard;
136
- }
137
-
138
- type SessionDomainError =
139
- | SessionStepUpInvalid
140
- | PendingAuthenticationInvalid
141
- | SessionConflict
142
- | SessionInvalid
143
- | SessionUnavailable
144
- | StaleAuthentication;
145
-
146
- type AdapterFailure =
147
- | QueryFailure
148
- | PersistenceMappingError
149
- | Schema.SchemaError
150
- | SqlError.SqlError;
151
-
152
- type NormalizedSessionFailure<E> = E extends SessionDomainError ? E : SessionUnavailable;
153
-
154
- type PendingProjection = "record" | "context";
155
-
156
- type SqlStepUpRejectAbsence = SessionStepUpInvalid | StaleAuthentication;
157
-
158
- type SqlStepUpReadFailure = AdapterFailure | SqlStepUpRejectAbsence | SessionUnavailable;
159
-
160
- export const makeSessionKernel = (
161
- operations: QueryOperations,
162
- nativeTables: (client: SqlClient, database: object) => NativeSqlTables = (client) =>
163
- makeNativeSqlTables(client),
164
- ) => {
165
- const { and, eq, gt, lte, inArray, or, sql, column, updateValues } = operations;
166
- const unavailable = () => SessionUnavailable.make({});
167
-
168
- const stale = () => StaleAuthentication.make({});
169
-
170
- const invalidPending = () => PendingAuthenticationInvalid.make({});
171
-
172
- const invalidSession = () => SessionInvalid.make({});
173
-
174
- const mapFailureCause = <A, E, R, E2>(
175
- effect: Effect.Effect<A, E, R>,
176
- map: (error: E) => E2,
177
- ): Effect.Effect<A, E2, R> =>
178
- Effect.catchCause(effect, (cause) => Effect.failCause(Cause.map(cause, map)));
179
-
180
- const isExpectedMutationError = (error: unknown): error is SessionDomainError => {
181
- const tag =
182
- typeof error === "object" && error !== null ? Reflect.get(error, "_tag") : undefined;
183
-
184
- return (
185
- tag === "SessionStepUpInvalid" ||
186
- tag === "StaleAuthentication" ||
187
- tag === "PendingAuthenticationInvalid" ||
188
- tag === "SessionConflict" ||
189
- tag === "SessionInvalid" ||
190
- tag === "SessionUnavailable"
191
- );
192
- };
193
-
194
- const normalizeMutation =
195
- (mapping: any) =>
196
- <A, E, R>(effect: Effect.Effect<A, E, R>) =>
197
- effect.pipe(
198
- Effect.catchCause((cause) =>
199
- Effect.failCause(
200
- Cause.map(cause, (error) =>
201
- isMappedConstraintConflict(mapping.isConstraintConflict, error)
202
- ? SessionConflict.make({})
203
- : error,
204
- ),
205
- ),
206
- ),
207
- );
208
-
209
- const safeTransaction = <A, E, R>(
210
- effect: Effect.Effect<A, E, R>,
211
- ): Effect.Effect<A, NormalizedSessionFailure<E> | SessionUnavailable, R> =>
212
- reportPersistenceFailure(effect, isExpectedMutationError).pipe(
213
- Effect.catchCause((cause) =>
214
- Effect.failCause(
215
- Cause.map(
216
- cause,
217
- (error): NormalizedSessionFailure<E> =>
218
- (isExpectedMutationError(error)
219
- ? error
220
- : unavailable()) as NormalizedSessionFailure<E>,
221
- ),
222
- ),
223
- ),
224
- );
225
-
226
- const selectRows = (
227
- query: SessionSqlQuery,
228
- locking: boolean,
229
- ): Effect.Effect<ReadonlyArray<any>, AdapterFailure> =>
230
- locking && typeof query.for === "function" ? query.for("update") : query;
231
-
232
- const inTransaction = <A, E, R>(
233
- database: Database,
234
- effect: Effect.Effect<A, E, R | CurrentSessionSql>,
235
- ): Effect.Effect<A, E | SqlError.SqlError, Exclude<R, CurrentSessionSql>> =>
236
- Effect.suspend(() =>
237
- database.transaction((current) =>
238
- effect.pipe(Effect.provideService(CurrentSessionSql, current)),
239
- ),
240
- );
241
-
242
- const owned = <A, E, R>(
243
- database: Database,
244
- options: SessionSqlOptions,
245
- isConstraintConflict: (cause: unknown) => boolean,
246
- body: Effect.Effect<A, E, R | CurrentSessionSql | CurrentCommitJournal>,
247
- ): Effect.Effect<
248
- A,
249
- NormalizedSessionFailure<E> | SessionUnavailable,
250
- Exclude<Exclude<R, CurrentSessionSql>, CurrentCommitJournal> | LifecycleHooks
251
- > => {
252
- const run = coordinateCommit(
253
- () =>
254
- database.transaction((current) =>
255
- body.pipe(Effect.provideService(CurrentSessionSql, current)),
256
- ),
257
- { mode: options.mode },
258
- ).pipe(
259
- (effect) =>
260
- reportPersistenceFailure(
261
- effect,
262
- (error) =>
263
- isExpectedMutationError(error) ||
264
- isMappedConstraintConflict(isConstraintConflict, error),
265
- ),
266
- Effect.map((result) => result.value),
267
- Effect.catchCause((cause) =>
268
- Effect.failCause(
269
- Cause.map(cause, (error): NormalizedSessionFailure<E> | SessionUnavailable =>
270
- isExpectedMutationError(error) ? (error as NormalizedSessionFailure<E>) : unavailable(),
271
- ),
272
- ),
273
- ),
274
- );
275
-
276
- return Effect.gen(function* () {
277
- const current = yield* Effect.serviceOption(CurrentSessionSql);
278
- const transactionBound = Option.isSome(current) && current.value === database;
279
-
280
- if (!transactionBound) {
281
- // A root service must never commit one database underneath another owner.
282
- if (yield* hasCommitScope) return yield* unavailable();
283
- yield* options.standaloneGuard;
284
- }
285
-
286
- // A tx-bound service still owns a savepoint and child journal. If the caller
287
- // catches its failure, both partial writes and prepared values are discarded.
288
- return yield* run;
289
- });
290
- };
291
-
292
- const uniqueStrings = (values: ReadonlyArray<string>): ReadonlyArray<string> =>
293
- [...new Set(values)].sort();
294
-
295
- const preservesRevision = (
296
- completed: AuthenticationEvidence,
297
- original: AuthenticationEvidence,
298
- ): boolean => {
299
- const current = new Map(
300
- completed.revision.credentials.map((item) => [item.credentialId, item.revision]),
301
- );
302
-
303
- return (
304
- completed.flowId === original.flowId &&
305
- completed.bindingDigest === original.bindingDigest &&
306
- completed.revision.subjectId === original.revision.subjectId &&
307
- completed.revision.securityRevision === original.revision.securityRevision &&
308
- original.revision.credentials.every(
309
- (item) => current.get(item.credentialId) === item.revision,
310
- ) &&
311
- original.proofs.every((expected) =>
312
- completed.proofs.some(
313
- (proof) =>
314
- proof.method === expected.method &&
315
- proof.credentialId === expected.credentialId &&
316
- proof.userVerified === expected.userVerified &&
317
- proof.phishingResistant === expected.phishingResistant &&
318
- DateTime.toEpochMillis(proof.verifiedAt) ===
319
- DateTime.toEpochMillis(expected.verifiedAt) &&
320
- proof.factors.length === expected.factors.length &&
321
- proof.factors.every((factor, index) => factor === expected.factors[index]),
322
- ),
323
- )
324
- );
325
- };
326
-
327
- const authorityColumns = (mapping: any) => ({
328
- subjectId: column(mapping.subject.table, mapping.subject.id),
329
- subjectStatus: column(mapping.subject.table, mapping.subject.status),
330
- subjectRevision: column(mapping.subject.table, mapping.subject.securityRevision),
331
- credentialSubjectId: column(mapping.credential.table, mapping.credential.subjectId),
332
- credentialId: column(mapping.credential.table, mapping.credential.credentialId),
333
- credentialRevision: column(mapping.credential.table, mapping.credential.revision),
334
- ...(mapping.credential.status === undefined
335
- ? {}
336
- : { credentialStatus: column(mapping.credential.table, mapping.credential.status) }),
337
- });
338
-
339
- const subjectColumns = (mapping: any) => ({
340
- subjectId: column(mapping.subject.table, mapping.subject.id),
341
- subjectStatus: column(mapping.subject.table, mapping.subject.status),
342
- subjectRevision: column(mapping.subject.table, mapping.subject.securityRevision),
343
- });
344
-
345
- const readSubject = Effect.fn("DrizzleSession.readSubject")(function* (
346
- mapping: AnySubjectMapping,
347
- nativeSubjectId: unknown,
348
- locking: boolean,
349
- ) {
350
- const database = yield* CurrentSessionSql;
351
- const columns = subjectColumns(mapping);
352
-
353
- return yield* selectRows(
354
- database
355
- .select()
356
- .from(mapping.subject.table)
357
- .where(eq(columns.subjectId, nativeSubjectId))
358
- .limit(1),
359
- locking,
360
- );
361
- });
362
-
363
- const captureDetailsIn = Effect.fn("DrizzleSession.captureDetails")(function* (
364
- mapping: AnyAuthorityMapping,
365
- subjectId: SubjectId,
366
- credentialIds: ReadonlyArray<string>,
367
- locking: boolean,
368
- ) {
369
- const database = yield* CurrentSessionSql;
370
- const requested = uniqueStrings(credentialIds);
371
-
372
- if (requested.length !== credentialIds.length) return yield* stale();
373
- const nativeSubjectId = yield* mapping.subjectId.toNative(subjectId);
374
- const subjectRows = yield* readSubject(mapping, nativeSubjectId, locking);
375
- const subject = subjectRows[0];
376
- const columns = authorityColumns(mapping);
377
-
378
- if (subject === undefined || !mapping.subject.isActiveStatus(subject[mapping.subject.status]))
379
- return yield* stale();
380
-
381
- const query = database
382
- .select()
383
- .from(mapping.credential.table)
384
- .where(
385
- and(
386
- eq(columns.credentialSubjectId, nativeSubjectId),
387
- requested.length === 0 ? sql`false` : inArray(columns.credentialId, requested),
388
- ),
389
- )
390
- .orderBy(columns.credentialId);
391
-
392
- const credentialRows = requested.length === 0 ? [] : yield* selectRows(query, locking);
393
-
394
- if (credentialRows.length !== requested.length) return yield* stale();
395
-
396
- const credentials = credentialRows
397
- .map((row: any) => ({
398
- credentialId: row[mapping.credential.credentialId] as string,
399
- revision: row[mapping.credential.revision] as SecurityRevision,
400
- active:
401
- mapping.credential.status === undefined ||
402
- mapping.credential.isActiveStatus?.(row[mapping.credential.status]) === true,
403
- }))
404
- .sort((left: any, right: any) => left.credentialId.localeCompare(right.credentialId));
405
-
406
- if (credentials.some((item: any) => !item.active)) return yield* stale();
407
-
408
- return {
409
- subject,
410
- revision: {
411
- subjectId,
412
- securityRevision: subject[mapping.subject.securityRevision] as SecurityRevision,
413
- credentials: credentials.map(({ credentialId, revision }: any) => ({
414
- credentialId,
415
- revision,
416
- })),
417
- } satisfies AuthenticationRevision,
418
- };
419
- });
420
-
421
- const validateEvidenceIn = Effect.fn("DrizzleSession.validateEvidence")(function* (
422
- mapping: AnyAuthorityMapping,
423
- evidence: AuthenticationEvidence,
424
- locking: boolean,
425
- ) {
426
- const captured = yield* captureDetailsIn(
427
- mapping,
428
- evidence.revision.subjectId,
429
- evidence.revision.credentials.map((item) => item.credentialId),
430
- locking,
431
- );
432
-
433
- return yield* validateCapturedEvidence(mapping, evidence, captured);
434
- });
435
-
436
- const validateCapturedEvidence = Effect.fnUntraced(function* (
437
- mapping: AnyAuthorityMapping,
438
- evidence: AuthenticationEvidence,
439
- captured: {
440
- readonly subject: Record<string, unknown>;
441
- readonly revision: AuthenticationRevision;
442
- },
443
- ) {
444
- if (
445
- captured.revision.securityRevision !== evidence.revision.securityRevision ||
446
- captured.revision.credentials.length !== evidence.revision.credentials.length ||
447
- captured.revision.credentials.some(
448
- (item: { credentialId: string; revision: SecurityRevision }, index: number) => {
449
- const expected = [...evidence.revision.credentials].sort((a, b) =>
450
- a.credentialId.localeCompare(b.credentialId),
451
- )[index];
452
-
453
- return (
454
- expected === undefined ||
455
- expected.credentialId !== item.credentialId ||
456
- expected.revision !== item.revision
457
- );
458
- },
459
- )
460
- )
461
- return yield* stale();
462
- const nativeSubjectId = yield* mapping.subjectId.toNative(evidence.revision.subjectId);
463
- const requirement = yield* mapping.subject.decodeRequirement(captured.subject);
464
-
465
- return { nativeSubjectId, requirement };
466
- });
467
-
468
- const flowColumns = (mapping: any) => ({
469
- flowId: column(mapping.flow.table, mapping.flow.flowId),
470
- subjectId: column(mapping.flow.table, mapping.flow.subjectId),
471
- state: column(mapping.flow.table, mapping.flow.state),
472
- pendingDigest: column(mapping.flow.table, mapping.flow.pendingDigest),
473
- dedupUntil: column(mapping.flow.table, mapping.flow.dedupUntil),
474
- });
475
-
476
- const readFlow = Effect.fn("DrizzleSession.readFlow")(function* (
477
- mapping: AnyFlowMapping,
478
- flowId: string,
479
- locking: boolean,
480
- ) {
481
- const database = yield* CurrentSessionSql;
482
- const columns = flowColumns(mapping);
483
-
484
- return yield* selectRows(
485
- database.select().from(mapping.flow.table).where(eq(columns.flowId, flowId)).limit(1),
486
- locking,
487
- );
488
- });
489
-
490
- const ensureDirectFlowAvailable = Effect.fn("DrizzleSession.ensureFlowAvailable")(function* (
491
- mapping: AnyAuthorityMapping &
492
- AnyFlowMapping & {
493
- readonly pending?: PendingAuthenticationTables<any, any, any, any>["pending"] | undefined;
494
- },
495
- evidence: AuthenticationEvidence,
496
- nativeSubjectId: unknown,
497
- dedupUntil: DateTime.Utc,
498
- ) {
499
- const database = yield* CurrentSessionSql;
500
- const rows = yield* readFlow(mapping, evidence.flowId, true);
501
- const existing = rows[0];
502
-
503
- if (existing !== undefined) {
504
- const now = yield* freshNow;
505
-
506
- if (
507
- DateTime.toEpochMillis(now) <
508
- DateTime.toEpochMillis(yield* mapping.flow.decodeInstant(existing[mapping.flow.dedupUntil]))
509
- )
510
- return yield* SessionConflict.make({});
511
- if (mapping.pending !== undefined)
512
- yield* database
513
- .delete(mapping.pending.table)
514
- .where(eq(column(mapping.pending.table, mapping.pending.flowId), evidence.flowId));
515
- yield* database
516
- .delete(mapping.flow.table)
517
- .where(eq(flowColumns(mapping).flowId, evidence.flowId));
518
- }
519
-
520
- return mapping.flow.encodeEstablishedInsert({
521
- evidence,
522
- subjectId: nativeSubjectId,
523
- dedupUntil,
524
- });
525
- });
526
-
527
- const pendingColumns = (mapping: any) => ({
528
- digest: column(mapping.pending.table, mapping.pending.digest),
529
- version: column(mapping.pending.table, mapping.pending.version),
530
- flowId: column(mapping.pending.table, mapping.pending.flowId),
531
- subjectId: column(mapping.pending.table, mapping.pending.subjectId),
532
- bindingDigest: column(mapping.pending.table, mapping.pending.bindingDigest),
533
- expiresAt: column(mapping.pending.table, mapping.pending.expiresAt),
534
- attemptLimit: column(mapping.pending.table, mapping.pending.attemptLimit),
535
- failedAttempts: column(mapping.pending.table, mapping.pending.failedAttempts),
536
- consumed: column(mapping.pending.table, mapping.pending.consumed),
537
- });
538
-
539
- const readPending = Effect.fn("DrizzleSession.readPending")(function* (
540
- mapping: AnyPendingMapping,
541
- digest: TokenDigest,
542
- locking: boolean,
543
- ) {
544
- const database = yield* CurrentSessionSql;
545
-
546
- return yield* selectRows(
547
- database
548
- .select()
549
- .from(mapping.pending.table)
550
- .where(eq(pendingColumns(mapping).digest, digest))
551
- .limit(1),
552
- locking,
553
- );
554
- });
555
-
556
- const validatePending = Effect.fn("DrizzleSession.validatePending")(function* <
557
- Record extends PendingAuthenticationState = PendingAuthenticationRecord<unknown>,
558
- >(
559
- mapping: AnyPendingMapping,
560
- input: PendingConsumption,
561
- projection: PendingProjection = "record",
562
- ) {
563
- const rows = yield* readPending(mapping, input.digest, true);
564
- const row = rows[0];
565
-
566
- if (row === undefined) return yield* invalidPending();
567
-
568
- const decode = (
569
- projection === "context" ? mapping.pending.decodeContext : mapping.pending.decode
570
- ) as (row: any) => Effect.Effect<Record, PersistenceMappingError>;
571
-
572
- const record = yield* decode(row);
573
- const failedAttempts = Number(row[mapping.pending.failedAttempts]);
574
-
575
- if (
576
- record.version !== input.version ||
577
- record.evidence.flowId !== input.flowId ||
578
- record.evidence.bindingDigest !== input.bindingDigest ||
579
- row[mapping.pending.consumed] !== false ||
580
- failedAttempts >= record.attemptLimit
581
- )
582
- return yield* invalidPending();
583
- const flows = yield* readFlow(mapping, input.flowId, true);
584
- const flow = flows[0];
585
-
586
- if (
587
- flow === undefined ||
588
- flow[mapping.flow.state] !== mapping.flow.pendingStateValue ||
589
- flow[mapping.flow.pendingDigest] !== input.digest
590
- )
591
- return yield* invalidPending();
592
- if (DateTime.toEpochMillis(yield* freshNow) >= DateTime.toEpochMillis(record.expiresAt))
593
- return yield* invalidPending();
594
-
595
- return record;
596
- });
597
-
598
- const consumePending = Effect.fn("DrizzleSession.consumePending")(function* (
599
- mapping: AnyPendingMapping,
600
- input: PendingConsumption,
601
- until: DateTime.Utc,
602
- ) {
603
- const database = yield* CurrentSessionSql;
604
-
605
- yield* Effect.all([
606
- database
607
- .update(mapping.pending.table)
608
- .set(updateValues([[mapping.pending.consumed, true]]))
609
- .where(
610
- and(
611
- eq(pendingColumns(mapping).digest, input.digest),
612
- eq(pendingColumns(mapping).version, input.version),
613
- eq(pendingColumns(mapping).consumed, false),
614
- ),
615
- ),
616
-
617
- database
618
- .update(mapping.flow.table)
619
- .set(
620
- updateValues([
621
- [mapping.flow.state, mapping.flow.establishedStateValue],
622
- [mapping.flow.pendingDigest, null],
623
- [mapping.flow.dedupUntil, mapping.flow.encodeInstant(until)],
624
- ]),
625
- )
626
- .where(eq(flowColumns(mapping).flowId, input.flowId)),
627
- ]);
628
- });
629
-
630
- const freshNow = DateTime.now;
631
-
632
- const allocate = <A>(
633
- mode: CommitMode,
634
- asynchronous: Effect.Effect<A, PersistenceMappingError> | undefined,
635
- synchronous: (() => A) | undefined,
636
- ): Effect.Effect<A, PersistenceMappingError | SessionUnavailable> => {
637
- if (mode === "synchronous")
638
- return synchronous === undefined
639
- ? Effect.fail(unavailable())
640
- : Effect.try({
641
- try: synchronous,
642
- catch: (cause) => PersistenceMappingError.make({ operation: "allocate", cause }),
643
- });
644
- if (asynchronous !== undefined) return asynchronous;
645
-
646
- return synchronous === undefined
647
- ? Effect.fail(unavailable())
648
- : Effect.try({
649
- try: synchronous,
650
- catch: (cause) => PersistenceMappingError.make({ operation: "allocate", cause }),
651
- });
652
- };
653
-
654
- const makeSqlAuthenticationAuthority = Effect.fnUntraced(function* <Claims>(
655
- mapping: AuthenticationAuthorityMapping<Claims, any, any, any, any, any>,
656
- options: SessionSqlOptions,
657
- ): Effect.fn.Return<
658
- AuthenticationAuthority["Service"],
659
- SessionUnavailable,
660
- LifecycleHooks | CurrentSessionSql | NativeDatabase
661
- > {
662
- const database = yield* CurrentSessionSql;
663
-
664
- if (!options.coordinated)
665
- yield* (operations.validateStorage?.(mapping) ?? Effect.void).pipe(
666
- Effect.mapError(unavailable),
667
- );
668
- const hooks = yield* LifecycleHooks;
669
-
670
- const root = yield* NativeDatabase;
671
- const sqlClient = root.$client.withoutTransforms();
672
-
673
- const tables = yield* sqlMapping(() => nativeTables(sqlClient, root)).pipe(
674
- Effect.mapError(unavailable),
675
- );
676
-
677
- const readSnapshot = makeAuthoritySnapshot(tables, mapping);
678
-
679
- const service = {
680
- capture: (subjectId: SubjectId, credentialIds: ReadonlyArray<string>) =>
681
- safeTransaction(
682
- readSnapshot(subjectId, credentialIds).pipe(
683
- Effect.flatMap((value) =>
684
- Effect.flatMap(mapping.subject.decodeRequirement(value.subject), (requirement) =>
685
- Schema.decodeEffect(AuthenticationCapture)({
686
- revision: value.revision,
687
- requirement,
688
- }),
689
- ),
690
- ),
691
- ),
692
- ),
693
- requirements: (evidence: AuthenticationEvidence) =>
694
- safeTransaction(
695
- readSnapshot(
696
- evidence.revision.subjectId,
697
- evidence.revision.credentials.map((item) => item.credentialId),
698
- ).pipe(
699
- Effect.flatMap((captured) => validateCapturedEvidence(mapping, evidence, captured)),
700
- Effect.map((value) => value.requirement),
701
- ),
702
- ),
703
- approve: <A>(
704
- input: Parameters<AuthenticationAuthority["Service"]["approve"]>[0],
705
- prepare: PrepareSessionCommit<void, A>,
706
- ) =>
707
- owned(
708
- database,
709
- options,
710
- mapping.isConstraintConflict,
711
- Effect.gen(function* () {
712
- const journal = yield* CurrentCommitJournal;
713
-
714
- // With row locks, lock the subject before its credentials in separate
715
- // reads: a joined FOR UPDATE does not fix that order on every table
716
- // layout, and a coordinated application may already hold the subject
717
- // lock. Without row locks one native snapshot is already serialized.
718
- const { requirement } = yield* options.locking
719
- ? validateEvidenceIn(mapping, input.evidence, true)
720
- : Effect.flatMap(
721
- readSnapshot(
722
- input.evidence.revision.subjectId,
723
- input.evidence.revision.credentials.map((item) => item.credentialId),
724
- ),
725
- (captured) => validateCapturedEvidence(mapping, input.evidence, captured),
726
- );
727
-
728
- if (input.pending === undefined) {
729
- const assessed = yield* assessAuthentication(input.evidence, requirement).pipe(
730
- Effect.mapError(unavailable),
731
- );
732
-
733
- const now = yield* freshNow;
734
-
735
- if (
736
- !assessed.satisfied ||
737
- DateTime.toEpochMillis(now) >= DateTime.toEpochMillis(input.expiresAt) ||
738
- DateTime.toEpochMillis(input.expiresAt) >
739
- DateTime.toEpochMillis(input.absoluteExpiresAt)
740
- )
741
- return yield* stale();
742
-
743
- return prepare(undefined, journal);
744
- }
745
- if (mapping.pending === undefined) return yield* invalidPending();
746
- const pendingMapping = { ...mapping, ...mapping.pending };
747
- const pending = yield* validatePending(pendingMapping, input.pending);
748
-
749
- if (!preservesRevision(input.evidence, pending.evidence))
750
- return yield* invalidPending();
751
-
752
- const assessed = yield* assessAuthentication(input.evidence, requirement).pipe(
753
- Effect.mapError(unavailable),
754
- );
755
-
756
- const now = yield* freshNow;
757
-
758
- if (
759
- !assessed.satisfied ||
760
- DateTime.toEpochMillis(now) >= DateTime.toEpochMillis(pending.expiresAt) ||
761
- DateTime.toEpochMillis(now) >= DateTime.toEpochMillis(input.expiresAt) ||
762
- DateTime.toEpochMillis(input.expiresAt) >
763
- DateTime.toEpochMillis(input.absoluteExpiresAt)
764
- )
765
- return yield* stale();
766
- const receipt = prepare(undefined, journal);
767
-
768
- yield* consumePending(pendingMapping, input.pending, input.absoluteExpiresAt);
769
-
770
- return receipt;
771
- }),
772
- ),
773
- };
774
-
775
- return {
776
- capture: (subjectId, credentialIds) =>
777
- service
778
- .capture(subjectId, credentialIds)
779
- .pipe(
780
- Effect.provideService(NativeDatabase, root),
781
- Effect.provideService(LifecycleHooks, hooks),
782
- ),
783
- requirements: (evidence) =>
784
- service
785
- .requirements(evidence)
786
- .pipe(
787
- Effect.provideService(NativeDatabase, root),
788
- Effect.provideService(LifecycleHooks, hooks),
789
- ),
790
- approve: (input, prepare) =>
791
- service
792
- .approve(input, prepare)
793
- .pipe(
794
- Effect.provideService(NativeDatabase, root),
795
- Effect.provideService(LifecycleHooks, hooks),
796
- ),
797
- } satisfies AuthenticationAuthority["Service"];
798
- });
799
-
800
- /** The same locked security checks serve full consumption reads and the
801
- * claims-free additional-factor context. No lock spans proof verification. */
802
- const readAuthenticatedPending = Effect.fn("DrizzleSession.readAuthenticatedPending")(function* <
803
- Record extends PendingAuthenticationState,
804
- >(
805
- mapping: AnyPendingMapping,
806
- input: { readonly digest: TokenDigest; readonly bindingDigest?: TokenDigest },
807
- projection: PendingProjection,
808
- ) {
809
- const initial = (yield* readPending(mapping, input.digest, false))[0];
810
-
811
- if (initial === undefined) return yield* invalidPending();
812
- const initialNativeSubjectId = initial[mapping.pending.subjectId];
813
- const subject = (yield* readSubject(mapping, initialNativeSubjectId, true))[0];
814
-
815
- if (subject === undefined) return yield* invalidPending();
816
- const current = (yield* readPending(mapping, input.digest, true))[0];
817
-
818
- if (
819
- current === undefined ||
820
- !mapping.subjectId.equals(current[mapping.pending.subjectId], initialNativeSubjectId)
821
- )
822
- return yield* invalidPending();
823
-
824
- const decode = (
825
- projection === "context" ? mapping.pending.decodeContext : mapping.pending.decode
826
- ) as (row: any) => Effect.Effect<Record, PersistenceMappingError>;
827
-
828
- const record = yield* decode(current);
829
- const nativeSubjectId = yield* mapping.subjectId.toNative(record.evidence.revision.subjectId);
830
- const failedAttempts = Number(current[mapping.pending.failedAttempts]);
831
-
832
- if (
833
- !mapping.subjectId.equals(nativeSubjectId, initialNativeSubjectId) ||
834
- (input.bindingDigest !== undefined &&
835
- record.evidence.bindingDigest !== input.bindingDigest) ||
836
- current[mapping.pending.version] !== record.version ||
837
- current[mapping.pending.flowId] !== record.evidence.flowId ||
838
- current[mapping.pending.bindingDigest] !== record.evidence.bindingDigest ||
839
- current[mapping.pending.consumed] !== false ||
840
- failedAttempts >= record.attemptLimit
841
- )
842
- return yield* invalidPending();
843
- const flow = (yield* readFlow(mapping, record.evidence.flowId, true))[0];
844
-
845
- if (
846
- flow === undefined ||
847
- flow[mapping.flow.state] !== mapping.flow.pendingStateValue ||
848
- flow[mapping.flow.pendingDigest] !== input.digest
849
- )
850
- return yield* invalidPending();
851
-
852
- const { requirement } = yield* mapFailureCause(
853
- validateEvidenceIn(mapping, record.evidence, true),
854
- (error) => (Schema.is(StaleAuthentication)(error) ? invalidPending() : error),
855
- );
856
-
857
- yield* assessAuthentication(record.evidence, requirement).pipe(
858
- Effect.mapError(() => invalidPending()),
859
- );
860
- if (DateTime.toEpochMillis(yield* freshNow) >= DateTime.toEpochMillis(record.expiresAt))
861
- return yield* invalidPending();
862
-
863
- return record;
864
- });
865
-
866
- const makeSqlPendingAuthentication = Effect.fnUntraced(function* <Claims>(
867
- mapping: PendingAuthenticationMapping<Claims, any, any, any, any, any>,
868
- options: SessionSqlOptions,
869
- ): Effect.fn.Return<
870
- PendingAuthentication<Claims>,
871
- SessionUnavailable,
872
- LifecycleHooks | CurrentSessionSql | NativeDatabase
873
- > {
874
- const database = yield* CurrentSessionSql;
875
-
876
- if (!options.coordinated)
877
- yield* (operations.validateStorage?.(mapping) ?? Effect.void).pipe(
878
- Effect.mapError(unavailable),
879
- );
880
- const hooks = yield* LifecycleHooks;
881
-
882
- const service = {
883
- create: <A>(
884
- input: Omit<PendingAuthenticationRecord<Claims>, "version">,
885
- _inputNow: DateTime.Utc,
886
- prepare: PrepareSessionCommit<PendingAuthenticationRecord<Claims>, A>,
887
- ) =>
888
- owned(
889
- database,
890
- options,
891
- mapping.isConstraintConflict,
892
- Effect.gen(function* () {
893
- const transaction = yield* CurrentSessionSql;
894
- const journal = yield* CurrentCommitJournal;
895
-
896
- const { nativeSubjectId, requirement } = yield* validateEvidenceIn(
897
- mapping,
898
- input.evidence,
899
- true,
900
- );
901
-
902
- const existing = (yield* readFlow(mapping, input.evidence.flowId, true))[0];
903
-
904
- if (existing !== undefined) {
905
- const dedupUntil = yield* mapping.flow.decodeInstant(
906
- existing[mapping.flow.dedupUntil],
907
- );
908
-
909
- if (DateTime.toEpochMillis(yield* freshNow) < DateTime.toEpochMillis(dedupUntil))
910
- return yield* SessionConflict.make({});
911
- yield* transaction
912
- .delete(mapping.pending.table)
913
- .where(eq(pendingColumns(mapping).flowId, input.evidence.flowId));
914
- yield* transaction
915
- .delete(mapping.flow.table)
916
- .where(eq(flowColumns(mapping).flowId, input.evidence.flowId));
917
- }
918
-
919
- const version = yield* allocate(
920
- options.mode,
921
- mapping.pending.allocateVersion,
922
- mapping.pending.allocateVersionSync,
923
- );
924
-
925
- yield* assessAuthentication(input.evidence, requirement);
926
- const now = yield* freshNow;
927
-
928
- if (DateTime.toEpochMillis(now) >= DateTime.toEpochMillis(input.expiresAt))
929
- return yield* stale();
930
- const record = { ...input, version };
931
- const receipt = prepare(record, journal);
932
-
933
- yield* transaction.insert(mapping.flow.table).values(
934
- mapping.flow.encodePendingInsert({
935
- evidence: input.evidence,
936
- subjectId: nativeSubjectId,
937
- pendingDigest: input.digest,
938
- dedupUntil: input.expiresAt,
939
- }),
940
- );
941
- yield* transaction.insert(mapping.pending.table).values(
942
- mapping.pending.encodeInsert(record, {
943
- subjectId: nativeSubjectId,
944
- failedAttempts: 0,
945
- consumed: false,
946
- }),
947
- );
948
-
949
- return receipt;
950
- }).pipe(normalizeMutation(mapping)),
951
- ),
952
- context: (input: { readonly digest: TokenDigest; readonly now: DateTime.Utc }) =>
953
- safeTransaction(
954
- inTransaction(
955
- database,
956
- readAuthenticatedPending<PendingAuthenticationState>(mapping, input, "context"),
957
- ),
958
- ).pipe(Effect.flatMap(pendingAuthenticationContext)),
959
- read: (input: {
960
- readonly digest: TokenDigest;
961
- readonly bindingDigest: TokenDigest;
962
- readonly now: DateTime.Utc;
963
- }) =>
964
- safeTransaction(
965
- inTransaction(
966
- database,
967
- readAuthenticatedPending<PendingAuthenticationRecord<Claims>>(mapping, input, "record"),
968
- ),
969
- ).pipe(Effect.map((record) => record as PendingAuthenticationRecord<Claims>)),
970
- reject: <A>(
971
- input: Parameters<PendingAuthentication<Claims>["reject"]>[0],
972
- prepare: PrepareSessionCommit<{ readonly _tag: "Rejected" }, A>,
973
- ) =>
974
- owned(
975
- database,
976
- options,
977
- mapping.isConstraintConflict,
978
- Effect.gen(function* () {
979
- const transaction = yield* CurrentSessionSql;
980
- const journal = yield* CurrentCommitJournal;
981
- const initial = (yield* readPending(mapping, input.digest, false))[0];
982
-
983
- if (initial === undefined) return prepare({ _tag: "Rejected" }, journal);
984
- const record = yield* mapping.pending.decodeContext(initial);
985
-
986
- const nativeSubjectId = yield* mapping.subjectId.toNative(
987
- record.evidence.revision.subjectId,
988
- );
989
-
990
- const subject = (yield* readSubject(mapping, nativeSubjectId, true))[0];
991
- const current = (yield* readPending(mapping, input.digest, true))[0];
992
- const now = yield* freshNow;
993
- const receipt = prepare({ _tag: "Rejected" }, journal);
994
-
995
- if (
996
- subject !== undefined &&
997
- mapping.subject.isActiveStatus(subject[mapping.subject.status]) &&
998
- subject[mapping.subject.securityRevision] ===
999
- record.evidence.revision.securityRevision &&
1000
- current !== undefined &&
1001
- current[mapping.pending.consumed] === false &&
1002
- record.evidence.bindingDigest === input.bindingDigest &&
1003
- DateTime.toEpochMillis(now) < DateTime.toEpochMillis(record.expiresAt)
1004
- ) {
1005
- yield* transaction
1006
- .update(mapping.pending.table)
1007
- .set(
1008
- updateValues([
1009
- [
1010
- mapping.pending.failedAttempts,
1011
- sql`case when ${pendingColumns(mapping).failedAttempts} < ${pendingColumns(mapping).attemptLimit} then ${pendingColumns(mapping).failedAttempts} + 1 else ${pendingColumns(mapping).failedAttempts} end`,
1012
- ],
1013
- ]),
1014
- )
1015
- .where(eq(pendingColumns(mapping).digest, input.digest));
1016
- }
1017
-
1018
- return receipt;
1019
- }),
1020
- ),
1021
- };
1022
-
1023
- return {
1024
- create: (input, now, prepare) =>
1025
- service.create(input, now, prepare).pipe(Effect.provideService(LifecycleHooks, hooks)),
1026
- context: (input) => service.context(input).pipe(Effect.provideService(LifecycleHooks, hooks)),
1027
- read: (input) => service.read(input).pipe(Effect.provideService(LifecycleHooks, hooks)),
1028
- reject: (input, prepare) =>
1029
- service.reject(input, prepare).pipe(Effect.provideService(LifecycleHooks, hooks)),
1030
- } satisfies PendingAuthentication<Claims>;
1031
- });
1032
-
1033
- const sessionColumns = (mapping: any) => ({
1034
- sessionId: column(mapping.session.table, mapping.session.sessionId),
1035
- subjectId: column(mapping.session.table, mapping.session.subjectId),
1036
- digest: column(mapping.session.table, mapping.session.digest),
1037
- version: column(mapping.session.table, mapping.session.version),
1038
- securityRevision: column(mapping.session.table, mapping.session.securityRevision),
1039
- issuedAt: column(mapping.session.table, mapping.session.issuedAt),
1040
- expiresAt: column(mapping.session.table, mapping.session.expiresAt),
1041
- absoluteExpiresAt: column(mapping.session.table, mapping.session.absoluteExpiresAt),
1042
- });
1043
-
1044
- const makeSqlStatefulSessions = Effect.fnUntraced(function* <Claims>(
1045
- mapping: StatefulSessionMapping<Claims, any, any, any, any, any, any, any>,
1046
- options: SessionSqlOptions,
1047
- ): Effect.fn.Return<
1048
- {
1049
- readonly statefulSessionPersistence: StatefulSessionPersistence<Claims>;
1050
- readonly sessionRepository: SessionRepository;
1051
- },
1052
- SessionUnavailable,
1053
- LifecycleHooks | CurrentSessionSql | NativeDatabase
1054
- > {
1055
- const database = yield* CurrentSessionSql;
1056
-
1057
- if (!options.coordinated)
1058
- yield* (operations.validateStorage?.(mapping) ?? Effect.void).pipe(
1059
- Effect.mapError(unavailable),
1060
- );
1061
- const hooks = yield* LifecycleHooks;
1062
-
1063
- const root = yield* NativeDatabase;
1064
-
1065
- const tables = yield* sqlMapping(() =>
1066
- nativeTables(root.$client.withoutTransforms(), root),
1067
- ).pipe(Effect.mapError(unavailable));
1068
-
1069
- const readSnapshot = makeAuthoritySnapshot(tables, mapping);
1070
- const c = sessionColumns(mapping);
1071
-
1072
- const verification = yield* safeTransaction(
1073
- Effect.gen(function* () {
1074
- const { session, subject } = yield* sqlMapping(() => ({
1075
- session: tables(mapping.session.table).as("auth_session"),
1076
- subject: tables(mapping.subject.table).as("auth_subject"),
1077
- }));
1078
-
1079
- const [sessionOwner, subjectOwner] = yield* sqlMapping(() => [
1080
- session.unencodedTextColumn?.(mapping.session.subjectId),
1081
- subject.unencodedTextColumn?.(mapping.subject.id),
1082
- ]);
1083
-
1084
- const canJoinOwner = yield* canJoinTextColumns(sessionOwner, subjectOwner);
1085
-
1086
- return { session, subject, canJoinOwner };
1087
- }),
1088
- );
1089
-
1090
- const persistence = {
1091
- establish: <A>(
1092
- input: Parameters<StatefulSessionPersistence<Claims>["establish"]>[0],
1093
- prepare: PrepareSessionCommit<StatefulSessionRecord<Claims>, A>,
1094
- ) =>
1095
- owned(
1096
- database,
1097
- options,
1098
- mapping.isConstraintConflict,
1099
- Effect.gen(function* () {
1100
- const transaction = yield* CurrentSessionSql;
1101
- const journal = yield* CurrentCommitJournal;
1102
-
1103
- // Ordered locks with row locks; one serialized snapshot without (see approve).
1104
- const { nativeSubjectId, requirement } = yield* options.locking
1105
- ? validateEvidenceIn(mapping, input.evidence, true)
1106
- : Effect.flatMap(
1107
- readSnapshot(
1108
- input.evidence.revision.subjectId,
1109
- input.evidence.revision.credentials.map((item) => item.credentialId),
1110
- ),
1111
- (captured) => validateCapturedEvidence(mapping, input.evidence, captured),
1112
- );
1113
-
1114
- let flowInsert: unknown | undefined;
1115
- let pendingExpiresAt: DateTime.Utc | undefined;
1116
-
1117
- if (input.pending === undefined) {
1118
- if (input.fresh !== true || input.handoffSourceSessionId !== undefined)
1119
- flowInsert = yield* ensureDirectFlowAvailable(
1120
- mapping,
1121
- input.evidence,
1122
- nativeSubjectId,
1123
- input.session.absoluteExpiresAt,
1124
- );
1125
- } else {
1126
- if (mapping.pending === undefined) return yield* invalidPending();
1127
- const pending = yield* validatePending(mapping, input.pending);
1128
-
1129
- if (!preservesRevision(input.evidence, pending.evidence))
1130
- return yield* invalidPending();
1131
- pendingExpiresAt = pending.expiresAt;
1132
- }
1133
-
1134
- const nativeSessionId = yield* allocate(
1135
- options.mode,
1136
- mapping.session.allocateId,
1137
- mapping.session.allocateIdSync,
1138
- );
1139
-
1140
- const sessionId = yield* mapping.sessionId.toSession(nativeSessionId);
1141
-
1142
- if (sessionId === input.handoffSourceSessionId) return yield* SessionConflict.make({});
1143
-
1144
- const version = yield* allocate(
1145
- options.mode,
1146
- mapping.session.allocateVersion,
1147
- mapping.session.allocateVersionSync,
1148
- );
1149
-
1150
- const assessed = yield* assessAuthentication(input.evidence, requirement);
1151
- const now = yield* freshNow;
1152
-
1153
- if (
1154
- !assessed.satisfied ||
1155
- (pendingExpiresAt !== undefined &&
1156
- DateTime.toEpochMillis(now) >= DateTime.toEpochMillis(pendingExpiresAt)) ||
1157
- DateTime.toEpochMillis(now) >= DateTime.toEpochMillis(input.session.expiresAt) ||
1158
- DateTime.toEpochMillis(input.session.expiresAt) >
1159
- DateTime.toEpochMillis(input.session.absoluteExpiresAt)
1160
- )
1161
- return yield* stale();
1162
-
1163
- const record = {
1164
- ...input.session,
1165
- sessionId,
1166
- version,
1167
- subjectId: input.evidence.revision.subjectId,
1168
- securityRevision: input.evidence.revision.securityRevision,
1169
- assurance:
1170
- input.handoffSourceSessionId === undefined
1171
- ? assessed.assurance
1172
- : input.session.assurance,
1173
- provenance: yield* snapshotSessionAuthenticationProvenance({
1174
- evidence: input.evidence,
1175
- }),
1176
- issuedAt: now,
1177
- };
1178
-
1179
- const receipt = prepare(record, journal);
1180
-
1181
- if (flowInsert !== undefined)
1182
- yield* transaction.insert(mapping.flow.table).values(flowInsert);
1183
- else if (input.pending !== undefined)
1184
- yield* consumePending(
1185
- { ...mapping, pending: mapping.pending! },
1186
- input.pending!,
1187
- input.session.absoluteExpiresAt,
1188
- );
1189
- yield* transaction.insert(mapping.session.table).values(
1190
- mapping.session.encodeInsert(record, {
1191
- subjectId: nativeSubjectId,
1192
- sessionId: nativeSessionId,
1193
- }),
1194
- );
1195
-
1196
- // Writes can suspend after prepare. Recheck the capped lifetime and
1197
- // original proof freshness while the authority rows remain locked.
1198
- const commitNow = yield* freshNow;
1199
-
1200
- if (
1201
- DateTime.toEpochMillis(commitNow) < DateTime.toEpochMillis(record.issuedAt) ||
1202
- DateTime.toEpochMillis(commitNow) >= DateTime.toEpochMillis(record.expiresAt) ||
1203
- !(yield* assessAuthentication(input.evidence, requirement)).satisfied
1204
- )
1205
- return yield* stale();
1206
-
1207
- return receipt;
1208
- }).pipe(normalizeMutation(mapping)),
1209
- ),
1210
- verify: (input: Parameters<StatefulSessionPersistence<Claims>["verify"]>[0]) =>
1211
- safeTransaction(
1212
- withVerificationSavepoint(
1213
- Effect.gen(function* () {
1214
- const sql = root.$client.withoutTransforms();
1215
- const { session, subject, canJoinOwner } = verification;
1216
-
1217
- // A digest carries no owner. Custom codecs require decoding that
1218
- // native ID first; never compare differently encoded ID columns.
1219
- const owner = canJoinOwner
1220
- ? undefined
1221
- : (yield* database
1222
- .select({ subjectId: c.subjectId })
1223
- .from(mapping.session.table)
1224
- .where(eq(c.digest, input.digest))
1225
- .limit(1))[0];
1226
-
1227
- if (!canJoinOwner && owner === undefined) return yield* invalidSession();
1228
-
1229
- // The final statement reads both rows even after owner discovery,
1230
- // so deletion, rotation or owner changes cannot reuse the first read.
1231
- const rows = yield* sqlMapping(
1232
- () => sql<
1233
- Record<string, unknown>
1234
- >`select ${session.fields("v_")}, ${subject.fields("s_")}
1235
- from ${session.name} inner join ${subject.name} on
1236
- ${subject.column(mapping.subject.id)} = ${
1237
- owner === undefined
1238
- ? session.column(mapping.session.subjectId)
1239
- : subject.value(mapping.subject.id, owner.subjectId)
1240
- }
1241
- where ${session.column(mapping.session.digest)} = ${session.value(mapping.session.digest, input.digest)}
1242
- limit 1`,
1243
- ).pipe(Effect.flatten);
1244
-
1245
- const row = rows[0];
1246
-
1247
- if (row === undefined) return yield* invalidSession();
1248
- const sessionRow = yield* sqlMapping(() => session.decode(row, "v_"));
1249
- const subjectRow = yield* sqlMapping(() => subject.decode(row, "s_"));
1250
- const record = yield* mapping.session.decode(sessionRow);
1251
- const nativeSubjectId = yield* mapping.subjectId.toNative(record.subjectId);
1252
- const now = yield* freshNow;
1253
-
1254
- if (
1255
- !mapping.subjectId.equals(nativeSubjectId, sessionRow[mapping.session.subjectId]) ||
1256
- !mapping.subjectId.equals(nativeSubjectId, subjectRow[mapping.subject.id]) ||
1257
- !mapping.subject.isActiveStatus(subjectRow[mapping.subject.status]) ||
1258
- subjectRow[mapping.subject.securityRevision] !== record.securityRevision ||
1259
- DateTime.toEpochMillis(now) >= DateTime.toEpochMillis(record.expiresAt) ||
1260
- DateTime.toEpochMillis(now) >= DateTime.toEpochMillis(record.absoluteExpiresAt)
1261
- )
1262
- return yield* invalidSession();
1263
-
1264
- return record;
1265
- }),
1266
- ),
1267
- ),
1268
- rotate: <A>(
1269
- input: Parameters<StatefulSessionPersistence<Claims>["rotate"]>[0],
1270
- prepare: PrepareSessionCommit<StatefulSessionRecord<Claims>, A>,
1271
- ) =>
1272
- owned(
1273
- database,
1274
- options,
1275
- mapping.isConstraintConflict,
1276
- Effect.gen(function* () {
1277
- const transaction = yield* CurrentSessionSql;
1278
- const journal = yield* CurrentCommitJournal;
1279
- const nativeSessionId = yield* mapping.sessionId.toNative(input.sessionId);
1280
-
1281
- const initial = (yield* transaction
1282
- .select()
1283
- .from(mapping.session.table)
1284
- .where(eq(c.sessionId, nativeSessionId))
1285
- .limit(1))[0];
1286
-
1287
- if (initial === undefined) return yield* SessionConflict.make({});
1288
- const initialRecord = yield* mapping.session.decode(initial);
1289
- const nativeSubjectId = yield* mapping.subjectId.toNative(initialRecord.subjectId);
1290
- const subject = (yield* readSubject(mapping, nativeSubjectId, true))[0];
1291
-
1292
- const row = (yield* selectRows(
1293
- transaction
1294
- .select()
1295
- .from(mapping.session.table)
1296
- .where(eq(c.sessionId, nativeSessionId))
1297
- .limit(1),
1298
- options.locking,
1299
- ))[0];
1300
-
1301
- if (row === undefined) return yield* SessionConflict.make({});
1302
- const record = yield* mapping.session.decode(row);
1303
-
1304
- const version = yield* allocate(
1305
- options.mode,
1306
- mapping.session.allocateVersion,
1307
- mapping.session.allocateVersionSync,
1308
- );
1309
-
1310
- const now = yield* freshNow;
1311
-
1312
- if (
1313
- subject === undefined ||
1314
- !mapping.subject.isActiveStatus(subject[mapping.subject.status]) ||
1315
- subject[mapping.subject.securityRevision] !== input.expectedSecurityRevision ||
1316
- record.securityRevision !== input.expectedSecurityRevision ||
1317
- record.digest !== input.expectedDigest ||
1318
- record.version !== input.expectedVersion ||
1319
- DateTime.toEpochMillis(now) >= DateTime.toEpochMillis(record.expiresAt) ||
1320
- DateTime.toEpochMillis(now) >= DateTime.toEpochMillis(record.absoluteExpiresAt) ||
1321
- DateTime.toEpochMillis(input.nextExpiresAt) >
1322
- DateTime.toEpochMillis(record.absoluteExpiresAt) ||
1323
- DateTime.toEpochMillis(input.nextExpiresAt) <= DateTime.toEpochMillis(now)
1324
- )
1325
- return yield* SessionConflict.make({});
1326
-
1327
- const next = {
1328
- ...record,
1329
- digest: input.nextDigest,
1330
- credentialVersion: input.nextCredentialVersion,
1331
- version,
1332
- issuedAt: now,
1333
- expiresAt: input.nextExpiresAt,
1334
- };
1335
-
1336
- const receipt = prepare(next, journal);
1337
-
1338
- yield* transaction
1339
- .update(mapping.session.table)
1340
- .set(mapping.session.encodeRotation(next))
1341
- .where(
1342
- and(
1343
- eq(c.sessionId, nativeSessionId),
1344
- eq(c.digest, input.expectedDigest),
1345
- eq(c.version, input.expectedVersion),
1346
- ),
1347
- );
1348
-
1349
- return receipt;
1350
- }),
1351
- ),
1352
- revokeDigest: <A>(digest: TokenDigest, prepare: PrepareSessionCommit<boolean, A>) =>
1353
- owned(
1354
- database,
1355
- options,
1356
- mapping.isConstraintConflict,
1357
- Effect.gen(function* () {
1358
- const transaction = yield* CurrentSessionSql;
1359
- const journal = yield* CurrentCommitJournal;
1360
-
1361
- const initial = (yield* transaction
1362
- .select()
1363
- .from(mapping.session.table)
1364
- .where(eq(c.digest, digest))
1365
- .limit(1))[0];
1366
-
1367
- if (initial === undefined) return prepare(false, journal);
1368
- const record = yield* mapping.session.decode(initial);
1369
- const nativeSubjectId = yield* mapping.subjectId.toNative(record.subjectId);
1370
-
1371
- yield* readSubject(mapping, nativeSubjectId, true);
1372
-
1373
- const row = (yield* selectRows(
1374
- transaction.select().from(mapping.session.table).where(eq(c.digest, digest)).limit(1),
1375
- options.locking,
1376
- ))[0];
1377
-
1378
- const receipt = prepare(row !== undefined, journal);
1379
-
1380
- if (row !== undefined)
1381
- yield* transaction.delete(mapping.session.table).where(eq(c.digest, digest));
1382
-
1383
- return receipt;
1384
- }),
1385
- ),
1386
- revoke: <A>(
1387
- input: Parameters<StatefulSessionPersistence<Claims>["revoke"]>[0],
1388
- prepare: PrepareSessionCommit<void, A>,
1389
- ) =>
1390
- owned(
1391
- database,
1392
- options,
1393
- mapping.isConstraintConflict,
1394
- Effect.gen(function* () {
1395
- const transaction = yield* CurrentSessionSql;
1396
- const journal = yield* CurrentCommitJournal;
1397
- const nativeSubjectId = yield* mapping.subjectId.toNative(input.subjectId);
1398
- const subject = (yield* readSubject(mapping, nativeSubjectId, true))[0];
1399
-
1400
- if (
1401
- subject === undefined ||
1402
- !mapping.subject.isActiveStatus(subject[mapping.subject.status]) ||
1403
- subject[mapping.subject.securityRevision] !== input.expectedSecurityRevision
1404
- )
1405
- return yield* stale();
1406
- const nativeSessionId = yield* mapping.sessionId.toNative(input.sessionId);
1407
- const receipt = prepare(undefined, journal);
1408
-
1409
- yield* transaction
1410
- .delete(mapping.session.table)
1411
- .where(and(eq(c.subjectId, nativeSubjectId), eq(c.sessionId, nativeSessionId)));
1412
-
1413
- return receipt;
1414
- }),
1415
- ),
1416
- revokeAll: <A>(
1417
- input: Parameters<StatefulSessionPersistence<Claims>["revokeAll"]>[0],
1418
- prepare: PrepareSessionCommit<void, A>,
1419
- ) =>
1420
- owned(
1421
- database,
1422
- options,
1423
- mapping.isConstraintConflict,
1424
- Effect.gen(function* () {
1425
- const transaction = yield* CurrentSessionSql;
1426
- const journal = yield* CurrentCommitJournal;
1427
- const nativeSubjectId = yield* mapping.subjectId.toNative(input.subjectId);
1428
- const subject = (yield* readSubject(mapping, nativeSubjectId, true))[0];
1429
-
1430
- if (
1431
- subject === undefined ||
1432
- !mapping.subject.isActiveStatus(subject[mapping.subject.status]) ||
1433
- subject[mapping.subject.securityRevision] !== input.expectedSecurityRevision
1434
- )
1435
- return yield* stale();
1436
- const nextRevisionSync = mapping.subject.nextSecurityRevisionSync;
1437
-
1438
- const next = yield* allocate(
1439
- options.mode,
1440
- mapping.subject.nextSecurityRevision?.(input.expectedSecurityRevision),
1441
- nextRevisionSync === undefined
1442
- ? undefined
1443
- : () => nextRevisionSync(input.expectedSecurityRevision),
1444
- );
1445
-
1446
- const receipt = prepare(undefined, journal);
1447
-
1448
- yield* transaction
1449
- .update(mapping.subject.table)
1450
- .set(updateValues([[mapping.subject.securityRevision, next]]))
1451
- .where(
1452
- and(
1453
- eq(authorityColumns(mapping).subjectId, nativeSubjectId),
1454
- eq(authorityColumns(mapping).subjectRevision, input.expectedSecurityRevision),
1455
- ),
1456
- );
1457
- yield* transaction
1458
- .delete(mapping.session.table)
1459
- .where(eq(c.subjectId, nativeSubjectId));
1460
-
1461
- return receipt;
1462
- }),
1463
- ),
1464
- };
1465
-
1466
- const repository = {
1467
- list: (input: any) =>
1468
- safeTransaction(
1469
- inTransaction(
1470
- database,
1471
- Effect.gen(function* () {
1472
- const read = yield* CurrentSessionSql;
1473
- const nativeSubjectId = yield* mapping.subjectId.toNative(input.subjectId);
1474
- const subject = (yield* readSubject(mapping, nativeSubjectId, false))[0];
1475
-
1476
- if (
1477
- subject === undefined ||
1478
- !mapping.subject.isActiveStatus(subject[mapping.subject.status])
1479
- )
1480
- return { sessions: [] };
1481
-
1482
- const cursor =
1483
- input.cursor === undefined
1484
- ? undefined
1485
- : yield* mapping.sessionId.toNative(input.cursor as any);
1486
-
1487
- const now = yield* freshNow;
1488
-
1489
- const rows = yield* read
1490
- .select()
1491
- .from(mapping.session.table)
1492
- .where(
1493
- and(
1494
- eq(c.subjectId, nativeSubjectId),
1495
- eq(c.securityRevision, subject[mapping.subject.securityRevision]),
1496
- gt(c.expiresAt, mapping.session.encodeInstant(now)),
1497
- gt(c.absoluteExpiresAt, mapping.session.encodeInstant(now)),
1498
- cursor === undefined ? undefined : gt(c.sessionId, cursor),
1499
- ),
1500
- )
1501
- .orderBy(c.sessionId)
1502
- .limit(input.limit + 1);
1503
-
1504
- const decoded = yield* Effect.forEach(rows, (row: any) =>
1505
- mapping.session.decode(row),
1506
- );
1507
-
1508
- const codec = Schema.toCodecJson(Schema.toType(SessionMetadata));
1509
-
1510
- const sessions = yield* Effect.forEach(decoded.slice(0, input.limit), (record: any) =>
1511
- Schema.encodeEffect(codec)(record).pipe(Effect.flatMap(Schema.decodeEffect(codec))),
1512
- );
1513
-
1514
- const extra = decoded[input.limit];
1515
- const nextCursor = extra === undefined ? undefined : sessions.at(-1)?.sessionId;
1516
-
1517
- return {
1518
- sessions,
1519
- ...(nextCursor === undefined ? {} : { nextCursor }),
1520
- };
1521
- }),
1522
- ),
1523
- ),
1524
- };
1525
-
1526
- return {
1527
- statefulSessionPersistence: {
1528
- establish: (input, prepare) =>
1529
- persistence
1530
- .establish(input, prepare)
1531
- .pipe(
1532
- Effect.provideService(NativeDatabase, root),
1533
- Effect.provideService(LifecycleHooks, hooks),
1534
- ),
1535
- verify: (input) =>
1536
- persistence
1537
- .verify(input)
1538
- .pipe(
1539
- Effect.provideService(NativeDatabase, root),
1540
- Effect.provideService(LifecycleHooks, hooks),
1541
- ),
1542
- rotate: (input, prepare) =>
1543
- persistence.rotate(input, prepare).pipe(Effect.provideService(LifecycleHooks, hooks)),
1544
- revokeDigest: (digest, prepare) =>
1545
- persistence
1546
- .revokeDigest(digest, prepare)
1547
- .pipe(Effect.provideService(LifecycleHooks, hooks)),
1548
- revoke: (input, prepare) =>
1549
- persistence.revoke(input, prepare).pipe(Effect.provideService(LifecycleHooks, hooks)),
1550
- revokeAll: (input, prepare) =>
1551
- persistence.revokeAll(input, prepare).pipe(Effect.provideService(LifecycleHooks, hooks)),
1552
- } satisfies StatefulSessionPersistence<Claims>,
1553
- sessionRepository: {
1554
- list: (input) => repository.list(input).pipe(Effect.provideService(LifecycleHooks, hooks)),
1555
- } satisfies SessionRepository,
1556
- };
1557
- });
1558
-
1559
- const makeSqlSignedValidity = Effect.fnUntraced(function* (
1560
- mapping: SignedSessionValidityMapping<any, any, any, any>,
1561
- options: SessionSqlOptions,
1562
- ): Effect.fn.Return<
1563
- SignedSessionValidity,
1564
- SessionUnavailable,
1565
- LifecycleHooks | CurrentSessionSql | NativeDatabase
1566
- > {
1567
- const database = yield* CurrentSessionSql;
1568
-
1569
- if (!options.coordinated)
1570
- yield* (operations.validateStorage?.(mapping) ?? Effect.void).pipe(
1571
- Effect.mapError(unavailable),
1572
- );
1573
- const hooks = yield* LifecycleHooks;
1574
-
1575
- const subject = subjectColumns(mapping);
1576
-
1577
- const tombstone = {
1578
- subjectId: column(mapping.tombstone.table, mapping.tombstone.subjectId),
1579
- sessionId: column(mapping.tombstone.table, mapping.tombstone.sessionId),
1580
- absoluteExpiresAt: column(mapping.tombstone.table, mapping.tombstone.absoluteExpiresAt),
1581
- };
1582
-
1583
- const root = yield* NativeDatabase;
1584
-
1585
- const tables = yield* sqlMapping(() =>
1586
- nativeTables(root.$client.withoutTransforms(), root),
1587
- ).pipe(Effect.mapError(unavailable));
1588
-
1589
- const service = {
1590
- verify: (session: SessionMetadata, _now: DateTime.Utc) =>
1591
- safeTransaction(
1592
- withVerificationSavepoint(
1593
- Effect.gen(function* () {
1594
- const sql = root.$client.withoutTransforms();
1595
- const nativeSubjectId = yield* mapping.subjectId.toNative(session.subjectId);
1596
- const nativeSessionId = yield* mapping.sessionId.toNative(session.sessionId);
1597
- const readNow = yield* freshNow;
1598
-
1599
- // Independent bound IDs preserve each column's physical type and
1600
- // codec. Both authority and revocation come from this statement.
1601
- const { subjectTable, rows } = yield* sqlMapping(() => {
1602
- const subjectTable = tables(mapping.subject.table).as("auth_subject");
1603
- const revoked = tables(mapping.tombstone.table).as("auth_revocation");
1604
-
1605
- return {
1606
- subjectTable,
1607
- rows: sql<Record<string, unknown>>`
1608
- select ${subjectTable.fields("s_")},
1609
- case when exists (
1610
- select 1 from ${revoked.name}
1611
- where ${revoked.column(mapping.tombstone.subjectId)} = ${revoked.value(mapping.tombstone.subjectId, nativeSubjectId)}
1612
- and ${revoked.column(mapping.tombstone.sessionId)} = ${revoked.value(mapping.tombstone.sessionId, nativeSessionId)}
1613
- and ${revoked.column(mapping.tombstone.absoluteExpiresAt)} > ${revoked.value(mapping.tombstone.absoluteExpiresAt, mapping.tombstone.encodeInstant(readNow))}
1614
- ) then 1 else 0 end as auth_revoked
1615
- from ${subjectTable.name}
1616
- where ${subjectTable.column(mapping.subject.id)} = ${subjectTable.value(mapping.subject.id, nativeSubjectId)}
1617
- limit 1`,
1618
- };
1619
- });
1620
-
1621
- const result = (yield* rows)[0];
1622
-
1623
- if (result === undefined) return yield* invalidSession();
1624
- const row = yield* sqlMapping(() => subjectTable.decode(result, "s_"));
1625
- const revoked = yield* decodeRevocationFlag(result.auth_revoked);
1626
- const authoritativeNow = yield* freshNow;
1627
-
1628
- if (
1629
- !mapping.subjectId.equals(nativeSubjectId, row[mapping.subject.id]) ||
1630
- !mapping.subject.isActiveStatus(row[mapping.subject.status]) ||
1631
- row[mapping.subject.securityRevision] !== session.securityRevision ||
1632
- revoked === 1 ||
1633
- DateTime.toEpochMillis(authoritativeNow) >=
1634
- DateTime.toEpochMillis(session.expiresAt) ||
1635
- DateTime.toEpochMillis(authoritativeNow) >=
1636
- DateTime.toEpochMillis(session.absoluteExpiresAt)
1637
- )
1638
- return yield* invalidSession();
1639
- }),
1640
- ),
1641
- ),
1642
- revoke: <A>(
1643
- input: Parameters<SignedSessionValidity["revoke"]>[0],
1644
- prepare: PrepareSessionCommit<void, A>,
1645
- ) =>
1646
- owned(
1647
- database,
1648
- options,
1649
- mapping.isConstraintConflict,
1650
- Effect.gen(function* () {
1651
- const transaction = yield* CurrentSessionSql;
1652
- const journal = yield* CurrentCommitJournal;
1653
- const nativeSubjectId = yield* mapping.subjectId.toNative(input.subjectId);
1654
- const row = (yield* readSubject(mapping, nativeSubjectId, true))[0];
1655
-
1656
- if (
1657
- row === undefined ||
1658
- !mapping.subject.isActiveStatus(row[mapping.subject.status]) ||
1659
- row[mapping.subject.securityRevision] !== input.expectedSecurityRevision
1660
- )
1661
- return yield* stale();
1662
- const nativeSessionId = yield* mapping.sessionId.toNative(input.sessionId);
1663
-
1664
- const existing = yield* transaction
1665
- .select()
1666
- .from(mapping.tombstone.table)
1667
- .where(
1668
- and(
1669
- eq(tombstone.subjectId, nativeSubjectId),
1670
- eq(tombstone.sessionId, nativeSessionId),
1671
- ),
1672
- )
1673
- .limit(1);
1674
-
1675
- const receipt = prepare(undefined, journal);
1676
-
1677
- if (existing[0] === undefined)
1678
- yield* transaction.insert(mapping.tombstone.table).values(
1679
- mapping.tombstone.encodeInsert({
1680
- subjectId: nativeSubjectId,
1681
- sessionId: nativeSessionId,
1682
- absoluteExpiresAt: input.absoluteExpiresAt,
1683
- }),
1684
- );
1685
- else {
1686
- const existingExpiry = yield* mapping.tombstone.decodeInstant(
1687
- existing[0][mapping.tombstone.absoluteExpiresAt],
1688
- );
1689
-
1690
- if (
1691
- DateTime.toEpochMillis(input.absoluteExpiresAt) >
1692
- DateTime.toEpochMillis(existingExpiry)
1693
- )
1694
- yield* transaction
1695
- .update(mapping.tombstone.table)
1696
- .set(
1697
- updateValues([
1698
- [
1699
- mapping.tombstone.absoluteExpiresAt,
1700
- mapping.tombstone.encodeInstant(input.absoluteExpiresAt),
1701
- ],
1702
- ]),
1703
- )
1704
- .where(
1705
- and(
1706
- eq(tombstone.subjectId, nativeSubjectId),
1707
- eq(tombstone.sessionId, nativeSessionId),
1708
- ),
1709
- );
1710
- }
1711
-
1712
- return receipt;
1713
- }),
1714
- ),
1715
- revokeAll: <A>(
1716
- input: Parameters<SignedSessionValidity["revokeAll"]>[0],
1717
- prepare: PrepareSessionCommit<void, A>,
1718
- ) =>
1719
- owned(
1720
- database,
1721
- options,
1722
- mapping.isConstraintConflict,
1723
- Effect.gen(function* () {
1724
- const transaction = yield* CurrentSessionSql;
1725
- const journal = yield* CurrentCommitJournal;
1726
- const nativeSubjectId = yield* mapping.subjectId.toNative(input.subjectId);
1727
- const row = (yield* readSubject(mapping, nativeSubjectId, true))[0];
1728
-
1729
- if (
1730
- row === undefined ||
1731
- !mapping.subject.isActiveStatus(row[mapping.subject.status]) ||
1732
- row[mapping.subject.securityRevision] !== input.expectedSecurityRevision
1733
- )
1734
- return yield* stale();
1735
- const nextRevisionSync = mapping.subject.nextSecurityRevisionSync;
1736
-
1737
- const next = yield* allocate(
1738
- options.mode,
1739
- mapping.subject.nextSecurityRevision?.(input.expectedSecurityRevision),
1740
- nextRevisionSync === undefined
1741
- ? undefined
1742
- : () => nextRevisionSync(input.expectedSecurityRevision),
1743
- );
1744
-
1745
- const receipt = prepare(undefined, journal);
1746
-
1747
- yield* transaction
1748
- .update(mapping.subject.table)
1749
- .set(updateValues([[mapping.subject.securityRevision, next]]))
1750
- .where(
1751
- and(
1752
- eq(subject.subjectId, nativeSubjectId),
1753
- eq(subject.subjectRevision, input.expectedSecurityRevision),
1754
- ),
1755
- );
1756
-
1757
- return receipt;
1758
- }),
1759
- ),
1760
- };
1761
-
1762
- return {
1763
- verify: (session, now) =>
1764
- service
1765
- .verify(session, now)
1766
- .pipe(
1767
- Effect.provideService(NativeDatabase, root),
1768
- Effect.provideService(LifecycleHooks, hooks),
1769
- ),
1770
- revoke: (input, prepare) =>
1771
- service.revoke(input, prepare).pipe(Effect.provideService(LifecycleHooks, hooks)),
1772
- revokeAll: (input, prepare) =>
1773
- service.revokeAll(input, prepare).pipe(Effect.provideService(LifecycleHooks, hooks)),
1774
- } satisfies SignedSessionValidity;
1775
- });
1776
-
1777
- const stepUpColumns = (mapping: any) =>
1778
- Object.fromEntries(
1779
- [
1780
- "digest",
1781
- "version",
1782
- "flowId",
1783
- "subjectId",
1784
- "bindingDigest",
1785
- "snapshot",
1786
- "expiresAt",
1787
- "attemptLimit",
1788
- "failedAttempts",
1789
- "consumed",
1790
- ].map((key) => [key, column(mapping.intent.table, mapping.intent[key])]),
1791
- );
1792
-
1793
- const stepUpLocator = Effect.fn("DrizzleStepUp.locator")(function* (
1794
- mapping: AnyStepUpMapping,
1795
- digest: TokenDigest,
1796
- locking: boolean,
1797
- expiresAt?: DateTime.Utc,
1798
- ) {
1799
- const database = yield* CurrentSessionSql;
1800
-
1801
- return yield* selectRows(
1802
- database
1803
- .select()
1804
- .from(mapping.intent.table)
1805
- .where(
1806
- and(
1807
- eq(stepUpColumns(mapping).digest, digest),
1808
- expiresAt === undefined
1809
- ? undefined
1810
- : eq(stepUpColumns(mapping).expiresAt, mapping.intent.encodeInstant(expiresAt)),
1811
- ),
1812
- )
1813
- .limit(1),
1814
- locking,
1815
- ).pipe(Effect.map((rows) => rows[0]));
1816
- });
1817
-
1818
- const stepUpAuthority = Effect.fn("DrizzleStepUp.authority")(function* (
1819
- mapping: AnyStepUpMapping,
1820
- revision: AuthenticationRevision,
1821
- locking: boolean,
1822
- ) {
1823
- const current = yield* captureDetailsIn(
1824
- mapping,
1825
- revision.subjectId,
1826
- revision.credentials.map((c) => c.credentialId),
1827
- locking,
1828
- );
1829
-
1830
- if (!sameStepUpRevision(current.revision, revision)) return yield* stale();
1831
- const requirement = yield* mapping.subject.decodeRequirement(current.subject);
1832
-
1833
- return {
1834
- nativeSubjectId: yield* mapping.subjectId.toNative(revision.subjectId),
1835
- requirement,
1836
- };
1837
- });
1838
-
1839
- const stepUpSource = Effect.fn("DrizzleStepUp.source")(function* (
1840
- mapping: AnyStepUpMapping,
1841
- intent: SessionStepUpIntent,
1842
- nativeSubjectId: unknown,
1843
- now: DateTime.Utc,
1844
- locking: boolean,
1845
- ) {
1846
- const database = yield* CurrentSessionSql;
1847
-
1848
- if (!stepUpIntentLive(intent, mapping.source.kind, now))
1849
- return yield* SessionStepUpInvalid.make({});
1850
- const source = mapping.source;
1851
-
1852
- if (source.kind === "StatelessSigned") return { nativeSessionId: undefined, row: undefined };
1853
- const nativeSessionId = yield* source.sessionId.toNative(intent.sourceSessionId);
1854
-
1855
- if (source.kind === "StateAssistedSigned") {
1856
- const t = source.tombstone;
1857
-
1858
- const found = yield* selectRows(
1859
- database
1860
- .select()
1861
- .from(t.table)
1862
- .where(
1863
- and(
1864
- eq(column(t.table, t.subjectId), nativeSubjectId),
1865
- eq(column(t.table, t.sessionId), nativeSessionId),
1866
- gt(column(t.table, t.absoluteExpiresAt), t.encodeInstant(now)),
1867
- ),
1868
- )
1869
- .limit(1),
1870
- locking,
1871
- );
1872
-
1873
- if (found.length > 0) return yield* SessionStepUpInvalid.make({});
1874
-
1875
- return { nativeSessionId, row: undefined };
1876
- }
1877
-
1878
- const s = source.session,
1879
- c = sessionColumns(source);
1880
-
1881
- const row = (yield* selectRows(
1882
- database
1883
- .select()
1884
- .from(s.table)
1885
- .where(
1886
- and(
1887
- eq(c.sessionId, nativeSessionId),
1888
- eq(c.subjectId, nativeSubjectId),
1889
- eq(c.securityRevision, intent.revision.securityRevision),
1890
- eq(column(s.table, s.credentialVersion), intent.sourceCredentialVersion),
1891
- eq(column(s.table, s.authenticatedAt), s.encodeInstant(intent.sourceAuthenticatedAt)),
1892
- eq(c.expiresAt, s.encodeInstant(intent.sourceExpiresAt)),
1893
- eq(c.absoluteExpiresAt, s.encodeInstant(intent.sourceAbsoluteExpiresAt)),
1894
- gt(c.expiresAt, s.encodeInstant(now)),
1895
- gt(c.absoluteExpiresAt, s.encodeInstant(now)),
1896
- ),
1897
- )
1898
- .limit(1),
1899
- locking,
1900
- ))[0];
1901
-
1902
- if (row === undefined) return yield* SessionStepUpInvalid.make({});
1903
-
1904
- return { nativeSessionId, row };
1905
- });
1906
-
1907
- const readSqlStepUp = Effect.fn("DrizzleStepUp.read")(function* (
1908
- mapping: AnyStepUpMapping,
1909
- digest: TokenDigest,
1910
- locking: boolean,
1911
- ) {
1912
- const first = yield* stepUpLocator(mapping, digest, false);
1913
-
1914
- if (first === undefined) return yield* SessionStepUpInvalid.make({});
1915
- const intent = yield* decodeStepUpIntent(first[mapping.intent.snapshot]);
1916
- const authority = yield* stepUpAuthority(mapping, intent.revision, locking);
1917
-
1918
- const source = yield* stepUpSource(
1919
- mapping,
1920
- intent,
1921
- authority.nativeSubjectId,
1922
- yield* freshNow,
1923
- locking,
1924
- );
1925
-
1926
- const row = yield* stepUpLocator(mapping, digest, locking, intent.expiresAt);
1927
- const m = mapping.intent;
1928
-
1929
- if (
1930
- row === undefined ||
1931
- row[m.snapshot] !== first[m.snapshot] ||
1932
- row[m.version] !== intent.version ||
1933
- row[m.digest] !== intent.digest ||
1934
- digest !== intent.digest ||
1935
- row[m.flowId] !== intent.flowId ||
1936
- row[m.bindingDigest] !== intent.bindingDigest ||
1937
- !mapping.subjectId.equals(row[m.subjectId], authority.nativeSubjectId) ||
1938
- Boolean(row[m.consumed]) ||
1939
- !Schema.is(Schema.Natural)(row[m.failedAttempts]) ||
1940
- row[m.failedAttempts] >= row[m.attemptLimit] ||
1941
- row[m.attemptLimit] !== intent.attemptLimit ||
1942
- !stepUpIntentLive(intent, mapping.source.kind, yield* freshNow)
1943
- )
1944
- return yield* SessionStepUpInvalid.make({});
1945
-
1946
- return { row, intent, source, ...authority };
1947
- });
1948
-
1949
- const isSqlStepUpRejectAbsence = (error: unknown): error is SqlStepUpRejectAbsence => {
1950
- const tag =
1951
- typeof error === "object" && error !== null ? Reflect.get(error, "_tag") : undefined;
1952
-
1953
- return tag === "SessionStepUpInvalid" || tag === "StaleAuthentication";
1954
- };
1955
-
1956
- const recoverSqlStepUpRejectAbsence = <A, B, R>(
1957
- effect: Effect.Effect<A, SqlStepUpReadFailure, R>,
1958
- rejected: B,
1959
- ): Effect.Effect<A | B, AdapterFailure | SessionUnavailable, R> =>
1960
- effect.pipe(
1961
- Effect.catchCause((cause) => {
1962
- if (
1963
- cause.reasons.length > 0 &&
1964
- cause.reasons.every(
1965
- (reason) => Cause.isFailReason(reason) && isSqlStepUpRejectAbsence(reason.error),
1966
- )
1967
- )
1968
- return Effect.succeed(rejected);
1969
-
1970
- return Effect.failCause(
1971
- Cause.map(cause, (error) => (isSqlStepUpRejectAbsence(error) ? unavailable() : error)),
1972
- );
1973
- }),
1974
- );
1975
-
1976
- /** Shared implementation; public driver entrypoints restore concrete tables/IDs. */
1977
- const makeSqlSessionStepUp = Effect.fnUntraced(function* <Claims>(
1978
- mapping: SessionStepUpMapping<Claims, any, any, any, any, any, any, any>,
1979
- options: SessionSqlOptions,
1980
- ): Effect.fn.Return<
1981
- SessionStepUpPersistence<Claims>,
1982
- SessionUnavailable,
1983
- LifecycleHooks | CurrentSessionSql | NativeDatabase
1984
- > {
1985
- const database = yield* CurrentSessionSql;
1986
-
1987
- if (!options.coordinated)
1988
- yield* (operations.validateStorage?.(mapping) ?? Effect.void).pipe(
1989
- Effect.mapError(unavailable),
1990
- );
1991
- const hooks = yield* LifecycleHooks;
1992
-
1993
- const m = mapping.intent,
1994
- c = stepUpColumns(mapping);
1995
-
1996
- const read = (digest: TokenDigest) =>
1997
- mapFailureCause(
1998
- safeTransaction(inTransaction(database, readSqlStepUp(mapping, digest, options.locking))),
1999
- (error) => (error._tag === "StaleAuthentication" ? SessionStepUpInvalid.make({}) : error),
2000
- );
2001
-
2002
- const service = {
2003
- create: <A>(
2004
- input: Omit<SessionStepUpIntent, "version">,
2005
- _now: DateTime.Utc,
2006
- prepare: PrepareSessionCommit<SessionStepUpIntent, A>,
2007
- ) =>
2008
- owned(
2009
- database,
2010
- options,
2011
- mapping.isConstraintConflict,
2012
- Effect.gen(function* () {
2013
- const tx = yield* CurrentSessionSql;
2014
- const journal = yield* CurrentCommitJournal;
2015
- const authority = yield* stepUpAuthority(mapping, input.revision, options.locking);
2016
-
2017
- const version = yield* allocate(options.mode, m.allocateVersion, m.allocateVersionSync);
2018
-
2019
- const record: SessionStepUpIntent = { ...input, version };
2020
-
2021
- yield* stepUpSource(
2022
- mapping,
2023
- record,
2024
- authority.nativeSubjectId,
2025
- yield* freshNow,
2026
- options.locking,
2027
- );
2028
-
2029
- const existing = yield* selectRows(
2030
- tx
2031
- .select()
2032
- .from(m.table)
2033
- .where(or(eq(c.digest, input.digest), eq(c.flowId, input.flowId)))
2034
- .limit(1),
2035
- options.locking,
2036
- );
2037
-
2038
- if (existing.length > 0) return yield* SessionConflict.make({});
2039
- const snapshot = yield* encodeStepUpIntent(record);
2040
-
2041
- const values = {
2042
- ...m.encodeInsert(record, { subjectId: authority.nativeSubjectId }),
2043
- ...updateValues([
2044
- [m.digest, record.digest],
2045
- [m.version, version],
2046
- [m.flowId, record.flowId],
2047
- [m.subjectId, authority.nativeSubjectId],
2048
- [m.bindingDigest, record.bindingDigest],
2049
- [m.snapshot, snapshot],
2050
- [m.expiresAt, m.encodeInstant(record.expiresAt)],
2051
- [m.attemptLimit, record.attemptLimit],
2052
- [m.failedAttempts, 0],
2053
- [m.consumed, false],
2054
- ]),
2055
- };
2056
-
2057
- const receipt = prepare(record, journal);
2058
-
2059
- yield* tx.insert(m.table).values(values).pipe(normalizeMutation(mapping));
2060
-
2061
- const inserted = (yield* tx
2062
- .select()
2063
- .from(m.table)
2064
- .where(
2065
- and(
2066
- eq(c.digest, record.digest),
2067
- eq(c.version, version),
2068
- eq(c.flowId, record.flowId),
2069
- eq(c.subjectId, authority.nativeSubjectId),
2070
- eq(c.bindingDigest, record.bindingDigest),
2071
- eq(c.snapshot, snapshot),
2072
- eq(c.expiresAt, m.encodeInstant(record.expiresAt)),
2073
- eq(c.attemptLimit, record.attemptLimit),
2074
- eq(c.failedAttempts, 0),
2075
- eq(c.consumed, false),
2076
- ),
2077
- )
2078
- .limit(1))[0];
2079
-
2080
- if (inserted === undefined) return yield* unavailable();
2081
-
2082
- return receipt;
2083
- }),
2084
- ).pipe((effect) =>
2085
- mapFailureCause(effect, (error) =>
2086
- error._tag === "SessionStepUpInvalid" ? stale() : error,
2087
- ),
2088
- ),
2089
- context: (input: any) =>
2090
- mapFailureCause(
2091
- read(input.digest).pipe(
2092
- Effect.flatMap(({ intent }) =>
2093
- snapshotPendingAuthenticationContext({
2094
- flowId: intent.flowId,
2095
- bindingDigest: intent.bindingDigest,
2096
- revision: intent.revision,
2097
- expiresAtMillis: DateTime.toEpochMillis(intent.expiresAt),
2098
- }),
2099
- ),
2100
- ),
2101
- (error) => (error._tag === "PendingAuthenticationInvalid" ? unavailable() : error),
2102
- ),
2103
- read: (input: any) =>
2104
- read(input.digest).pipe(
2105
- Effect.flatMap((value) =>
2106
- value.intent.bindingDigest === input.bindingDigest
2107
- ? Effect.succeed(value.intent)
2108
- : Effect.fail(SessionStepUpInvalid.make({})),
2109
- ),
2110
- ),
2111
- reject: <A>(
2112
- input: Parameters<SessionStepUpPersistence<Claims>["reject"]>[0],
2113
- prepare: PrepareSessionCommit<{ readonly _tag: "Rejected" }, A>,
2114
- ) =>
2115
- owned(
2116
- database,
2117
- options,
2118
- mapping.isConstraintConflict,
2119
- Effect.gen(function* () {
2120
- const tx = yield* CurrentSessionSql;
2121
- const journal = yield* CurrentCommitJournal;
2122
-
2123
- const selected = yield* recoverSqlStepUpRejectAbsence(
2124
- readSqlStepUp(mapping, input.digest, options.locking).pipe(
2125
- Effect.map((value) => ({ _tag: "Found" as const, value })),
2126
- ),
2127
- { _tag: "Rejected" as const },
2128
- );
2129
-
2130
- if (selected._tag === "Rejected") return prepare({ _tag: "Rejected" }, journal);
2131
- const { row, intent } = selected.value;
2132
- const receipt = prepare({ _tag: "Rejected" }, journal);
2133
-
2134
- yield* tx
2135
- .update(m.table)
2136
- .set(updateValues([[m.failedAttempts, sql`${c.failedAttempts} + 1`]]))
2137
- .where(
2138
- and(
2139
- eq(c.digest, input.digest),
2140
- eq(c.snapshot, row[m.snapshot]),
2141
- eq(c.version, intent.version),
2142
- eq(c.consumed, false),
2143
- sql`${c.failedAttempts} < ${c.attemptLimit}`,
2144
- ),
2145
- );
2146
- const written = yield* stepUpLocator(mapping, input.digest, false);
2147
-
2148
- if (
2149
- written === undefined ||
2150
- written[m.version] !== intent.version ||
2151
- written[m.snapshot] !== row[m.snapshot] ||
2152
- written[m.failedAttempts] !== row[m.failedAttempts] + 1
2153
- )
2154
- return yield* unavailable();
2155
-
2156
- return receipt;
2157
- }),
2158
- ),
2159
- complete: <A>(
2160
- plan: Parameters<SessionStepUpPersistence<Claims>["complete"]>[0],
2161
- prepare: PrepareSessionCommit<SessionInspection<Claims>, A>,
2162
- ) =>
2163
- owned(
2164
- database,
2165
- options,
2166
- mapping.isConstraintConflict,
2167
- Effect.gen(function* () {
2168
- const tx = yield* CurrentSessionSql;
2169
- const journal = yield* CurrentCommitJournal;
2170
-
2171
- yield* validateStepUpPlan(plan);
2172
-
2173
- // Lock the entire combined vector before source/intent, including added factors.
2174
- const current = yield* stepUpAuthority(
2175
- mapping,
2176
- plan.evidence.revision,
2177
- options.locking,
2178
- );
2179
-
2180
- const stored = yield* readSqlStepUp(mapping, plan.intent.digest, options.locking);
2181
-
2182
- if (
2183
- (yield* encodeStepUpIntent(stored.intent)) !==
2184
- (yield* encodeStepUpIntent(plan.intent))
2185
- )
2186
- return yield* SessionStepUpInvalid.make({});
2187
-
2188
- const base = yield* assessAuthentication(plan.evidence, current.requirement).pipe(
2189
- Effect.mapError(() => stale()),
2190
- );
2191
-
2192
- const profile = yield* assessAuthentication(
2193
- plan.evidence,
2194
- stored.intent.requirement,
2195
- ).pipe(Effect.mapError(() => stale()));
2196
-
2197
- if (!base.satisfied || !profile.satisfied) return yield* stale();
2198
-
2199
- const replacement = plan.replacement,
2200
- now = yield* freshNow;
2201
-
2202
- if (
2203
- DateTime.toEpochMillis(replacement.inspection.session.expiresAt) >
2204
- DateTime.toEpochMillis(stored.intent.sourceAbsoluteExpiresAt) ||
2205
- DateTime.toEpochMillis(now) <
2206
- DateTime.toEpochMillis(replacement.inspection.session.issuedAt) ||
2207
- DateTime.toEpochMillis(now) >=
2208
- Math.min(
2209
- DateTime.toEpochMillis(replacement.inspection.session.expiresAt),
2210
- DateTime.toEpochMillis(stored.intent.expiresAt),
2211
- )
2212
- )
2213
- return yield* SessionStepUpInvalid.make({});
2214
- let rotation: any;
2215
-
2216
- if (replacement._tag === "Stateful") {
2217
- if (mapping.source.kind !== "Stateful") return yield* SessionStepUpInvalid.make({});
2218
-
2219
- const s = mapping.source.session,
2220
- row = stored.source.row;
2221
-
2222
- if (
2223
- row[s.digest] !== replacement.expectedDigest ||
2224
- row[s.version] !== replacement.expectedRowVersion
2225
- )
2226
- return yield* SessionConflict.make({});
2227
-
2228
- const version = yield* allocate(
2229
- options.mode,
2230
- s.allocateVersion,
2231
- s.allocateVersionSync,
2232
- );
2233
-
2234
- const next: StatefulSessionRecord<Claims> = {
2235
- ...replacement.inspection.session,
2236
- provenance: replacement.inspection.provenance,
2237
- credentialVersion: replacement.inspection.credentialVersion,
2238
- digest: replacement.nextDigest,
2239
- version,
2240
- };
2241
-
2242
- rotation = {
2243
- ...s.encodeRotation(next),
2244
- ...updateValues([
2245
- [s.version, version],
2246
- [s.digest, next.digest],
2247
- [s.credentialVersion, next.credentialVersion],
2248
- [s.authenticatedAt, s.encodeInstant(next.assurance.authenticatedAt)],
2249
- [s.issuedAt, s.encodeInstant(next.issuedAt)],
2250
- [s.expiresAt, s.encodeInstant(next.expiresAt)],
2251
- [s.absoluteExpiresAt, s.encodeInstant(next.absoluteExpiresAt)],
2252
- ]),
2253
- };
2254
- }
2255
-
2256
- const completionVersion = yield* allocate(
2257
- options.mode,
2258
- m.allocateVersion,
2259
- m.allocateVersionSync,
2260
- );
2261
-
2262
- if (completionVersion === stored.intent.version) return yield* unavailable();
2263
-
2264
- const completionSnapshot = yield* encodeStepUpIntent({
2265
- ...stored.intent,
2266
- version: completionVersion,
2267
- });
2268
-
2269
- const receipt = prepare(replacement.inspection, journal);
2270
-
2271
- if (replacement._tag === "Stateful" && mapping.source.kind === "Stateful") {
2272
- const s = mapping.source.session,
2273
- sc = sessionColumns(mapping.source);
2274
-
2275
- yield* tx
2276
- .update(s.table)
2277
- .set(rotation)
2278
- .where(
2279
- and(
2280
- eq(sc.sessionId, stored.source.nativeSessionId),
2281
- eq(sc.digest, replacement.expectedDigest),
2282
- eq(sc.version, replacement.expectedRowVersion),
2283
- ),
2284
- );
2285
-
2286
- const written = (yield* tx
2287
- .select()
2288
- .from(s.table)
2289
- .where(eq(sc.sessionId, stored.source.nativeSessionId))
2290
- .limit(1))[0];
2291
-
2292
- if (written === undefined || written[s.version] !== rotation[s.version])
2293
- return yield* unavailable();
2294
- const decoded = yield* s.decode(written);
2295
-
2296
- if (decoded.version !== rotation[s.version] || !stepUpRotationMatches(plan, decoded))
2297
- return yield* unavailable();
2298
- } else if (
2299
- replacement._tag === "StateAssistedSigned" &&
2300
- mapping.source.kind === "StateAssistedSigned"
2301
- ) {
2302
- const t = mapping.source.tombstone;
2303
-
2304
- const owner = column(t.table, t.subjectId),
2305
- id = column(t.table, t.sessionId),
2306
- expiry = column(t.table, t.absoluteExpiresAt);
2307
-
2308
- yield* tx
2309
- .delete(t.table)
2310
- .where(
2311
- and(
2312
- eq(owner, stored.nativeSubjectId),
2313
- eq(id, stored.source.nativeSessionId),
2314
- lte(expiry, t.encodeInstant(yield* freshNow)),
2315
- ),
2316
- );
2317
-
2318
- const values = t.encodeInsert({
2319
- subjectId: stored.nativeSubjectId,
2320
- sessionId: stored.source.nativeSessionId,
2321
- absoluteExpiresAt: replacement.tombstoneUntil,
2322
- });
2323
-
2324
- yield* tx.insert(t.table).values(values).pipe(normalizeMutation(mapping));
2325
-
2326
- const inserted = (yield* tx
2327
- .select()
2328
- .from(t.table)
2329
- .where(
2330
- and(
2331
- eq(owner, stored.nativeSubjectId),
2332
- eq(id, stored.source.nativeSessionId),
2333
- eq(expiry, t.encodeInstant(replacement.tombstoneUntil)),
2334
- ),
2335
- )
2336
- .limit(1))[0];
2337
-
2338
- if (inserted === undefined) return yield* unavailable();
2339
- }
2340
- yield* tx
2341
- .update(m.table)
2342
- .set(
2343
- updateValues([
2344
- [m.consumed, true],
2345
- [m.version, completionVersion],
2346
- [m.snapshot, completionSnapshot],
2347
- ]),
2348
- )
2349
- .where(
2350
- and(
2351
- eq(c.digest, stored.intent.digest),
2352
- eq(c.version, stored.intent.version),
2353
- eq(c.snapshot, stored.row[m.snapshot]),
2354
- eq(c.consumed, false),
2355
- eq(c.failedAttempts, stored.row[m.failedAttempts]),
2356
- ),
2357
- );
2358
- const completed = yield* stepUpLocator(mapping, stored.intent.digest, false);
2359
-
2360
- if (
2361
- completed === undefined ||
2362
- !completed[m.consumed] ||
2363
- completed[m.version] !== completionVersion ||
2364
- completed[m.snapshot] !== completionSnapshot
2365
- )
2366
- return yield* unavailable();
2367
- const commitNow = yield* freshNow;
2368
-
2369
- if (
2370
- !stepUpIntentLive(stored.intent, mapping.source.kind, commitNow) ||
2371
- DateTime.toEpochMillis(commitNow) >=
2372
- DateTime.toEpochMillis(replacement.inspection.session.expiresAt) ||
2373
- !(yield* assessAuthentication(plan.evidence, current.requirement).pipe(
2374
- Effect.mapError(() => stale()),
2375
- )).satisfied ||
2376
- !(yield* assessAuthentication(plan.evidence, stored.intent.requirement).pipe(
2377
- Effect.mapError(() => stale()),
2378
- )).satisfied
2379
- )
2380
- return yield* stale();
2381
-
2382
- return receipt;
2383
- }),
2384
- ),
2385
- };
2386
-
2387
- return {
2388
- create: (input, now, prepare) =>
2389
- service.create(input, now, prepare).pipe(Effect.provideService(LifecycleHooks, hooks)),
2390
- context: (input) => service.context(input).pipe(Effect.provideService(LifecycleHooks, hooks)),
2391
- read: (input) => service.read(input).pipe(Effect.provideService(LifecycleHooks, hooks)),
2392
- reject: (input, prepare) =>
2393
- service.reject(input, prepare).pipe(Effect.provideService(LifecycleHooks, hooks)),
2394
- complete: (plan, prepare) =>
2395
- service.complete(plan, prepare).pipe(Effect.provideService(LifecycleHooks, hooks)),
2396
- } satisfies SessionStepUpPersistence<Claims>;
2397
- });
2398
-
2399
- return {
2400
- makeSqlAuthenticationAuthority,
2401
- makeSqlPendingAuthentication,
2402
- makeSqlStatefulSessions,
2403
- makeSqlSignedValidity,
2404
- makeSqlSessionStepUp,
2405
- };
2406
- };