@yielded/auth-persistence 0.1.0-beta.7

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 (526) hide show
  1. package/README.md +21 -0
  2. package/dist/Drizzle.d.mts +28 -0
  3. package/dist/Drizzle.mjs +28 -0
  4. package/dist/DrizzleD1.d.mts +963 -0
  5. package/dist/DrizzleD1.mjs +29 -0
  6. package/dist/DrizzleD1.mjs.map +1 -0
  7. package/dist/DrizzleLibsql.d.mts +2284 -0
  8. package/dist/DrizzleLibsql.mjs +92 -0
  9. package/dist/DrizzleLibsql.mjs.map +1 -0
  10. package/dist/DrizzleMysql2.d.mts +1820 -0
  11. package/dist/DrizzleMysql2.mjs +55 -0
  12. package/dist/DrizzleMysql2.mjs.map +1 -0
  13. package/dist/DrizzlePglite.d.mts +1826 -0
  14. package/dist/DrizzlePglite.mjs +53 -0
  15. package/dist/DrizzlePglite.mjs.map +1 -0
  16. package/dist/DrizzlePostgres.d.mts +1826 -0
  17. package/dist/DrizzlePostgres.mjs +53 -0
  18. package/dist/DrizzlePostgres.mjs.map +1 -0
  19. package/dist/DrizzleSqliteBun.d.mts +2279 -0
  20. package/dist/DrizzleSqliteBun.mjs +70 -0
  21. package/dist/DrizzleSqliteBun.mjs.map +1 -0
  22. package/dist/DrizzleSqliteDo.d.mts +2329 -0
  23. package/dist/DrizzleSqliteDo.mjs +105 -0
  24. package/dist/DrizzleSqliteDo.mjs.map +1 -0
  25. package/dist/DrizzleSqliteNode.d.mts +2279 -0
  26. package/dist/DrizzleSqliteNode.mjs +70 -0
  27. package/dist/DrizzleSqliteNode.mjs.map +1 -0
  28. package/dist/DrizzleSqliteWasm.d.mts +2282 -0
  29. package/dist/DrizzleSqliteWasm.mjs +70 -0
  30. package/dist/DrizzleSqliteWasm.mjs.map +1 -0
  31. package/dist/drizzle/D1BatchStatements.d.mts +11 -0
  32. package/dist/drizzle/D1BatchStatements.mjs +8 -0
  33. package/dist/drizzle/D1BatchStatements.mjs.map +1 -0
  34. package/dist/drizzle/PasswordPreparedPostconditions.mjs +12 -0
  35. package/dist/drizzle/PasswordPreparedPostconditions.mjs.map +1 -0
  36. package/dist/drizzle/SuppliedService.d.mts +10 -0
  37. package/dist/drizzle/d1-emails.d.mts +62 -0
  38. package/dist/drizzle/d1-emails.mjs +580 -0
  39. package/dist/drizzle/d1-emails.mjs.map +1 -0
  40. package/dist/drizzle/d1-generated-statement.d.mts +2 -0
  41. package/dist/drizzle/d1-generated-statement.mjs +79 -0
  42. package/dist/drizzle/d1-generated-statement.mjs.map +1 -0
  43. package/dist/drizzle/d1-identity.d.mts +31 -0
  44. package/dist/drizzle/d1-identity.mjs +119 -0
  45. package/dist/drizzle/d1-identity.mjs.map +1 -0
  46. package/dist/drizzle/d1-oauth.d.mts +301 -0
  47. package/dist/drizzle/d1-oauth.mjs +76 -0
  48. package/dist/drizzle/d1-oauth.mjs.map +1 -0
  49. package/dist/drizzle/d1-passkey.d.mts +51 -0
  50. package/dist/drizzle/d1-passkey.mjs +95 -0
  51. package/dist/drizzle/d1-passkey.mjs.map +1 -0
  52. package/dist/drizzle/d1-password-prepared.d.mts +25 -0
  53. package/dist/drizzle/d1-password-prepared.mjs +461 -0
  54. package/dist/drizzle/d1-password-prepared.mjs.map +1 -0
  55. package/dist/drizzle/d1-passwords.d.mts +87 -0
  56. package/dist/drizzle/d1-passwords.mjs +1004 -0
  57. package/dist/drizzle/d1-passwords.mjs.map +1 -0
  58. package/dist/drizzle/d1-phone.d.mts +26 -0
  59. package/dist/drizzle/d1-phone.mjs +30 -0
  60. package/dist/drizzle/d1-phone.mjs.map +1 -0
  61. package/dist/drizzle/d1-planning.d.mts +12 -0
  62. package/dist/drizzle/d1-planning.mjs +43 -0
  63. package/dist/drizzle/d1-planning.mjs.map +1 -0
  64. package/dist/drizzle/d1-proofs.d.mts +80 -0
  65. package/dist/drizzle/d1-proofs.mjs +830 -0
  66. package/dist/drizzle/d1-proofs.mjs.map +1 -0
  67. package/dist/drizzle/d1-sessions.d.mts +67 -0
  68. package/dist/drizzle/d1-sessions.mjs +966 -0
  69. package/dist/drizzle/d1-sessions.mjs.map +1 -0
  70. package/dist/drizzle/d1-totp.d.mts +26 -0
  71. package/dist/drizzle/d1-totp.mjs +27 -0
  72. package/dist/drizzle/d1-totp.mjs.map +1 -0
  73. package/dist/drizzle/email-model.d.mts +354 -0
  74. package/dist/drizzle/email-model.mjs +21 -0
  75. package/dist/drizzle/email-model.mjs.map +1 -0
  76. package/dist/drizzle/email-registration.d.mts +30 -0
  77. package/dist/drizzle/email-registration.mjs +238 -0
  78. package/dist/drizzle/email-registration.mjs.map +1 -0
  79. package/dist/drizzle/email-sql.d.mts +4 -0
  80. package/dist/drizzle/email-sql.mjs +9 -0
  81. package/dist/drizzle/email-sql.mjs.map +1 -0
  82. package/dist/drizzle/email-target.d.mts +20 -0
  83. package/dist/drizzle/email-target.mjs +72 -0
  84. package/dist/drizzle/email-target.mjs.map +1 -0
  85. package/dist/drizzle/model.d.mts +250 -0
  86. package/dist/drizzle/model.mjs +58 -0
  87. package/dist/drizzle/model.mjs.map +1 -0
  88. package/dist/drizzle/mysql-emails.d.mts +71 -0
  89. package/dist/drizzle/mysql-emails.mjs +38 -0
  90. package/dist/drizzle/mysql-emails.mjs.map +1 -0
  91. package/dist/drizzle/mysql-identity.d.mts +22 -0
  92. package/dist/drizzle/mysql-identity.mjs +94 -0
  93. package/dist/drizzle/mysql-identity.mjs.map +1 -0
  94. package/dist/drizzle/mysql-password-prepared.d.mts +30 -0
  95. package/dist/drizzle/mysql-password-prepared.mjs +16 -0
  96. package/dist/drizzle/mysql-password-prepared.mjs.map +1 -0
  97. package/dist/drizzle/mysql-passwords.d.mts +86 -0
  98. package/dist/drizzle/mysql-passwords.mjs +38 -0
  99. package/dist/drizzle/mysql-passwords.mjs.map +1 -0
  100. package/dist/drizzle/mysql-proofs.d.mts +75 -0
  101. package/dist/drizzle/mysql-proofs.mjs +23 -0
  102. package/dist/drizzle/mysql-proofs.mjs.map +1 -0
  103. package/dist/drizzle/mysql-sessions.d.mts +237 -0
  104. package/dist/drizzle/mysql-sessions.mjs +53 -0
  105. package/dist/drizzle/mysql-sessions.mjs.map +1 -0
  106. package/dist/drizzle/mysql.d.mts +70 -0
  107. package/dist/drizzle/mysql.mjs +218 -0
  108. package/dist/drizzle/mysql.mjs.map +1 -0
  109. package/dist/drizzle/oauth-accounts.mjs +358 -0
  110. package/dist/drizzle/oauth-accounts.mjs.map +1 -0
  111. package/dist/drizzle/oauth-connected-access.mjs +210 -0
  112. package/dist/drizzle/oauth-connected-access.mjs.map +1 -0
  113. package/dist/drizzle/oauth-connected-collection.mjs +202 -0
  114. package/dist/drizzle/oauth-connected-collection.mjs.map +1 -0
  115. package/dist/drizzle/oauth-connected-custody.mjs +146 -0
  116. package/dist/drizzle/oauth-connected-custody.mjs.map +1 -0
  117. package/dist/drizzle/oauth-connected-drivers.mjs +76 -0
  118. package/dist/drizzle/oauth-connected-drivers.mjs.map +1 -0
  119. package/dist/drizzle/oauth-connected-flow.mjs +247 -0
  120. package/dist/drizzle/oauth-connected-flow.mjs.map +1 -0
  121. package/dist/drizzle/oauth-connected-input.mjs +103 -0
  122. package/dist/drizzle/oauth-connected-input.mjs.map +1 -0
  123. package/dist/drizzle/oauth-connected-maintenance.mjs +214 -0
  124. package/dist/drizzle/oauth-connected-maintenance.mjs.map +1 -0
  125. package/dist/drizzle/oauth-connected-management.mjs +228 -0
  126. package/dist/drizzle/oauth-connected-management.mjs.map +1 -0
  127. package/dist/drizzle/oauth-connected-model.d.mts +278 -0
  128. package/dist/drizzle/oauth-connected-model.mjs +22 -0
  129. package/dist/drizzle/oauth-connected-model.mjs.map +1 -0
  130. package/dist/drizzle/oauth-connected-reference.d.mts +17 -0
  131. package/dist/drizzle/oauth-connected-reference.mjs +53 -0
  132. package/dist/drizzle/oauth-connected-reference.mjs.map +1 -0
  133. package/dist/drizzle/oauth-connected-settlement.mjs +117 -0
  134. package/dist/drizzle/oauth-connected-settlement.mjs.map +1 -0
  135. package/dist/drizzle/oauth-connected-state.d.mts +14 -0
  136. package/dist/drizzle/oauth-connected-state.mjs +422 -0
  137. package/dist/drizzle/oauth-connected-state.mjs.map +1 -0
  138. package/dist/drizzle/oauth-connected-target.d.mts +753 -0
  139. package/dist/drizzle/oauth-connected-target.mjs +91 -0
  140. package/dist/drizzle/oauth-connected-target.mjs.map +1 -0
  141. package/dist/drizzle/oauth-drivers.mjs +51 -0
  142. package/dist/drizzle/oauth-drivers.mjs.map +1 -0
  143. package/dist/drizzle/oauth-flow.d.mts +5 -0
  144. package/dist/drizzle/oauth-flow.mjs +253 -0
  145. package/dist/drizzle/oauth-flow.mjs.map +1 -0
  146. package/dist/drizzle/oauth-input.mjs +127 -0
  147. package/dist/drizzle/oauth-input.mjs.map +1 -0
  148. package/dist/drizzle/oauth-model.d.mts +463 -0
  149. package/dist/drizzle/oauth-model.mjs +49 -0
  150. package/dist/drizzle/oauth-model.mjs.map +1 -0
  151. package/dist/drizzle/oauth-mysql.mjs +8 -0
  152. package/dist/drizzle/oauth-mysql.mjs.map +1 -0
  153. package/dist/drizzle/oauth-owner.d.mts +4 -0
  154. package/dist/drizzle/oauth-owner.mjs +10 -0
  155. package/dist/drizzle/oauth-owner.mjs.map +1 -0
  156. package/dist/drizzle/oauth-registration.d.mts +6 -0
  157. package/dist/drizzle/oauth-registration.mjs +374 -0
  158. package/dist/drizzle/oauth-registration.mjs.map +1 -0
  159. package/dist/drizzle/oauth-state.mjs +107 -0
  160. package/dist/drizzle/oauth-state.mjs.map +1 -0
  161. package/dist/drizzle/oauth-target.d.mts +111 -0
  162. package/dist/drizzle/oauth-target.mjs +246 -0
  163. package/dist/drizzle/oauth-target.mjs.map +1 -0
  164. package/dist/drizzle/passkey-drivers.mjs +34 -0
  165. package/dist/drizzle/passkey-drivers.mjs.map +1 -0
  166. package/dist/drizzle/passkey-kernel.mjs +8 -0
  167. package/dist/drizzle/passkey-kernel.mjs.map +1 -0
  168. package/dist/drizzle/passkey-model.d.mts +272 -0
  169. package/dist/drizzle/passkey-model.mjs +34 -0
  170. package/dist/drizzle/passkey-model.mjs.map +1 -0
  171. package/dist/drizzle/passkey-registration-ceremony-model.d.mts +50 -0
  172. package/dist/drizzle/passkey-registration-ceremony-model.mjs +18 -0
  173. package/dist/drizzle/passkey-registration-ceremony-model.mjs.map +1 -0
  174. package/dist/drizzle/passkey-state.mjs +7 -0
  175. package/dist/drizzle/passkey-state.mjs.map +1 -0
  176. package/dist/drizzle/passkey-target.d.mts +9 -0
  177. package/dist/drizzle/passkey-target.mjs +7 -0
  178. package/dist/drizzle/passkey-target.mjs.map +1 -0
  179. package/dist/drizzle/passkey-write-drivers.mjs +32 -0
  180. package/dist/drizzle/passkey-write-drivers.mjs.map +1 -0
  181. package/dist/drizzle/passkey-write-model.d.mts +190 -0
  182. package/dist/drizzle/passkey-write-model.mjs +13 -0
  183. package/dist/drizzle/passkey-write-model.mjs.map +1 -0
  184. package/dist/drizzle/passkey-write-target.d.mts +11 -0
  185. package/dist/drizzle/passkey-write-target.mjs +7 -0
  186. package/dist/drizzle/passkey-write-target.mjs.map +1 -0
  187. package/dist/drizzle/password-model.d.mts +329 -0
  188. package/dist/drizzle/password-model.mjs +23 -0
  189. package/dist/drizzle/password-model.mjs.map +1 -0
  190. package/dist/drizzle/password-prepared-model.d.mts +94 -0
  191. package/dist/drizzle/password-prepared-model.mjs +11 -0
  192. package/dist/drizzle/password-prepared-model.mjs.map +1 -0
  193. package/dist/drizzle/password-prepared-sql.mjs +390 -0
  194. package/dist/drizzle/password-prepared-sql.mjs.map +1 -0
  195. package/dist/drizzle/password-prepared-state.mjs +104 -0
  196. package/dist/drizzle/password-prepared-state.mjs.map +1 -0
  197. package/dist/drizzle/password-prepared-target.d.mts +12 -0
  198. package/dist/drizzle/password-prepared-target.mjs +56 -0
  199. package/dist/drizzle/password-prepared-target.mjs.map +1 -0
  200. package/dist/drizzle/password-registration.d.mts +19 -0
  201. package/dist/drizzle/password-registration.mjs +135 -0
  202. package/dist/drizzle/password-registration.mjs.map +1 -0
  203. package/dist/drizzle/password-sql.d.mts +5 -0
  204. package/dist/drizzle/password-sql.mjs +9 -0
  205. package/dist/drizzle/password-sql.mjs.map +1 -0
  206. package/dist/drizzle/password-target.d.mts +17 -0
  207. package/dist/drizzle/password-target.mjs +61 -0
  208. package/dist/drizzle/password-target.mjs.map +1 -0
  209. package/dist/drizzle/pg-emails.d.mts +72 -0
  210. package/dist/drizzle/pg-emails.mjs +38 -0
  211. package/dist/drizzle/pg-emails.mjs.map +1 -0
  212. package/dist/drizzle/pg-password-prepared.d.mts +31 -0
  213. package/dist/drizzle/pg-password-prepared.mjs +16 -0
  214. package/dist/drizzle/pg-password-prepared.mjs.map +1 -0
  215. package/dist/drizzle/pg-passwords.d.mts +87 -0
  216. package/dist/drizzle/pg-passwords.mjs +38 -0
  217. package/dist/drizzle/pg-passwords.mjs.map +1 -0
  218. package/dist/drizzle/pg-proofs.d.mts +76 -0
  219. package/dist/drizzle/pg-proofs.mjs +22 -0
  220. package/dist/drizzle/pg-proofs.mjs.map +1 -0
  221. package/dist/drizzle/pg-sessions.d.mts +238 -0
  222. package/dist/drizzle/pg-sessions.mjs +53 -0
  223. package/dist/drizzle/pg-sessions.mjs.map +1 -0
  224. package/dist/drizzle/pg.d.mts +81 -0
  225. package/dist/drizzle/pg.mjs +290 -0
  226. package/dist/drizzle/pg.mjs.map +1 -0
  227. package/dist/drizzle/phone-model.d.mts +115 -0
  228. package/dist/drizzle/phone-model.mjs +39 -0
  229. package/dist/drizzle/phone-model.mjs.map +1 -0
  230. package/dist/drizzle/phone-state.mjs +16 -0
  231. package/dist/drizzle/phone-state.mjs.map +1 -0
  232. package/dist/drizzle/phone-target.d.mts +17 -0
  233. package/dist/drizzle/phone-target.mjs +170 -0
  234. package/dist/drizzle/phone-target.mjs.map +1 -0
  235. package/dist/drizzle/proof-model.d.mts +286 -0
  236. package/dist/drizzle/proof-model.mjs +17 -0
  237. package/dist/drizzle/proof-model.mjs.map +1 -0
  238. package/dist/drizzle/proof-sql.d.mts +4 -0
  239. package/dist/drizzle/proof-sql.mjs +8 -0
  240. package/dist/drizzle/proof-sql.mjs.map +1 -0
  241. package/dist/drizzle/proof-target.d.mts +12 -0
  242. package/dist/drizzle/proof-target.mjs +36 -0
  243. package/dist/drizzle/proof-target.mjs.map +1 -0
  244. package/dist/drizzle/query-operations.mjs +26 -0
  245. package/dist/drizzle/query-operations.mjs.map +1 -0
  246. package/dist/drizzle/session-model.d.mts +200 -0
  247. package/dist/drizzle/session-model.mjs +20 -0
  248. package/dist/drizzle/session-model.mjs.map +1 -0
  249. package/dist/drizzle/session-sql.mjs +8 -0
  250. package/dist/drizzle/session-sql.mjs.map +1 -0
  251. package/dist/drizzle/session-target.d.mts +28 -0
  252. package/dist/drizzle/session-target.mjs +47 -0
  253. package/dist/drizzle/session-target.mjs.map +1 -0
  254. package/dist/drizzle/sqlite-emails.mjs +45 -0
  255. package/dist/drizzle/sqlite-emails.mjs.map +1 -0
  256. package/dist/drizzle/sqlite-identity.mjs +91 -0
  257. package/dist/drizzle/sqlite-identity.mjs.map +1 -0
  258. package/dist/drizzle/sqlite-password-prepared.mjs +21 -0
  259. package/dist/drizzle/sqlite-password-prepared.mjs.map +1 -0
  260. package/dist/drizzle/sqlite-passwords.mjs +49 -0
  261. package/dist/drizzle/sqlite-passwords.mjs.map +1 -0
  262. package/dist/drizzle/sqlite-proofs.mjs +29 -0
  263. package/dist/drizzle/sqlite-proofs.mjs.map +1 -0
  264. package/dist/drizzle/sqlite-sessions.mjs +74 -0
  265. package/dist/drizzle/sqlite-sessions.mjs.map +1 -0
  266. package/dist/drizzle/sqlite.d.mts +78 -0
  267. package/dist/drizzle/sqlite.mjs +217 -0
  268. package/dist/drizzle/sqlite.mjs.map +1 -0
  269. package/dist/drizzle/standalone-guard.mjs +19 -0
  270. package/dist/drizzle/standalone-guard.mjs.map +1 -0
  271. package/dist/drizzle/step-up-model.d.mts +65 -0
  272. package/dist/drizzle/step-up-model.mjs +9 -0
  273. package/dist/drizzle/step-up-model.mjs.map +1 -0
  274. package/dist/drizzle/step-up-state.mjs +32 -0
  275. package/dist/drizzle/step-up-state.mjs.map +1 -0
  276. package/dist/drizzle/totp-model.d.mts +84 -0
  277. package/dist/drizzle/totp-model.mjs +13 -0
  278. package/dist/drizzle/totp-model.mjs.map +1 -0
  279. package/dist/drizzle/totp-state.d.mts +38 -0
  280. package/dist/drizzle/totp-state.mjs +287 -0
  281. package/dist/drizzle/totp-state.mjs.map +1 -0
  282. package/dist/drizzle/totp-target.d.mts +17 -0
  283. package/dist/drizzle/totp-target.mjs +81 -0
  284. package/dist/drizzle/totp-target.mjs.map +1 -0
  285. package/dist/drizzle/transaction-execution.d.mts +4 -0
  286. package/dist/drizzle/transaction-execution.mjs +11 -0
  287. package/dist/drizzle/transaction-execution.mjs.map +1 -0
  288. package/dist/drizzle/transaction-mysql.mjs +46 -0
  289. package/dist/drizzle/transaction-mysql.mjs.map +1 -0
  290. package/dist/drizzle/transaction-owner.d.mts +2 -0
  291. package/dist/drizzle/transaction-owner.mjs +8 -0
  292. package/dist/drizzle/transaction-owner.mjs.map +1 -0
  293. package/dist/index.d.mts +5 -0
  294. package/dist/index.mjs +5 -0
  295. package/dist/internal/configuration.d.mts +174 -0
  296. package/dist/internal/configuration.mjs +7 -0
  297. package/dist/internal/configuration.mjs.map +1 -0
  298. package/dist/internal/drizzle-migrations.mjs +19 -0
  299. package/dist/internal/drizzle-migrations.mjs.map +1 -0
  300. package/dist/internal/drizzle-postgres.mjs +39 -0
  301. package/dist/internal/drizzle-postgres.mjs.map +1 -0
  302. package/dist/internal/drizzle-sqlite.mjs +37 -0
  303. package/dist/internal/drizzle-sqlite.mjs.map +1 -0
  304. package/dist/internal/email-kernel.d.mts +13 -0
  305. package/dist/internal/email-kernel.mjs +487 -0
  306. package/dist/internal/email-kernel.mjs.map +1 -0
  307. package/dist/internal/mapping-error.d.mts +11 -0
  308. package/dist/internal/mapping-error.mjs +30 -0
  309. package/dist/internal/mapping-error.mjs.map +1 -0
  310. package/dist/internal/passkey/admission.d.mts +7 -0
  311. package/dist/internal/passkey/admission.mjs +216 -0
  312. package/dist/internal/passkey/admission.mjs.map +1 -0
  313. package/dist/internal/passkey/credentials.d.mts +6 -0
  314. package/dist/internal/passkey/credentials.mjs +249 -0
  315. package/dist/internal/passkey/credentials.mjs.map +1 -0
  316. package/dist/internal/passkey/enrollment.mjs +138 -0
  317. package/dist/internal/passkey/enrollment.mjs.map +1 -0
  318. package/dist/internal/passkey/flow.d.mts +10 -0
  319. package/dist/internal/passkey/flow.mjs +452 -0
  320. package/dist/internal/passkey/flow.mjs.map +1 -0
  321. package/dist/internal/passkey/management.mjs +231 -0
  322. package/dist/internal/passkey/management.mjs.map +1 -0
  323. package/dist/internal/passkey/registration-ceremony.d.mts +9 -0
  324. package/dist/internal/passkey/registration-ceremony.mjs +129 -0
  325. package/dist/internal/passkey/registration-ceremony.mjs.map +1 -0
  326. package/dist/internal/passkey/registration-custody.d.mts +2 -0
  327. package/dist/internal/passkey/registration-custody.mjs +52 -0
  328. package/dist/internal/passkey/registration-custody.mjs.map +1 -0
  329. package/dist/internal/passkey/registration-write.mjs +211 -0
  330. package/dist/internal/passkey/registration-write.mjs.map +1 -0
  331. package/dist/internal/passkey/state.d.mts +5 -0
  332. package/dist/internal/passkey/state.mjs +234 -0
  333. package/dist/internal/passkey/state.mjs.map +1 -0
  334. package/dist/internal/passkey/target.d.mts +19 -0
  335. package/dist/internal/passkey/target.mjs +190 -0
  336. package/dist/internal/passkey/target.mjs.map +1 -0
  337. package/dist/internal/passkey/write-state.mjs +345 -0
  338. package/dist/internal/passkey/write-state.mjs.map +1 -0
  339. package/dist/internal/passkey/write-target.mjs +192 -0
  340. package/dist/internal/passkey/write-target.mjs.map +1 -0
  341. package/dist/internal/passkey-kernel.mjs +49 -0
  342. package/dist/internal/passkey-kernel.mjs.map +1 -0
  343. package/dist/internal/passkeys.mjs +367 -0
  344. package/dist/internal/passkeys.mjs.map +1 -0
  345. package/dist/internal/password-kernel.d.mts +13 -0
  346. package/dist/internal/password-kernel.mjs +704 -0
  347. package/dist/internal/password-kernel.mjs.map +1 -0
  348. package/dist/internal/persistence.mjs +289 -0
  349. package/dist/internal/persistence.mjs.map +1 -0
  350. package/dist/internal/phone-kernel.mjs +434 -0
  351. package/dist/internal/phone-kernel.mjs.map +1 -0
  352. package/dist/internal/proof-kernel.d.mts +9 -0
  353. package/dist/internal/proof-kernel.mjs +619 -0
  354. package/dist/internal/proof-kernel.mjs.map +1 -0
  355. package/dist/internal/query-operations.d.mts +3 -0
  356. package/dist/internal/registration.mjs +65 -0
  357. package/dist/internal/registration.mjs.map +1 -0
  358. package/dist/internal/session-kernel.mjs +737 -0
  359. package/dist/internal/session-kernel.mjs.map +1 -0
  360. package/dist/internal/sql-persistence.d.mts +11 -0
  361. package/dist/internal/sql-persistence.mjs +37 -0
  362. package/dist/internal/sql-persistence.mjs.map +1 -0
  363. package/dist/internal/sql-query.mjs +207 -0
  364. package/dist/internal/sql-query.mjs.map +1 -0
  365. package/dist/internal/sql-table.d.mts +37 -0
  366. package/dist/internal/sql-table.mjs +71 -0
  367. package/dist/internal/sql-table.mjs.map +1 -0
  368. package/dist/internal/storage-mapping.mjs +687 -0
  369. package/dist/internal/storage-mapping.mjs.map +1 -0
  370. package/dist/internal/storage-tables.d.mts +914 -0
  371. package/dist/internal/storage-tables.mjs +387 -0
  372. package/dist/internal/storage-tables.mjs.map +1 -0
  373. package/dist/internal/storage-validation.mjs +58 -0
  374. package/dist/internal/storage-validation.mjs.map +1 -0
  375. package/dist/internal/transaction-execution-kernel.d.mts +18 -0
  376. package/dist/internal/transaction-execution-kernel.mjs +118 -0
  377. package/dist/internal/transaction-execution-kernel.mjs.map +1 -0
  378. package/dist/internal/transaction-kernel.d.mts +7 -0
  379. package/dist/internal/transaction-kernel.mjs +260 -0
  380. package/dist/internal/transaction-kernel.mjs.map +1 -0
  381. package/package.json +1 -0
  382. package/src/Drizzle.ts +378 -0
  383. package/src/DrizzleD1.ts +136 -0
  384. package/src/DrizzleLibsql.ts +301 -0
  385. package/src/DrizzleMysql2.ts +184 -0
  386. package/src/DrizzlePglite.ts +174 -0
  387. package/src/DrizzlePostgres.ts +174 -0
  388. package/src/DrizzleSqliteBun.ts +258 -0
  389. package/src/DrizzleSqliteDo.ts +473 -0
  390. package/src/DrizzleSqliteNode.ts +259 -0
  391. package/src/DrizzleSqliteWasm.ts +263 -0
  392. package/src/drizzle/D1BatchStatements.ts +10 -0
  393. package/src/drizzle/PasswordPreparedPostconditions.ts +30 -0
  394. package/src/drizzle/SuppliedService.ts +10 -0
  395. package/src/drizzle/d1-emails.ts +1829 -0
  396. package/src/drizzle/d1-generated-statement.ts +129 -0
  397. package/src/drizzle/d1-identity.ts +375 -0
  398. package/src/drizzle/d1-oauth.ts +340 -0
  399. package/src/drizzle/d1-passkey.ts +373 -0
  400. package/src/drizzle/d1-password-prepared.ts +1270 -0
  401. package/src/drizzle/d1-passwords.ts +2656 -0
  402. package/src/drizzle/d1-phone.ts +96 -0
  403. package/src/drizzle/d1-planning.ts +104 -0
  404. package/src/drizzle/d1-proofs.ts +2305 -0
  405. package/src/drizzle/d1-sessions.ts +2857 -0
  406. package/src/drizzle/d1-totp.ts +78 -0
  407. package/src/drizzle/email-model.ts +516 -0
  408. package/src/drizzle/email-registration.ts +698 -0
  409. package/src/drizzle/email-sql.ts +24 -0
  410. package/src/drizzle/email-target.ts +243 -0
  411. package/src/drizzle/model.ts +463 -0
  412. package/src/drizzle/mysql-emails.ts +321 -0
  413. package/src/drizzle/mysql-identity.ts +324 -0
  414. package/src/drizzle/mysql-password-prepared.ts +204 -0
  415. package/src/drizzle/mysql-passwords.ts +339 -0
  416. package/src/drizzle/mysql-proofs.ts +181 -0
  417. package/src/drizzle/mysql-sessions.ts +741 -0
  418. package/src/drizzle/mysql.ts +687 -0
  419. package/src/drizzle/oauth-accounts.ts +804 -0
  420. package/src/drizzle/oauth-connected-access.ts +354 -0
  421. package/src/drizzle/oauth-connected-collection.ts +324 -0
  422. package/src/drizzle/oauth-connected-custody.ts +277 -0
  423. package/src/drizzle/oauth-connected-drivers.ts +465 -0
  424. package/src/drizzle/oauth-connected-flow.ts +441 -0
  425. package/src/drizzle/oauth-connected-input.ts +127 -0
  426. package/src/drizzle/oauth-connected-maintenance.ts +345 -0
  427. package/src/drizzle/oauth-connected-management.ts +365 -0
  428. package/src/drizzle/oauth-connected-model.ts +347 -0
  429. package/src/drizzle/oauth-connected-reference.ts +112 -0
  430. package/src/drizzle/oauth-connected-settlement.ts +226 -0
  431. package/src/drizzle/oauth-connected-state.ts +845 -0
  432. package/src/drizzle/oauth-connected-target.ts +264 -0
  433. package/src/drizzle/oauth-drivers.ts +569 -0
  434. package/src/drizzle/oauth-flow.ts +462 -0
  435. package/src/drizzle/oauth-input.ts +201 -0
  436. package/src/drizzle/oauth-model.ts +617 -0
  437. package/src/drizzle/oauth-mysql.ts +10 -0
  438. package/src/drizzle/oauth-owner.ts +29 -0
  439. package/src/drizzle/oauth-registration.ts +640 -0
  440. package/src/drizzle/oauth-state.ts +215 -0
  441. package/src/drizzle/oauth-target.ts +750 -0
  442. package/src/drizzle/passkey-drivers.ts +385 -0
  443. package/src/drizzle/passkey-kernel.ts +3 -0
  444. package/src/drizzle/passkey-model.ts +353 -0
  445. package/src/drizzle/passkey-registration-ceremony-model.ts +69 -0
  446. package/src/drizzle/passkey-state.ts +3 -0
  447. package/src/drizzle/passkey-target.ts +23 -0
  448. package/src/drizzle/passkey-write-drivers.ts +395 -0
  449. package/src/drizzle/passkey-write-model.ts +271 -0
  450. package/src/drizzle/passkey-write-target.ts +9 -0
  451. package/src/drizzle/password-model.ts +491 -0
  452. package/src/drizzle/password-prepared-model.ts +259 -0
  453. package/src/drizzle/password-prepared-sql.ts +1191 -0
  454. package/src/drizzle/password-prepared-state.ts +231 -0
  455. package/src/drizzle/password-prepared-target.ts +160 -0
  456. package/src/drizzle/password-registration.ts +374 -0
  457. package/src/drizzle/password-sql.ts +12 -0
  458. package/src/drizzle/password-target.ts +224 -0
  459. package/src/drizzle/pg-emails.ts +321 -0
  460. package/src/drizzle/pg-password-prepared.ts +205 -0
  461. package/src/drizzle/pg-passwords.ts +338 -0
  462. package/src/drizzle/pg-proofs.ts +191 -0
  463. package/src/drizzle/pg-sessions.ts +750 -0
  464. package/src/drizzle/pg.ts +984 -0
  465. package/src/drizzle/phone-model.ts +190 -0
  466. package/src/drizzle/phone-state.ts +21 -0
  467. package/src/drizzle/phone-target.ts +472 -0
  468. package/src/drizzle/proof-model.ts +398 -0
  469. package/src/drizzle/proof-sql.ts +9 -0
  470. package/src/drizzle/proof-target.ts +122 -0
  471. package/src/drizzle/query-operations.ts +37 -0
  472. package/src/drizzle/session-model.ts +370 -0
  473. package/src/drizzle/session-sql.ts +13 -0
  474. package/src/drizzle/session-target.ts +338 -0
  475. package/src/drizzle/sqlite-emails.ts +366 -0
  476. package/src/drizzle/sqlite-identity.ts +344 -0
  477. package/src/drizzle/sqlite-password-prepared.ts +260 -0
  478. package/src/drizzle/sqlite-passwords.ts +497 -0
  479. package/src/drizzle/sqlite-proofs.ts +202 -0
  480. package/src/drizzle/sqlite-sessions.ts +971 -0
  481. package/src/drizzle/sqlite.ts +705 -0
  482. package/src/drizzle/standalone-guard.ts +38 -0
  483. package/src/drizzle/step-up-model.ts +128 -0
  484. package/src/drizzle/step-up-state.ts +139 -0
  485. package/src/drizzle/totp-model.ts +92 -0
  486. package/src/drizzle/totp-state.ts +525 -0
  487. package/src/drizzle/totp-target.ts +308 -0
  488. package/src/drizzle/transaction-execution.ts +12 -0
  489. package/src/drizzle/transaction-mysql.ts +104 -0
  490. package/src/drizzle/transaction-owner.ts +10 -0
  491. package/src/index.ts +4 -0
  492. package/src/internal/configuration.ts +282 -0
  493. package/src/internal/drizzle-migrations.ts +30 -0
  494. package/src/internal/drizzle-postgres.ts +81 -0
  495. package/src/internal/drizzle-sqlite.ts +79 -0
  496. package/src/internal/email-kernel.ts +1532 -0
  497. package/src/internal/mapping-error.ts +36 -0
  498. package/src/internal/passkey/admission.ts +362 -0
  499. package/src/internal/passkey/credentials.ts +511 -0
  500. package/src/internal/passkey/enrollment.ts +326 -0
  501. package/src/internal/passkey/flow.ts +1062 -0
  502. package/src/internal/passkey/management.ts +422 -0
  503. package/src/internal/passkey/registration-ceremony.ts +283 -0
  504. package/src/internal/passkey/registration-custody.ts +91 -0
  505. package/src/internal/passkey/registration-write.ts +426 -0
  506. package/src/internal/passkey/state.ts +354 -0
  507. package/src/internal/passkey/target.ts +586 -0
  508. package/src/internal/passkey/write-state.ts +681 -0
  509. package/src/internal/passkey/write-target.ts +452 -0
  510. package/src/internal/passkey-kernel.ts +102 -0
  511. package/src/internal/passkeys.ts +597 -0
  512. package/src/internal/password-kernel.ts +2122 -0
  513. package/src/internal/persistence.ts +590 -0
  514. package/src/internal/phone-kernel.ts +843 -0
  515. package/src/internal/proof-kernel.ts +1949 -0
  516. package/src/internal/query-operations.ts +26 -0
  517. package/src/internal/registration.ts +160 -0
  518. package/src/internal/session-kernel.ts +2210 -0
  519. package/src/internal/sql-persistence.ts +45 -0
  520. package/src/internal/sql-query.ts +298 -0
  521. package/src/internal/sql-table.ts +89 -0
  522. package/src/internal/storage-mapping.ts +706 -0
  523. package/src/internal/storage-tables.ts +408 -0
  524. package/src/internal/storage-validation.ts +117 -0
  525. package/src/internal/transaction-execution-kernel.ts +317 -0
  526. package/src/internal/transaction-kernel.ts +582 -0
@@ -0,0 +1,2857 @@
1
+ import {
2
+ coordinateCommit,
3
+ CurrentCommitJournal,
4
+ hasCommitScope,
5
+ type PreparedCommit,
6
+ LifecycleHooks,
7
+ type HookConfigurationError,
8
+ } from "@yielded/auth/Hooks";
9
+ import { reportPersistenceFailure } from "@yielded/auth/Persistence";
10
+ import type { SubjectId, TokenDigest } from "@yielded/auth/Schema";
11
+ import {
12
+ assessAuthentication,
13
+ snapshotSessionAuthenticationProvenance,
14
+ AuthenticationAuthority,
15
+ PendingAuthenticationInvalid,
16
+ SessionConflict,
17
+ type SessionConfigurationError,
18
+ SessionInvalid,
19
+ SessionStepUpInvalid,
20
+ SessionUnavailable,
21
+ StaleAuthentication,
22
+ SessionMetadata,
23
+ type AuthenticationEvidence,
24
+ type AuthenticationRequirement,
25
+ type AuthenticationRevision,
26
+ type PendingConsumption,
27
+ type SecurityRevision,
28
+ pendingAuthenticationContext,
29
+ snapshotPendingAuthenticationContext,
30
+ type PendingAuthentication,
31
+ type PendingAuthenticationRecord,
32
+ type PendingAuthenticationState,
33
+ type PrepareSessionCommit,
34
+ type SessionRepository,
35
+ type SignedSessionValidity,
36
+ type StatefulSessionPersistence,
37
+ type SessionStepUpIntent,
38
+ type SessionStepUpCompletionPlan,
39
+ type SessionStepUpPersistence,
40
+ } from "@yielded/auth/Sessions";
41
+ /* oxlint-disable no-explicit-any -- D1 batch planning bridges dynamic consumer Drizzle tables to Effect SQL statements. */
42
+ import { and, eq, getTableColumns, gt, inArray, not, or, sql, type SQL } from "drizzle-orm";
43
+ import type { EffectDrizzleQueryError } from "drizzle-orm/effect-core";
44
+ import type { AnySQLiteTable } from "drizzle-orm/sqlite-core";
45
+ import { Cause, Context, Data, DateTime, Effect, Schema } from "effect";
46
+ import type * as SqlError from "effect/unstable/sql/SqlError";
47
+ import type { Statement } from "effect/unstable/sql/Statement";
48
+
49
+ import { compactD1GeneratedStatement } from "./d1-generated-statement";
50
+ import {
51
+ CurrentD1PlanningDatabase,
52
+ makeD1Owner,
53
+ type D1Owner,
54
+ type D1PlanningDatabase,
55
+ } from "./d1-planning";
56
+ import { D1BatchStatements } from "./D1BatchStatements";
57
+ import { column, PersistenceMappingError, isMappedConstraintConflict, updateValues } from "./model";
58
+ import type {
59
+ D1AuthenticationAuthorityMapping,
60
+ D1PendingAuthenticationMapping,
61
+ D1SignedSessionValidityMapping,
62
+ D1StatefulSessionMapping,
63
+ } from "./session-model";
64
+ import type { D1SessionStepUpMapping } from "./step-up-model";
65
+ import {
66
+ decodeStepUpIntent,
67
+ encodeStepUpIntent,
68
+ stepUpIntentLive,
69
+ validateStepUpPlan,
70
+ } from "./step-up-state";
71
+ import type { SuppliedService } from "./SuppliedService";
72
+
73
+ type Database = D1PlanningDatabase;
74
+ type AnyD1StepUpMapping = D1SessionStepUpMapping<any, any, any, any, any, any, any, any>;
75
+ type D1DomainError =
76
+ | SessionStepUpInvalid
77
+ | PendingAuthenticationInvalid
78
+ | SessionConflict
79
+ | SessionInvalid
80
+ | SessionUnavailable
81
+ | StaleAuthentication;
82
+ type D1PlanError =
83
+ | D1DomainError
84
+ | SessionConfigurationError
85
+ | PersistenceMappingError
86
+ | EffectDrizzleQueryError
87
+ | Schema.SchemaError;
88
+ type D1AdapterFailure = PersistenceMappingError | EffectDrizzleQueryError | Schema.SchemaError;
89
+ type NormalizedD1Error<E> = Extract<E, D1DomainError> | SessionUnavailable;
90
+ type PreparedValue<Method extends (...args: any[]) => any> =
91
+ Parameters<Method>[1] extends PrepareSessionCommit<infer Value, any> ? Value : never;
92
+ interface Planned<A> {
93
+ readonly receipt: A;
94
+ readonly statements: ReadonlyArray<Statement<any>>;
95
+ readonly isConstraintConflict?: (cause: unknown) => boolean;
96
+ readonly recoverGuard?: Effect.Effect<never, D1PlanError, CurrentD1PlanningDatabase>;
97
+ readonly journalGuard?: PreparedCommit<void>;
98
+ }
99
+ interface D1SessionBatch {
100
+ readonly owner: D1Owner<SessionUnavailable>;
101
+ readonly append: (planned: Planned<any>) => Effect.Effect<void, SessionUnavailable>;
102
+ }
103
+ class CurrentD1SessionBatch extends Context.Service<CurrentD1SessionBatch, D1SessionBatch>()(
104
+ "effect-auth/drizzle/CurrentD1SessionBatch",
105
+ ) {}
106
+ class D1BatchFailure extends Data.TaggedError("D1BatchFailure")<{
107
+ readonly cause: Cause.Cause<SqlError.SqlError>;
108
+ readonly mutation?: Planned<any>;
109
+ }> {}
110
+
111
+ const unavailable = () => SessionUnavailable.make({});
112
+ const stale = () => StaleAuthentication.make({});
113
+ const invalidPending = () => PendingAuthenticationInvalid.make({});
114
+ const invalidSession = () => SessionInvalid.make({});
115
+
116
+ const mapFailureCause = <A, E, R, E2>(
117
+ effect: Effect.Effect<A, E, R>,
118
+ map: (error: E) => E2,
119
+ ): Effect.Effect<A, E2, R> =>
120
+ Effect.catchCause(effect, (cause) => Effect.failCause(Cause.map(cause, map)));
121
+
122
+ const isD1DomainError = (error: unknown): error is D1DomainError => {
123
+ const tag = typeof error === "object" && error !== null ? Reflect.get(error, "_tag") : undefined;
124
+
125
+ return (
126
+ tag === "SessionStepUpInvalid" ||
127
+ tag === "PendingAuthenticationInvalid" ||
128
+ tag === "SessionConflict" ||
129
+ tag === "SessionInvalid" ||
130
+ tag === "SessionUnavailable" ||
131
+ tag === "StaleAuthentication"
132
+ );
133
+ };
134
+
135
+ const normalizeD1Failure = <A, E, R>(
136
+ effect: Effect.Effect<A, E, R>,
137
+ isExpected: (error: E) => boolean,
138
+ ): Effect.Effect<A, NormalizedD1Error<E>, R> =>
139
+ reportPersistenceFailure(effect, isExpected).pipe(
140
+ Effect.catchCause((cause) =>
141
+ Effect.failCause(
142
+ Cause.map(cause, (error): NormalizedD1Error<E> =>
143
+ isD1DomainError(error) ? (error as Extract<E, D1DomainError>) : unavailable(),
144
+ ),
145
+ ),
146
+ ),
147
+ );
148
+
149
+ const terminalD1 = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
150
+ normalizeD1Failure(effect, isD1DomainError);
151
+
152
+ const containsFailure = (failure: unknown, predicate: (value: unknown) => boolean): boolean => {
153
+ const seen = new Set<unknown>();
154
+ const pending: Array<unknown> = [failure];
155
+
156
+ for (let inspected = 0; inspected < 24 && pending.length > 0; inspected++) {
157
+ const current = pending.shift();
158
+
159
+ if (current === undefined || current === null || seen.has(current)) continue;
160
+ seen.add(current);
161
+ if (predicate(current)) return true;
162
+ if (Cause.isCause(current)) {
163
+ for (const reason of current.reasons) {
164
+ if (Cause.isFailReason(reason)) pending.push(reason.error);
165
+ else if (Cause.isDieReason(reason)) pending.push(reason.defect);
166
+ }
167
+ continue;
168
+ }
169
+ if (typeof current !== "object") continue;
170
+
171
+ const wrapped = current as {
172
+ readonly cause?: unknown;
173
+ readonly reason?: unknown;
174
+ readonly message?: unknown;
175
+ };
176
+
177
+ if (wrapped.cause !== undefined) pending.push(wrapped.cause);
178
+ if (wrapped.reason !== undefined) pending.push(wrapped.reason);
179
+ if (wrapped.message !== undefined) pending.push(wrapped.message);
180
+ }
181
+
182
+ return false;
183
+ };
184
+
185
+ const isGuardFailure = (failure: unknown): boolean =>
186
+ containsFailure(
187
+ failure,
188
+ (value) => typeof value === "string" && value.includes("effect-auth-session-guard"),
189
+ );
190
+
191
+ const recoverBatchFailure = (
192
+ failure: D1BatchFailure,
193
+ ): Effect.Effect<never, D1PlanError | SqlError.SqlError, CurrentD1PlanningDatabase> => {
194
+ const { cause, mutation } = failure;
195
+ const onlyFailures = cause.reasons.length > 0 && cause.reasons.every(Cause.isFailReason);
196
+
197
+ if (mutation === undefined) return Effect.failCause(cause);
198
+ if (
199
+ onlyFailures &&
200
+ mutation.isConstraintConflict !== undefined &&
201
+ cause.reasons.every(
202
+ (reason) =>
203
+ Cause.isFailReason(reason) &&
204
+ isMappedConstraintConflict(mutation.isConstraintConflict!, reason.error),
205
+ )
206
+ )
207
+ return Effect.fail(SessionConflict.make({}));
208
+ if (
209
+ onlyFailures &&
210
+ mutation.recoverGuard !== undefined &&
211
+ cause.reasons.every((reason) => Cause.isFailReason(reason) && isGuardFailure(reason.error))
212
+ )
213
+ return mutation.recoverGuard;
214
+
215
+ return Effect.failCause(cause);
216
+ };
217
+
218
+ const splitD1BatchFailure = <E>(cause: Cause.Cause<E | D1BatchFailure>) => {
219
+ const marker = cause.reasons.find(
220
+ (reason) => Cause.isFailReason(reason) && reason.error instanceof D1BatchFailure,
221
+ );
222
+
223
+ if (marker === undefined || !Cause.isFailReason(marker)) return undefined;
224
+ const remaining = Cause.fromReasons(cause.reasons.filter((reason) => reason !== marker));
225
+
226
+ return {
227
+ failure: marker.error as D1BatchFailure,
228
+ // The private marker is removed by identity; every retained reason belongs to E.
229
+ remaining: remaining as Cause.Cause<E>,
230
+ };
231
+ };
232
+
233
+ const recoverD1BatchCause = <E>(
234
+ cause: Cause.Cause<E | D1BatchFailure>,
235
+ ): Effect.Effect<never, E | D1PlanError | SqlError.SqlError, CurrentD1PlanningDatabase> => {
236
+ const split = splitD1BatchFailure(cause);
237
+
238
+ if (split === undefined) {
239
+ // No private marker remains, so this is the original E Cause.
240
+ return Effect.failCause(cause as Cause.Cause<E>);
241
+ }
242
+
243
+ return recoverBatchFailure(split.failure).pipe(
244
+ Effect.catchCause((recovered) => Effect.failCause(Cause.combine(recovered, split.remaining))),
245
+ );
246
+ };
247
+
248
+ const recoverCoordinatedD1BatchCause = <E>(
249
+ cause: Cause.Cause<E | D1BatchFailure>,
250
+ ): Effect.Effect<never, E | D1DomainError, CurrentD1PlanningDatabase> => {
251
+ const split = splitD1BatchFailure(cause);
252
+
253
+ if (split === undefined) {
254
+ // No private marker remains, so this is the original E Cause.
255
+ return Effect.failCause(cause as Cause.Cause<E>);
256
+ }
257
+
258
+ return terminalD1(recoverBatchFailure(split.failure)).pipe(
259
+ Effect.catchCause((recovered) => Effect.failCause(Cause.combine(recovered, split.remaining))),
260
+ );
261
+ };
262
+
263
+ const statement = (query: { readonly toSQL: () => { sql: string; params: unknown[] } }) =>
264
+ Effect.map(CurrentD1PlanningDatabase, (database) => {
265
+ const rendered = query.toSQL();
266
+
267
+ return compactD1GeneratedStatement(
268
+ database.$client,
269
+ database.$client.unsafe(rendered.sql, rendered.params),
270
+ unavailable,
271
+ );
272
+ });
273
+
274
+ const assertion = (
275
+ condition: SQL,
276
+ ): Effect.Effect<Statement<any>, never, CurrentD1PlanningDatabase> =>
277
+ Effect.gen(function* () {
278
+ const database = yield* CurrentD1PlanningDatabase;
279
+
280
+ return yield* statement(
281
+ database
282
+ .select({
283
+ ok: sql`case when ${condition} then 1 else json_extract('[]', '$[effect-auth-session-guard]') end`.as(
284
+ "ok",
285
+ ),
286
+ })
287
+ .from(sql`(select 1)`),
288
+ );
289
+ });
290
+
291
+ const conditionMatches = (condition: SQL) =>
292
+ Effect.flatMap(CurrentD1PlanningDatabase, (database) =>
293
+ database
294
+ .select({ ok: sql<number>`1` })
295
+ .from(sql`(select 1)`)
296
+ .where(condition)
297
+ .limit(1)
298
+ .pipe(Effect.map((rows) => rows.length > 0)),
299
+ );
300
+
301
+ function runPlanned<A, E>(
302
+ plan: Effect.Effect<Planned<A>, E, CurrentD1PlanningDatabase | CurrentCommitJournal>,
303
+ ): Effect.Effect<
304
+ A,
305
+ NormalizedD1Error<E | HookConfigurationError | SqlError.SqlError>,
306
+ CurrentD1PlanningDatabase | LifecycleHooks
307
+ >;
308
+ function runPlanned<A, E>(
309
+ plan: Effect.Effect<Planned<A>, E, CurrentD1PlanningDatabase | CurrentCommitJournal>,
310
+ isConstraintConflict: (cause: unknown) => boolean,
311
+ ): Effect.Effect<
312
+ A,
313
+ NormalizedD1Error<E | SessionConflict | HookConfigurationError | SqlError.SqlError>,
314
+ CurrentD1PlanningDatabase | LifecycleHooks
315
+ >;
316
+ function runPlanned<A, E, GuardE extends D1PlanError>(
317
+ plan: Effect.Effect<Planned<A>, E, CurrentD1PlanningDatabase | CurrentCommitJournal>,
318
+ isConstraintConflict: undefined,
319
+ recoverGuard: Effect.Effect<never, GuardE, CurrentD1PlanningDatabase>,
320
+ ): Effect.Effect<
321
+ A,
322
+ NormalizedD1Error<E | GuardE | HookConfigurationError | SqlError.SqlError>,
323
+ CurrentD1PlanningDatabase | LifecycleHooks
324
+ >;
325
+ function runPlanned<A, E, GuardE extends D1PlanError>(
326
+ plan: Effect.Effect<Planned<A>, E, CurrentD1PlanningDatabase | CurrentCommitJournal>,
327
+ isConstraintConflict: (cause: unknown) => boolean,
328
+ recoverGuard: Effect.Effect<never, GuardE, CurrentD1PlanningDatabase>,
329
+ ): Effect.Effect<
330
+ A,
331
+ NormalizedD1Error<E | GuardE | SessionConflict | HookConfigurationError | SqlError.SqlError>,
332
+ CurrentD1PlanningDatabase | LifecycleHooks
333
+ >;
334
+ function runPlanned<A, E, GuardE extends D1PlanError = never>(
335
+ plan: Effect.Effect<Planned<A>, E, CurrentD1PlanningDatabase | CurrentCommitJournal>,
336
+ isConstraintConflict?: (cause: unknown) => boolean,
337
+ recoverGuard?: Effect.Effect<never, GuardE, CurrentD1PlanningDatabase>,
338
+ ) {
339
+ const planned = plan.pipe(
340
+ Effect.map((value) => ({ ...value, isConstraintConflict, recoverGuard })),
341
+ );
342
+
343
+ const execution = Effect.gen(function* () {
344
+ const batch = yield* Effect.serviceOption(CurrentD1SessionBatch);
345
+
346
+ if (batch._tag === "Some") {
347
+ const owned = batch.value;
348
+
349
+ const result = yield* coordinateCommit(
350
+ () =>
351
+ Effect.gen(function* () {
352
+ yield* owned.owner.check;
353
+ const journal = yield* CurrentCommitJournal;
354
+ const value = yield* planned;
355
+
356
+ yield* owned.owner.check;
357
+ const guarded = { ...value, journalGuard: journal.prepare(undefined) };
358
+
359
+ yield* owned.append(guarded);
360
+
361
+ return guarded.receipt;
362
+ }),
363
+ { mode: "batch" },
364
+ );
365
+
366
+ return result.value;
367
+ }
368
+
369
+ const database = yield* CurrentD1PlanningDatabase;
370
+
371
+ const result = yield* coordinateCommit(
372
+ () =>
373
+ Effect.gen(function* () {
374
+ const prepared = yield* planned;
375
+
376
+ yield* database.$client
377
+ .batch(prepared.statements)
378
+ .pipe(
379
+ Effect.catchCause((cause) =>
380
+ Effect.fail(new D1BatchFailure({ cause, mutation: prepared })),
381
+ ),
382
+ );
383
+
384
+ return prepared.receipt;
385
+ }),
386
+ { mode: "batch" },
387
+ );
388
+
389
+ return result.value;
390
+ });
391
+
392
+ return normalizeD1Failure(
393
+ execution.pipe(Effect.catchCause(recoverD1BatchCause)),
394
+ isD1DomainError,
395
+ );
396
+ }
397
+
398
+ const subjectColumns = (mapping: any) => ({
399
+ id: column(mapping.subject.table, mapping.subject.id),
400
+ status: column(mapping.subject.table, mapping.subject.status),
401
+ revision: column(mapping.subject.table, mapping.subject.securityRevision),
402
+ });
403
+
404
+ const credentialColumns = (mapping: any) => ({
405
+ subjectId: column(mapping.credential.table, mapping.credential.subjectId),
406
+ id: column(mapping.credential.table, mapping.credential.credentialId),
407
+ revision: column(mapping.credential.table, mapping.credential.revision),
408
+ ...(mapping.credential.status === undefined
409
+ ? {}
410
+ : { status: column(mapping.credential.table, mapping.credential.status) }),
411
+ });
412
+
413
+ const flowColumns = (mapping: any) => ({
414
+ flowId: column(mapping.flow.table, mapping.flow.flowId),
415
+ subjectId: column(mapping.flow.table, mapping.flow.subjectId),
416
+ state: column(mapping.flow.table, mapping.flow.state),
417
+ pendingDigest: column(mapping.flow.table, mapping.flow.pendingDigest),
418
+ dedupUntil: column(mapping.flow.table, mapping.flow.dedupUntil),
419
+ });
420
+
421
+ const hasActiveFlow = Effect.fn("D1Session.hasActiveFlow")(function* (
422
+ mapping: any,
423
+ flowId: string,
424
+ ) {
425
+ const database = yield* CurrentD1PlanningDatabase;
426
+ const f = flowColumns(mapping);
427
+
428
+ return yield* database
429
+ .select({ flowId: f.flowId })
430
+ .from(mapping.flow.table)
431
+ .where(and(eq(f.flowId, flowId), gt(f.dedupUntil, mapping.d1.engineNow)))
432
+ .limit(1)
433
+ .pipe(Effect.map((rows) => rows.length > 0));
434
+ });
435
+
436
+ const recoverFlowOrStale = (
437
+ mapping: any,
438
+ flowId: string,
439
+ ): Effect.Effect<
440
+ never,
441
+ SessionConflict | StaleAuthentication | D1AdapterFailure,
442
+ CurrentD1PlanningDatabase
443
+ > =>
444
+ hasActiveFlow(mapping, flowId).pipe(
445
+ Effect.flatMap((active) =>
446
+ Effect.fail(active ? SessionConflict.make({}) : StaleAuthentication.make({})),
447
+ ),
448
+ );
449
+
450
+ const pendingColumns = (mapping: any) => ({
451
+ digest: column(mapping.pending.table, mapping.pending.digest),
452
+ version: column(mapping.pending.table, mapping.pending.version),
453
+ flowId: column(mapping.pending.table, mapping.pending.flowId),
454
+ subjectId: column(mapping.pending.table, mapping.pending.subjectId),
455
+ bindingDigest: column(mapping.pending.table, mapping.pending.bindingDigest),
456
+ expiresAt: column(mapping.pending.table, mapping.pending.expiresAt),
457
+ attemptLimit: column(mapping.pending.table, mapping.pending.attemptLimit),
458
+ failedAttempts: column(mapping.pending.table, mapping.pending.failedAttempts),
459
+ consumed: column(mapping.pending.table, mapping.pending.consumed),
460
+ });
461
+
462
+ const sessionColumns = (mapping: any) => ({
463
+ sessionId: column(mapping.session.table, mapping.session.sessionId),
464
+ subjectId: column(mapping.session.table, mapping.session.subjectId),
465
+ digest: column(mapping.session.table, mapping.session.digest),
466
+ version: column(mapping.session.table, mapping.session.version),
467
+ securityRevision: column(mapping.session.table, mapping.session.securityRevision),
468
+ issuedAt: column(mapping.session.table, mapping.session.issuedAt),
469
+ expiresAt: column(mapping.session.table, mapping.session.expiresAt),
470
+ absoluteExpiresAt: column(mapping.session.table, mapping.session.absoluteExpiresAt),
471
+ });
472
+
473
+ const captureDetails = Effect.fn("DrizzleD1Session.captureDetails")(function* (
474
+ mapping: any,
475
+ subjectId: SubjectId,
476
+ credentialIds: ReadonlyArray<string>,
477
+ ): Effect.fn.Return<
478
+ { readonly subject: any; readonly revision: AuthenticationRevision },
479
+ StaleAuthentication | D1AdapterFailure,
480
+ CurrentD1PlanningDatabase
481
+ > {
482
+ const database = yield* CurrentD1PlanningDatabase;
483
+ const requested = [...new Set(credentialIds)].sort();
484
+
485
+ if (requested.length !== credentialIds.length) return yield* stale();
486
+ const nativeSubjectId = yield* mapping.subjectId.toNative(subjectId);
487
+ const sc = subjectColumns(mapping);
488
+ const cc = credentialColumns(mapping);
489
+
490
+ const rows: any[] = yield* requested.length === 0
491
+ ? database
492
+ .select({ subject: getTableColumns(mapping.subject.table) })
493
+ .from(mapping.subject.table)
494
+ .where(eq(sc.id, nativeSubjectId))
495
+ .limit(1)
496
+ .pipe(
497
+ Effect.map((selected: any[]) =>
498
+ selected.map((row: any) => ({ subject: row.subject, credential: undefined })),
499
+ ),
500
+ )
501
+ : database
502
+ .select({
503
+ subject: getTableColumns(mapping.subject.table),
504
+ credential: getTableColumns(mapping.credential.table),
505
+ })
506
+ .from(mapping.subject.table)
507
+ .innerJoin(
508
+ mapping.credential.table,
509
+ and(eq(cc.subjectId, sc.id), inArray(cc.id, requested)),
510
+ )
511
+ .where(eq(sc.id, nativeSubjectId));
512
+
513
+ const subject = rows[0]?.subject;
514
+
515
+ if (subject === undefined || !mapping.subject.isActiveStatus(subject[mapping.subject.status]))
516
+ return yield* stale();
517
+
518
+ const credentials = rows.flatMap((row: any) =>
519
+ row.credential === undefined || row.credential === null ? [] : [row.credential],
520
+ );
521
+
522
+ if (
523
+ credentials.length !== requested.length ||
524
+ credentials.some(
525
+ (row: any) =>
526
+ mapping.credential.status !== undefined &&
527
+ mapping.credential.isActiveStatus?.(row[mapping.credential.status]) !== true,
528
+ )
529
+ )
530
+ return yield* stale();
531
+
532
+ return {
533
+ subject,
534
+ revision: {
535
+ subjectId,
536
+ securityRevision: subject[mapping.subject.securityRevision],
537
+ credentials: credentials
538
+ .map((row: any) => ({
539
+ credentialId: row[mapping.credential.credentialId],
540
+ revision: row[mapping.credential.revision],
541
+ }))
542
+ .sort((a: any, b: any) => a.credentialId.localeCompare(b.credentialId)),
543
+ } satisfies AuthenticationRevision,
544
+ };
545
+ });
546
+
547
+ const capture = Effect.fn("DrizzleD1Session.capture")(function* (
548
+ mapping: any,
549
+ subjectId: SubjectId,
550
+ credentialIds: ReadonlyArray<string>,
551
+ ): Effect.fn.Return<
552
+ AuthenticationRevision,
553
+ StaleAuthentication | D1AdapterFailure,
554
+ CurrentD1PlanningDatabase
555
+ > {
556
+ const details = yield* captureDetails(mapping, subjectId, credentialIds);
557
+
558
+ return details.revision;
559
+ });
560
+
561
+ const requirement = Effect.fn("DrizzleD1Session.requirement")(function* (
562
+ mapping: any,
563
+ evidence: AuthenticationEvidence,
564
+ ): Effect.fn.Return<
565
+ AuthenticationRequirement,
566
+ StaleAuthentication | D1AdapterFailure,
567
+ CurrentD1PlanningDatabase
568
+ > {
569
+ const current = yield* captureDetails(
570
+ mapping,
571
+ evidence.revision.subjectId,
572
+ evidence.revision.credentials.map((item) => item.credentialId),
573
+ );
574
+
575
+ const expected = new Map(
576
+ evidence.revision.credentials.map((item) => [item.credentialId, item.revision]),
577
+ );
578
+
579
+ if (
580
+ current.revision.securityRevision !== evidence.revision.securityRevision ||
581
+ current.revision.credentials.some((item) => expected.get(item.credentialId) !== item.revision)
582
+ )
583
+ return yield* stale();
584
+
585
+ return yield* mapping.subject.decodeRequirement(current.subject);
586
+ });
587
+
588
+ const authorityCondition = (
589
+ mapping: any,
590
+ nativeSubjectId: unknown,
591
+ evidence: Pick<AuthenticationEvidence, "revision">,
592
+ ): SQL => {
593
+ const sc = subjectColumns(mapping);
594
+ const cc = credentialColumns(mapping);
595
+ const subject = sql`exists(select 1 from ${mapping.subject.table} where ${sc.id} = ${sql.param(nativeSubjectId, sc.id)} and ${sc.status} = ${sql.param(mapping.d1.activeSubjectStatusValue, sc.status)} and ${sc.revision} = ${sql.param(evidence.revision.securityRevision, sc.revision)})`;
596
+
597
+ const credentials = evidence.revision.credentials.map(
598
+ (item) =>
599
+ sql`exists(select 1 from ${mapping.credential.table} where ${cc.subjectId} = ${sql.param(nativeSubjectId, cc.subjectId)} and ${cc.id} = ${sql.param(item.credentialId, cc.id)} and ${cc.revision} = ${sql.param(item.revision, cc.revision)}${
600
+ mapping.credential.status === undefined
601
+ ? sql``
602
+ : sql` and ${cc.status} = ${sql.param(mapping.d1.activeCredentialStatusValue, cc.status)}`
603
+ })`,
604
+ );
605
+
606
+ return and(subject, ...credentials)!;
607
+ };
608
+
609
+ const subjectRevisionCondition = (
610
+ mapping: any,
611
+ nativeSubjectId: unknown,
612
+ revision: SecurityRevision,
613
+ ) => {
614
+ const sc = subjectColumns(mapping);
615
+
616
+ return sql`exists(select 1 from ${mapping.subject.table} where ${sc.id} = ${sql.param(nativeSubjectId, sc.id)} and ${sc.status} = ${sql.param(mapping.d1.activeSubjectStatusValue, sc.status)} and ${sc.revision} = ${sql.param(revision, sc.revision)})`;
617
+ };
618
+
619
+ /** Engine-time counterpart of assessAuthentication. Complete transitions must
620
+ * satisfy an entire alternative; incomplete pending flows need one eligible proof. */
621
+ const freshnessCondition = (
622
+ mapping: any,
623
+ evidence: AuthenticationEvidence,
624
+ requirement: AuthenticationRequirement,
625
+ complete: boolean,
626
+ ): SQL => {
627
+ const fresh = evidence.proofs.map((proof) => {
628
+ const verified = DateTime.toEpochMillis(proof.verifiedAt);
629
+
630
+ return and(
631
+ sql`${mapping.d1.engineNowMillis} >= ${verified}`,
632
+ sql`${mapping.d1.engineNowMillis} - ${verified} < ${requirement.maximumAgeMillis}`,
633
+ )!;
634
+ });
635
+
636
+ const any = (conditions: ReadonlyArray<SQL>): SQL =>
637
+ conditions.length === 0 ? sql`false` : or(...conditions)!;
638
+
639
+ const notFuture = evidence.proofs.map(
640
+ (proof) => sql`${mapping.d1.engineNowMillis} >= ${DateTime.toEpochMillis(proof.verifiedAt)}`,
641
+ );
642
+
643
+ if (!complete) return and(...notFuture, any(fresh))!;
644
+ const byCredential = new Map<string, SQL[]>();
645
+
646
+ evidence.proofs.forEach((proof, index) => {
647
+ const previous = byCredential.get(proof.credentialId) ?? [];
648
+
649
+ previous.push(fresh[index]);
650
+ byCredential.set(proof.credentialId, previous);
651
+ });
652
+
653
+ const credentialCount = sql.join(
654
+ [...byCredential.values()].map((proofs) => sql`case when ${any(proofs)} then 1 else 0 end`),
655
+ sql` + `,
656
+ );
657
+
658
+ const alternatives = requirement.alternatives.map((alternative) =>
659
+ and(
660
+ ...alternative.factors.map((factor) =>
661
+ any(fresh.filter((_, index) => evidence.proofs[index].factors.includes(factor))),
662
+ ),
663
+ sql`(${credentialCount}) >= ${alternative.minimumCredentials}`,
664
+ any(
665
+ fresh.filter(
666
+ (_, index) =>
667
+ (!alternative.userVerified || evidence.proofs[index].userVerified) &&
668
+ (!alternative.phishingResistant || evidence.proofs[index].phishingResistant),
669
+ ),
670
+ ),
671
+ )!,
672
+ );
673
+
674
+ return and(...notFuture, any(alternatives))!;
675
+ };
676
+
677
+ const allocate = <A>(
678
+ asynchronous: Effect.Effect<A, PersistenceMappingError> | undefined,
679
+ synchronous: (() => A) | undefined,
680
+ ): Effect.Effect<A, D1AdapterFailure | SessionUnavailable> =>
681
+ asynchronous !== undefined
682
+ ? asynchronous
683
+ : synchronous === undefined
684
+ ? Effect.fail(unavailable())
685
+ : Effect.try({
686
+ try: synchronous,
687
+ catch: (cause) => PersistenceMappingError.make({ operation: "allocate", cause }),
688
+ });
689
+
690
+ const sameOriginalRevision = (
691
+ completed: AuthenticationEvidence,
692
+ original: AuthenticationEvidence,
693
+ ) => {
694
+ const current = new Map(
695
+ completed.revision.credentials.map((item) => [item.credentialId, item.revision]),
696
+ );
697
+
698
+ return (
699
+ completed.flowId === original.flowId &&
700
+ completed.bindingDigest === original.bindingDigest &&
701
+ completed.revision.subjectId === original.revision.subjectId &&
702
+ completed.revision.securityRevision === original.revision.securityRevision &&
703
+ original.revision.credentials.every(
704
+ (item) => current.get(item.credentialId) === item.revision,
705
+ ) &&
706
+ original.proofs.every((expected) =>
707
+ completed.proofs.some(
708
+ (proof) =>
709
+ proof.method === expected.method &&
710
+ proof.credentialId === expected.credentialId &&
711
+ proof.userVerified === expected.userVerified &&
712
+ proof.phishingResistant === expected.phishingResistant &&
713
+ DateTime.toEpochMillis(proof.verifiedAt) ===
714
+ DateTime.toEpochMillis(expected.verifiedAt) &&
715
+ proof.factors.length === expected.factors.length &&
716
+ proof.factors.every((factor, index) => factor === expected.factors[index]),
717
+ ),
718
+ )
719
+ );
720
+ };
721
+
722
+ type PendingProjection = "record" | "context";
723
+
724
+ const readPendingWith = Effect.fn("D1Session.readPendingWith")(function* <
725
+ Record extends PendingAuthenticationState,
726
+ >(
727
+ mapping: any,
728
+ digest: TokenDigest,
729
+ projection: PendingProjection,
730
+ ): Effect.fn.Return<Record | undefined, D1AdapterFailure, CurrentD1PlanningDatabase> {
731
+ const database = yield* CurrentD1PlanningDatabase;
732
+
733
+ const row = (yield* database
734
+ .select()
735
+ .from(mapping.pending.table)
736
+ .where(eq(pendingColumns(mapping).digest, digest))
737
+ .limit(1))[0];
738
+
739
+ if (row === undefined) return undefined;
740
+
741
+ const decode: (row: any) => Effect.Effect<Record, PersistenceMappingError> =
742
+ projection === "context" ? mapping.pending.decodeContext : mapping.pending.decode;
743
+
744
+ return yield* decode(row);
745
+ });
746
+
747
+ const readPending = <Claims>(mapping: any, digest: TokenDigest) =>
748
+ readPendingWith<PendingAuthenticationRecord<Claims>>(mapping, digest, "record");
749
+
750
+ const pendingCondition = (
751
+ mapping: any,
752
+ input: PendingConsumption,
753
+ nativeSubjectId: unknown,
754
+ ): SQL => {
755
+ const p = pendingColumns(mapping);
756
+ const f = flowColumns(mapping);
757
+
758
+ return and(
759
+ sql`exists(select 1 from ${mapping.pending.table} where ${p.digest} = ${sql.param(input.digest, p.digest)} and ${p.version} = ${sql.param(input.version, p.version)} and ${p.flowId} = ${sql.param(input.flowId, p.flowId)} and ${p.subjectId} = ${sql.param(nativeSubjectId, p.subjectId)} and ${p.bindingDigest} = ${sql.param(input.bindingDigest, p.bindingDigest)} and ${p.consumed} = ${sql.param(false, p.consumed)} and ${p.failedAttempts} < ${p.attemptLimit} and ${p.expiresAt} > ${mapping.d1.engineNow})`,
760
+ sql`exists(select 1 from ${mapping.flow.table} where ${f.flowId} = ${sql.param(input.flowId, f.flowId)} and ${f.subjectId} = ${sql.param(nativeSubjectId, f.subjectId)} and ${f.state} = ${sql.param(mapping.flow.pendingStateValue, f.state)} and ${f.pendingDigest} = ${sql.param(input.digest, f.pendingDigest)} and ${f.dedupUntil} > ${mapping.d1.engineNow})`,
761
+ )!;
762
+ };
763
+
764
+ const recoverPendingOrStale = (
765
+ mapping: any,
766
+ input: PendingConsumption,
767
+ subjectId: SubjectId,
768
+ ): Effect.Effect<
769
+ never,
770
+ PendingAuthenticationInvalid | StaleAuthentication | D1AdapterFailure,
771
+ CurrentD1PlanningDatabase
772
+ > =>
773
+ mapping.subjectId.toNative(subjectId).pipe(
774
+ Effect.flatMap((nativeSubjectId: unknown) =>
775
+ conditionMatches(pendingCondition(mapping, input, nativeSubjectId)),
776
+ ),
777
+ Effect.flatMap((pendingIsCurrent) =>
778
+ Effect.fail(
779
+ pendingIsCurrent ? StaleAuthentication.make({}) : PendingAuthenticationInvalid.make({}),
780
+ ),
781
+ ),
782
+ );
783
+
784
+ const consumePendingStatements = Effect.fn("D1Session.consumePendingStatements")(function* (
785
+ mapping: any,
786
+ input: PendingConsumption,
787
+ absoluteExpiresAt: DateTime.Utc,
788
+ ): Effect.fn.Return<ReadonlyArray<Statement<any>>, never, CurrentD1PlanningDatabase> {
789
+ const database = yield* CurrentD1PlanningDatabase;
790
+
791
+ return [
792
+ yield* statement(
793
+ database
794
+ .update(mapping.pending.table)
795
+ .set(updateValues<any>([[mapping.pending.consumed, true]]))
796
+ .where(
797
+ and(
798
+ eq(pendingColumns(mapping).digest, input.digest),
799
+ eq(pendingColumns(mapping).version, input.version),
800
+ ),
801
+ ),
802
+ ),
803
+ yield* statement(
804
+ database
805
+ .update(mapping.flow.table)
806
+ .set(
807
+ updateValues<any>([
808
+ [mapping.flow.state, mapping.flow.establishedStateValue],
809
+ [mapping.flow.pendingDigest, null],
810
+ [mapping.flow.dedupUntil, mapping.flow.encodeInstant(absoluteExpiresAt)],
811
+ ]),
812
+ )
813
+ .where(eq(flowColumns(mapping).flowId, input.flowId)),
814
+ ),
815
+ ];
816
+ });
817
+
818
+ export const makeD1AuthenticationAuthority = <Claims>(
819
+ mapping: D1AuthenticationAuthorityMapping<Claims, any, any, any, any, any>,
820
+ ) => ({
821
+ capture: (
822
+ subjectId: Parameters<AuthenticationAuthority["Service"]["capture"]>[0],
823
+ credentialIds: Parameters<AuthenticationAuthority["Service"]["capture"]>[1],
824
+ ) => capture(mapping, subjectId, credentialIds).pipe(terminalD1),
825
+ requirements: (evidence: AuthenticationEvidence) =>
826
+ requirement(mapping, evidence).pipe(terminalD1),
827
+ approve: <A>(
828
+ input: Parameters<AuthenticationAuthority["Service"]["approve"]>[0],
829
+ prepare: PrepareSessionCommit<void, A>,
830
+ ) =>
831
+ runPlanned(
832
+ Effect.gen(function* () {
833
+ const journal = yield* CurrentCommitJournal;
834
+ const policy = yield* requirement(mapping, input.evidence);
835
+ const assessed = yield* assessAuthentication(input.evidence, policy);
836
+
837
+ if (!assessed.satisfied) return yield* stale();
838
+
839
+ const nativeSubjectId = yield* mapping.subjectId.toNative(
840
+ input.evidence.revision.subjectId,
841
+ );
842
+
843
+ let condition = and(
844
+ authorityCondition(mapping, nativeSubjectId, input.evidence),
845
+ freshnessCondition(mapping, input.evidence, policy, true),
846
+ sql`${mapping.d1.engineNowMillis} < ${DateTime.toEpochMillis(input.expiresAt)}`,
847
+ sql`${DateTime.toEpochMillis(input.expiresAt)} <= ${DateTime.toEpochMillis(input.absoluteExpiresAt)}`,
848
+ )!;
849
+
850
+ const statements: Statement<any>[] = [];
851
+
852
+ if (input.pending !== undefined) {
853
+ if (mapping.pending === undefined) return yield* invalidPending();
854
+ const pendingMapping = { ...mapping, ...mapping.pending };
855
+ const stored = yield* readPending(pendingMapping, input.pending.digest);
856
+
857
+ if (stored === undefined || !sameOriginalRevision(input.evidence, stored.evidence))
858
+ return yield* invalidPending();
859
+ condition = and(
860
+ condition,
861
+ pendingCondition(pendingMapping, input.pending, nativeSubjectId),
862
+ )!;
863
+ statements.push(
864
+ ...(yield* consumePendingStatements(
865
+ pendingMapping,
866
+ input.pending,
867
+ input.absoluteExpiresAt,
868
+ )),
869
+ );
870
+ }
871
+ const receipt = prepare(undefined, journal);
872
+
873
+ return { receipt, statements: [yield* assertion(condition), ...statements] };
874
+ }),
875
+ undefined,
876
+ Effect.suspend(() =>
877
+ input.pending === undefined || mapping.pending === undefined
878
+ ? Effect.fail(stale())
879
+ : recoverPendingOrStale(
880
+ { ...mapping, ...mapping.pending },
881
+ input.pending,
882
+ input.evidence.revision.subjectId,
883
+ ),
884
+ ),
885
+ ),
886
+ });
887
+
888
+ /** D1's last authoritative statement checks the original record version and
889
+ * security state together. A later race is still rejected by final consumption. */
890
+ const readAuthenticatedPending = Effect.fn("D1Session.readAuthenticatedPending")(function* <
891
+ Record extends PendingAuthenticationState,
892
+ >(
893
+ mapping: any,
894
+ input: { readonly digest: TokenDigest; readonly bindingDigest?: TokenDigest },
895
+ projection: PendingProjection,
896
+ ): Effect.fn.Return<
897
+ Record,
898
+ PendingAuthenticationInvalid | D1AdapterFailure,
899
+ CurrentD1PlanningDatabase
900
+ > {
901
+ const record = yield* readPendingWith<Record>(mapping, input.digest, projection);
902
+
903
+ if (
904
+ record === undefined ||
905
+ (input.bindingDigest !== undefined && record.evidence.bindingDigest !== input.bindingDigest)
906
+ )
907
+ return yield* invalidPending();
908
+
909
+ const policy = yield* mapFailureCause(requirement(mapping, record.evidence), (error) =>
910
+ error._tag === "StaleAuthentication" ? invalidPending() : error,
911
+ );
912
+
913
+ yield* assessAuthentication(record.evidence, policy).pipe(
914
+ Effect.mapError(() => invalidPending()),
915
+ );
916
+ const nativeSubjectId = yield* mapping.subjectId.toNative(record.evidence.revision.subjectId);
917
+
918
+ const current = yield* conditionMatches(
919
+ and(
920
+ authorityCondition(mapping, nativeSubjectId, record.evidence),
921
+ freshnessCondition(mapping, record.evidence, policy, false),
922
+ pendingCondition(
923
+ mapping,
924
+ {
925
+ digest: input.digest,
926
+ bindingDigest: record.evidence.bindingDigest,
927
+ flowId: record.evidence.flowId,
928
+ version: record.version,
929
+ },
930
+ nativeSubjectId,
931
+ ),
932
+ )!,
933
+ );
934
+
935
+ if (!current) return yield* invalidPending();
936
+
937
+ return record;
938
+ });
939
+
940
+ export const makeD1PendingAuthentication = <Claims>(
941
+ mapping: D1PendingAuthenticationMapping<Claims, any, any, any, any, any>,
942
+ ) => ({
943
+ create: <A>(
944
+ input: Parameters<PendingAuthentication<Claims>["create"]>[0],
945
+ _now: DateTime.Utc,
946
+ prepare: PrepareSessionCommit<PendingAuthenticationRecord<Claims>, A>,
947
+ ) =>
948
+ runPlanned(
949
+ Effect.gen(function* () {
950
+ const database = yield* CurrentD1PlanningDatabase;
951
+ const journal = yield* CurrentCommitJournal;
952
+ const policy = yield* requirement(mapping, input.evidence);
953
+
954
+ yield* assessAuthentication(input.evidence, policy);
955
+
956
+ const nativeSubjectId = yield* mapping.subjectId.toNative(
957
+ input.evidence.revision.subjectId,
958
+ );
959
+
960
+ if (yield* hasActiveFlow(mapping, input.evidence.flowId))
961
+ return yield* SessionConflict.make({});
962
+
963
+ const version = yield* allocate(
964
+ mapping.pending.allocateVersion,
965
+ mapping.pending.allocateVersionSync,
966
+ );
967
+
968
+ const record = { ...input, version };
969
+ const f = flowColumns(mapping);
970
+
971
+ const condition = and(
972
+ authorityCondition(mapping, nativeSubjectId, input.evidence),
973
+ freshnessCondition(mapping, input.evidence, policy, false),
974
+ sql`${mapping.d1.engineNowMillis} < ${DateTime.toEpochMillis(input.expiresAt)}`,
975
+ not(
976
+ sql`exists(select 1 from ${mapping.flow.table} where ${f.flowId} = ${sql.param(input.evidence.flowId, f.flowId)} and ${f.dedupUntil} > ${mapping.d1.engineNow})`,
977
+ ),
978
+ )!;
979
+
980
+ const cleanupPending = database
981
+ .delete(mapping.pending.table)
982
+ .where(
983
+ sql`${pendingColumns(mapping).flowId} in (select ${f.flowId} from ${mapping.flow.table} where ${f.flowId} = ${sql.param(input.evidence.flowId, f.flowId)} and ${f.dedupUntil} <= ${mapping.d1.engineNow})`,
984
+ );
985
+
986
+ const cleanupFlow = database
987
+ .delete(mapping.flow.table)
988
+ .where(
989
+ and(
990
+ eq(f.flowId, input.evidence.flowId),
991
+ sql`${f.dedupUntil} <= ${mapping.d1.engineNow}`,
992
+ ),
993
+ );
994
+
995
+ const insertFlow = database.insert(mapping.flow.table).values(
996
+ mapping.flow.encodePendingInsert({
997
+ evidence: input.evidence,
998
+ subjectId: nativeSubjectId,
999
+ pendingDigest: input.digest,
1000
+ dedupUntil: input.expiresAt,
1001
+ }),
1002
+ );
1003
+
1004
+ const insertPending = database.insert(mapping.pending.table).values(
1005
+ mapping.pending.encodeInsert(record, {
1006
+ subjectId: nativeSubjectId,
1007
+ failedAttempts: 0,
1008
+ consumed: false,
1009
+ }),
1010
+ );
1011
+
1012
+ const receipt = prepare(record, journal);
1013
+
1014
+ return {
1015
+ receipt,
1016
+ statements: [
1017
+ yield* assertion(condition),
1018
+ yield* statement(cleanupPending),
1019
+ yield* statement(cleanupFlow),
1020
+ yield* statement(insertFlow),
1021
+ yield* statement(insertPending),
1022
+ ],
1023
+ };
1024
+ }),
1025
+ mapping.isConstraintConflict,
1026
+ recoverFlowOrStale(mapping, input.evidence.flowId),
1027
+ ),
1028
+ context: (input: Parameters<PendingAuthentication<Claims>["context"]>[0]) =>
1029
+ readAuthenticatedPending<PendingAuthenticationState>(mapping, input, "context").pipe(
1030
+ Effect.flatMap(pendingAuthenticationContext),
1031
+ terminalD1,
1032
+ ),
1033
+ read: (input: Parameters<PendingAuthentication<Claims>["read"]>[0]) =>
1034
+ readAuthenticatedPending<PendingAuthenticationRecord<Claims>>(mapping, input, "record").pipe(
1035
+ terminalD1,
1036
+ ),
1037
+ reject: <A>(
1038
+ input: Parameters<PendingAuthentication<Claims>["reject"]>[0],
1039
+ prepare: PrepareSessionCommit<{ readonly _tag: "Rejected" }, A>,
1040
+ ) =>
1041
+ runPlanned(
1042
+ Effect.gen(function* () {
1043
+ const database = yield* CurrentD1PlanningDatabase;
1044
+ const journal = yield* CurrentCommitJournal;
1045
+
1046
+ const record = yield* readPendingWith<PendingAuthenticationState>(
1047
+ mapping,
1048
+ input.digest,
1049
+ "context",
1050
+ );
1051
+
1052
+ if (record === undefined)
1053
+ return {
1054
+ receipt: prepare({ _tag: "Rejected" }, journal),
1055
+ statements: [yield* assertion(sql`true`)],
1056
+ };
1057
+
1058
+ const nativeSubjectId = yield* mapping.subjectId.toNative(
1059
+ record.evidence.revision.subjectId,
1060
+ );
1061
+
1062
+ const p = pendingColumns(mapping);
1063
+
1064
+ const condition = and(
1065
+ authorityCondition(mapping, nativeSubjectId, record.evidence),
1066
+ eq(p.digest, input.digest),
1067
+ eq(p.bindingDigest, input.bindingDigest),
1068
+ eq(p.consumed, false),
1069
+ gt(p.expiresAt, mapping.d1.engineNow),
1070
+ )!;
1071
+
1072
+ const update = database
1073
+ .update(mapping.pending.table)
1074
+ .set(
1075
+ updateValues<any>([
1076
+ [
1077
+ mapping.pending.failedAttempts,
1078
+ sql`case when ${p.failedAttempts} < ${p.attemptLimit} then ${p.failedAttempts} + 1 else ${p.failedAttempts} end`,
1079
+ ],
1080
+ ]),
1081
+ )
1082
+ .where(condition);
1083
+
1084
+ const receipt = prepare({ _tag: "Rejected" }, journal);
1085
+
1086
+ return { receipt, statements: [yield* statement(update)] };
1087
+ }),
1088
+ ),
1089
+ });
1090
+
1091
+ export const makeD1StatefulSessions = <Claims>(
1092
+ mapping: D1StatefulSessionMapping<Claims, any, any, any, any, any, any, any>,
1093
+ ) => {
1094
+ const s = sessionColumns(mapping);
1095
+
1096
+ const persistence = {
1097
+ establish: <A>(
1098
+ input: Parameters<StatefulSessionPersistence<Claims>["establish"]>[0],
1099
+ prepare: PrepareSessionCommit<
1100
+ PreparedValue<StatefulSessionPersistence<Claims>["establish"]>,
1101
+ A
1102
+ >,
1103
+ ) =>
1104
+ runPlanned(
1105
+ Effect.gen(function* () {
1106
+ const database = yield* CurrentD1PlanningDatabase;
1107
+ const journal = yield* CurrentCommitJournal;
1108
+ const policy = yield* requirement(mapping, input.evidence);
1109
+ const assessed = yield* assessAuthentication(input.evidence, policy);
1110
+
1111
+ if (!assessed.satisfied) return yield* stale();
1112
+
1113
+ const nativeSubjectId = yield* mapping.subjectId.toNative(
1114
+ input.evidence.revision.subjectId,
1115
+ );
1116
+
1117
+ if (input.pending === undefined && (yield* hasActiveFlow(mapping, input.evidence.flowId)))
1118
+ return yield* SessionConflict.make({});
1119
+
1120
+ const nativeSessionId = yield* allocate(
1121
+ mapping.session.allocateId,
1122
+ mapping.session.allocateIdSync,
1123
+ );
1124
+
1125
+ const sessionId = yield* mapping.sessionId.toSession(nativeSessionId);
1126
+
1127
+ const version = yield* allocate(
1128
+ mapping.session.allocateVersion,
1129
+ mapping.session.allocateVersionSync,
1130
+ );
1131
+
1132
+ const record = {
1133
+ ...input.session,
1134
+ sessionId,
1135
+ version,
1136
+ subjectId: input.evidence.revision.subjectId,
1137
+ securityRevision: input.evidence.revision.securityRevision,
1138
+ assurance: assessed.assurance,
1139
+ provenance: yield* snapshotSessionAuthenticationProvenance({
1140
+ evidence: input.evidence,
1141
+ }),
1142
+ // Ordered D1 batches cannot expose the engine timestamp to the
1143
+ // prepare callback. Persist the core's trusted planned time and
1144
+ // guard it against the engine clock in the final batch.
1145
+ issuedAt: DateTime.makeUnsafe(DateTime.toEpochMillis(input.session.issuedAt)),
1146
+ };
1147
+
1148
+ let condition = and(
1149
+ authorityCondition(mapping, nativeSubjectId, input.evidence),
1150
+ freshnessCondition(mapping, input.evidence, policy, true),
1151
+ sql`${DateTime.toEpochMillis(record.issuedAt)} <= ${mapping.d1.engineNowMillis}`,
1152
+ sql`${mapping.d1.engineNowMillis} < ${DateTime.toEpochMillis(record.expiresAt)}`,
1153
+ sql`${DateTime.toEpochMillis(record.expiresAt)} <= ${DateTime.toEpochMillis(record.absoluteExpiresAt)}`,
1154
+ )!;
1155
+
1156
+ const statements: Statement<any>[] = [];
1157
+
1158
+ if (input.pending === undefined) {
1159
+ const f = flowColumns(mapping);
1160
+
1161
+ condition = and(
1162
+ condition,
1163
+ not(
1164
+ sql`exists(select 1 from ${mapping.flow.table} where ${f.flowId} = ${sql.param(input.evidence.flowId, f.flowId)} and ${f.dedupUntil} > ${mapping.d1.engineNow})`,
1165
+ ),
1166
+ )!;
1167
+ if (mapping.pending !== undefined)
1168
+ statements.push(
1169
+ yield* statement(
1170
+ database
1171
+ .delete(mapping.pending.table)
1172
+ .where(
1173
+ eq(
1174
+ column(mapping.pending.table, mapping.pending.flowId),
1175
+ input.evidence.flowId,
1176
+ ),
1177
+ ),
1178
+ ),
1179
+ );
1180
+ statements.push(
1181
+ yield* statement(
1182
+ database.delete(mapping.flow.table).where(eq(f.flowId, input.evidence.flowId)),
1183
+ ),
1184
+ yield* statement(
1185
+ database.insert(mapping.flow.table).values(
1186
+ mapping.flow.encodeEstablishedInsert({
1187
+ evidence: input.evidence,
1188
+ subjectId: nativeSubjectId,
1189
+ dedupUntil: record.absoluteExpiresAt,
1190
+ }),
1191
+ ),
1192
+ ),
1193
+ );
1194
+ } else {
1195
+ if (mapping.pending === undefined) return yield* invalidPending();
1196
+ const stored = yield* readPending(mapping, input.pending.digest);
1197
+
1198
+ if (stored === undefined || !sameOriginalRevision(input.evidence, stored.evidence))
1199
+ return yield* invalidPending();
1200
+ condition = and(condition, pendingCondition(mapping, input.pending, nativeSubjectId))!;
1201
+ statements.push(
1202
+ ...(yield* consumePendingStatements(
1203
+ mapping,
1204
+ input.pending,
1205
+ record.absoluteExpiresAt,
1206
+ )),
1207
+ );
1208
+ }
1209
+ statements.push(
1210
+ yield* statement(
1211
+ database.insert(mapping.session.table).values(
1212
+ mapping.session.encodeInsert(record, {
1213
+ subjectId: nativeSubjectId,
1214
+ sessionId: nativeSessionId,
1215
+ }),
1216
+ ),
1217
+ ),
1218
+ );
1219
+ const receipt = prepare(record, journal);
1220
+
1221
+ return { receipt, statements: [yield* assertion(condition), ...statements] };
1222
+ }),
1223
+ mapping.isConstraintConflict,
1224
+ Effect.suspend<
1225
+ never,
1226
+ SessionConflict | PendingAuthenticationInvalid | StaleAuthentication | D1AdapterFailure,
1227
+ CurrentD1PlanningDatabase
1228
+ >(() =>
1229
+ input.pending === undefined
1230
+ ? recoverFlowOrStale(mapping, input.evidence.flowId)
1231
+ : recoverPendingOrStale(mapping, input.pending, input.evidence.revision.subjectId),
1232
+ ),
1233
+ ),
1234
+ verify: (input: Parameters<StatefulSessionPersistence<Claims>["verify"]>[0]) =>
1235
+ Effect.gen(function* () {
1236
+ const database = yield* CurrentD1PlanningDatabase;
1237
+
1238
+ const rows = yield* database
1239
+ .select({ ...getTableColumns(mapping.session.table) })
1240
+ .from(mapping.session.table)
1241
+ .innerJoin(
1242
+ mapping.subject.table,
1243
+ and(
1244
+ eq(s.subjectId, subjectColumns(mapping).id),
1245
+ eq(s.securityRevision, subjectColumns(mapping).revision),
1246
+ eq(subjectColumns(mapping).status, mapping.d1.activeSubjectStatusValue),
1247
+ ),
1248
+ )
1249
+ .where(
1250
+ and(
1251
+ eq(s.digest, input.digest),
1252
+ gt(s.expiresAt, mapping.d1.engineNow),
1253
+ gt(s.absoluteExpiresAt, mapping.d1.engineNow),
1254
+ ),
1255
+ )
1256
+ .limit(1);
1257
+
1258
+ if (rows[0] === undefined) return yield* invalidSession();
1259
+
1260
+ return yield* mapping.session.decode(rows[0]);
1261
+ }).pipe(terminalD1),
1262
+ rotate: <A>(
1263
+ input: Parameters<StatefulSessionPersistence<Claims>["rotate"]>[0],
1264
+ prepare: PrepareSessionCommit<PreparedValue<StatefulSessionPersistence<Claims>["rotate"]>, A>,
1265
+ ) =>
1266
+ runPlanned(
1267
+ Effect.gen(function* () {
1268
+ const database = yield* CurrentD1PlanningDatabase;
1269
+ const journal = yield* CurrentCommitJournal;
1270
+ const nativeSessionId = yield* mapping.sessionId.toNative(input.sessionId);
1271
+
1272
+ const row = (yield* database
1273
+ .select()
1274
+ .from(mapping.session.table)
1275
+ .where(eq(s.sessionId, nativeSessionId))
1276
+ .limit(1))[0];
1277
+
1278
+ if (row === undefined) return yield* SessionConflict.make({});
1279
+ const current = yield* mapping.session.decode(row);
1280
+ const nativeSubjectId = yield* mapping.subjectId.toNative(current.subjectId);
1281
+
1282
+ const version = yield* allocate(
1283
+ mapping.session.allocateVersion,
1284
+ mapping.session.allocateVersionSync,
1285
+ );
1286
+
1287
+ const next = {
1288
+ ...current,
1289
+ digest: input.nextDigest,
1290
+ credentialVersion: input.nextCredentialVersion,
1291
+ version,
1292
+ issuedAt: DateTime.makeUnsafe(DateTime.toEpochMillis(input.now)),
1293
+ expiresAt: input.nextExpiresAt,
1294
+ };
1295
+
1296
+ const sessionCondition = and(
1297
+ eq(s.sessionId, nativeSessionId),
1298
+ eq(s.digest, input.expectedDigest),
1299
+ eq(s.version, input.expectedVersion),
1300
+ eq(s.securityRevision, input.expectedSecurityRevision),
1301
+ gt(s.expiresAt, mapping.d1.engineNow),
1302
+ gt(s.absoluteExpiresAt, mapping.d1.engineNow),
1303
+ )!;
1304
+
1305
+ const timeCondition = and(
1306
+ sql`${DateTime.toEpochMillis(next.issuedAt)} <= ${mapping.d1.engineNowMillis}`,
1307
+ sql`${mapping.d1.engineNowMillis} < ${DateTime.toEpochMillis(input.nextExpiresAt)}`,
1308
+ sql`${DateTime.toEpochMillis(input.nextExpiresAt)} <= ${DateTime.toEpochMillis(current.absoluteExpiresAt)}`,
1309
+ )!;
1310
+
1311
+ const authority = subjectRevisionCondition(
1312
+ mapping,
1313
+ nativeSubjectId,
1314
+ input.expectedSecurityRevision,
1315
+ );
1316
+
1317
+ const assertionCondition = and(
1318
+ authority,
1319
+ sql`exists(select 1 from ${mapping.session.table} where ${sessionCondition})`,
1320
+ timeCondition,
1321
+ )!;
1322
+
1323
+ const update = database
1324
+ .update(mapping.session.table)
1325
+ .set(mapping.session.encodeRotation(next))
1326
+ .where(and(authority, sessionCondition, timeCondition));
1327
+
1328
+ const receipt = prepare(next, journal);
1329
+
1330
+ return {
1331
+ receipt,
1332
+ statements: [yield* assertion(assertionCondition), yield* statement(update)],
1333
+ };
1334
+ }),
1335
+ undefined,
1336
+ Effect.fail(SessionConflict.make({})),
1337
+ ),
1338
+ revokeDigest: <A>(
1339
+ digest: Parameters<StatefulSessionPersistence<Claims>["revokeDigest"]>[0],
1340
+ prepare: PrepareSessionCommit<boolean, A>,
1341
+ ) =>
1342
+ runPlanned(
1343
+ Effect.gen(function* () {
1344
+ const database = yield* CurrentD1PlanningDatabase;
1345
+ const journal = yield* CurrentCommitJournal;
1346
+
1347
+ const row = (yield* database
1348
+ .select()
1349
+ .from(mapping.session.table)
1350
+ .where(eq(s.digest, digest))
1351
+ .limit(1))[0];
1352
+
1353
+ const statements =
1354
+ row === undefined
1355
+ ? [yield* assertion(sql`true`)]
1356
+ : [
1357
+ yield* statement(
1358
+ database.delete(mapping.session.table).where(eq(s.digest, digest)),
1359
+ ),
1360
+ ];
1361
+
1362
+ return { receipt: prepare(row !== undefined, journal), statements };
1363
+ }),
1364
+ ),
1365
+ revoke: <A>(
1366
+ input: Parameters<StatefulSessionPersistence<Claims>["revoke"]>[0],
1367
+ prepare: PrepareSessionCommit<void, A>,
1368
+ ) =>
1369
+ runPlanned(
1370
+ Effect.gen(function* () {
1371
+ const database = yield* CurrentD1PlanningDatabase;
1372
+ const journal = yield* CurrentCommitJournal;
1373
+ const nativeSubjectId = yield* mapping.subjectId.toNative(input.subjectId);
1374
+ const nativeSessionId = yield* mapping.sessionId.toNative(input.sessionId);
1375
+
1376
+ const condition = subjectRevisionCondition(
1377
+ mapping,
1378
+ nativeSubjectId,
1379
+ input.expectedSecurityRevision,
1380
+ );
1381
+
1382
+ const remove = database
1383
+ .delete(mapping.session.table)
1384
+ .where(and(eq(s.subjectId, nativeSubjectId), eq(s.sessionId, nativeSessionId)));
1385
+
1386
+ const receipt = prepare(undefined, journal);
1387
+
1388
+ return {
1389
+ receipt,
1390
+ statements: [yield* assertion(condition), yield* statement(remove)],
1391
+ };
1392
+ }),
1393
+ undefined,
1394
+ Effect.fail(stale()),
1395
+ ),
1396
+ revokeAll: <A>(
1397
+ input: Parameters<StatefulSessionPersistence<Claims>["revokeAll"]>[0],
1398
+ prepare: PrepareSessionCommit<void, A>,
1399
+ ) =>
1400
+ runPlanned(
1401
+ Effect.gen(function* () {
1402
+ const database = yield* CurrentD1PlanningDatabase;
1403
+ const journal = yield* CurrentCommitJournal;
1404
+ const nativeSubjectId = yield* mapping.subjectId.toNative(input.subjectId);
1405
+
1406
+ const next = yield* allocate(
1407
+ mapping.subject.nextSecurityRevision?.(input.expectedSecurityRevision),
1408
+ mapping.subject.nextSecurityRevisionSync === undefined
1409
+ ? undefined
1410
+ : () => mapping.subject.nextSecurityRevisionSync!(input.expectedSecurityRevision),
1411
+ );
1412
+
1413
+ const sc = subjectColumns(mapping);
1414
+
1415
+ const condition = and(
1416
+ eq(sc.id, nativeSubjectId),
1417
+ eq(sc.status, mapping.d1.activeSubjectStatusValue),
1418
+ eq(sc.revision, input.expectedSecurityRevision),
1419
+ )!;
1420
+
1421
+ const updateSubject = database
1422
+ .update(mapping.subject.table)
1423
+ .set(updateValues<any>([[mapping.subject.securityRevision, next]]))
1424
+ .where(condition);
1425
+
1426
+ const removeSessions = database
1427
+ .delete(mapping.session.table)
1428
+ .where(eq(s.subjectId, nativeSubjectId));
1429
+
1430
+ const receipt = prepare(undefined, journal);
1431
+
1432
+ return {
1433
+ receipt,
1434
+ statements: [
1435
+ yield* assertion(
1436
+ sql`exists(select 1 from ${mapping.subject.table} where ${condition})`,
1437
+ ),
1438
+ yield* statement(updateSubject),
1439
+ yield* statement(removeSessions),
1440
+ ],
1441
+ };
1442
+ }),
1443
+ undefined,
1444
+ Effect.fail(stale()),
1445
+ ),
1446
+ };
1447
+
1448
+ const repository = {
1449
+ list: (input: Parameters<SessionRepository["list"]>[0]) =>
1450
+ Effect.gen(function* () {
1451
+ const database = yield* CurrentD1PlanningDatabase;
1452
+ const nativeSubjectId = yield* mapping.subjectId.toNative(input.subjectId);
1453
+
1454
+ const cursor =
1455
+ input.cursor === undefined
1456
+ ? undefined
1457
+ : yield* mapping.sessionId.toNative(input.cursor as any);
1458
+
1459
+ const rows = yield* database
1460
+ .select({ ...getTableColumns(mapping.session.table) })
1461
+ .from(mapping.session.table)
1462
+ .innerJoin(
1463
+ mapping.subject.table,
1464
+ and(
1465
+ eq(s.subjectId, subjectColumns(mapping).id),
1466
+ eq(s.securityRevision, subjectColumns(mapping).revision),
1467
+ eq(subjectColumns(mapping).status, mapping.d1.activeSubjectStatusValue),
1468
+ ),
1469
+ )
1470
+ .where(
1471
+ and(
1472
+ eq(s.subjectId, nativeSubjectId),
1473
+ gt(s.expiresAt, mapping.d1.engineNow),
1474
+ gt(s.absoluteExpiresAt, mapping.d1.engineNow),
1475
+ cursor === undefined ? undefined : gt(s.sessionId, cursor),
1476
+ ),
1477
+ )
1478
+ .orderBy(s.sessionId)
1479
+ .limit(input.limit + 1);
1480
+
1481
+ const decoded = yield* Effect.forEach(rows, (row) => mapping.session.decode(row));
1482
+ const codec = Schema.toCodecJson(Schema.toType(SessionMetadata));
1483
+
1484
+ const sessions = yield* Effect.forEach(decoded.slice(0, input.limit), (record) =>
1485
+ Schema.encodeEffect(codec)(record).pipe(Effect.flatMap(Schema.decodeEffect(codec))),
1486
+ );
1487
+
1488
+ return {
1489
+ sessions,
1490
+ ...(decoded.length <= input.limit ? {} : { nextCursor: sessions.at(-1)?.sessionId }),
1491
+ };
1492
+ }).pipe(terminalD1),
1493
+ };
1494
+
1495
+ return { persistence, repository };
1496
+ };
1497
+
1498
+ export const makeD1SignedValidity = (
1499
+ mapping: D1SignedSessionValidityMapping<any, any, any, any>,
1500
+ ) => {
1501
+ const sc = subjectColumns(mapping);
1502
+
1503
+ const t = {
1504
+ subjectId: column(mapping.tombstone.table, mapping.tombstone.subjectId),
1505
+ sessionId: column(mapping.tombstone.table, mapping.tombstone.sessionId),
1506
+ absoluteExpiresAt: column(mapping.tombstone.table, mapping.tombstone.absoluteExpiresAt),
1507
+ };
1508
+
1509
+ return {
1510
+ verify: (session: Parameters<SignedSessionValidity["verify"]>[0], _now: DateTime.Utc) =>
1511
+ Effect.gen(function* () {
1512
+ const database = yield* CurrentD1PlanningDatabase;
1513
+ const nativeSubjectId = yield* mapping.subjectId.toNative(session.subjectId);
1514
+ const nativeSessionId = yield* mapping.sessionId.toNative(session.sessionId);
1515
+
1516
+ const rows = yield* database
1517
+ .select({ subjectId: sc.id })
1518
+ .from(mapping.subject.table)
1519
+ .where(
1520
+ and(
1521
+ eq(sc.id, nativeSubjectId),
1522
+ eq(sc.status, mapping.d1.activeSubjectStatusValue),
1523
+ eq(sc.revision, session.securityRevision),
1524
+ sql`${mapping.d1.engineNowMillis} < ${DateTime.toEpochMillis(session.expiresAt)}`,
1525
+ sql`${mapping.d1.engineNowMillis} < ${DateTime.toEpochMillis(session.absoluteExpiresAt)}`,
1526
+ not(
1527
+ sql`exists(select 1 from ${mapping.tombstone.table} where ${t.subjectId} = ${sql.param(nativeSubjectId, t.subjectId)} and ${t.sessionId} = ${sql.param(nativeSessionId, t.sessionId)} and ${t.absoluteExpiresAt} > ${mapping.d1.engineNow})`,
1528
+ ),
1529
+ ),
1530
+ )
1531
+ .limit(1);
1532
+
1533
+ if (rows.length === 0) return yield* invalidSession();
1534
+ }).pipe(terminalD1),
1535
+ revoke: <A>(
1536
+ input: Parameters<SignedSessionValidity["revoke"]>[0],
1537
+ prepare: PrepareSessionCommit<void, A>,
1538
+ ) =>
1539
+ runPlanned(
1540
+ Effect.gen(function* () {
1541
+ const database = yield* CurrentD1PlanningDatabase;
1542
+ const journal = yield* CurrentCommitJournal;
1543
+ const nativeSubjectId = yield* mapping.subjectId.toNative(input.subjectId);
1544
+ const nativeSessionId = yield* mapping.sessionId.toNative(input.sessionId);
1545
+ const condition = sql`exists(select 1 from ${mapping.subject.table} where ${sc.id} = ${sql.param(nativeSubjectId, sc.id)} and ${sc.status} = ${sql.param(mapping.d1.activeSubjectStatusValue, sc.status)} and ${sc.revision} = ${sql.param(input.expectedSecurityRevision, sc.revision)})`;
1546
+ const encodedExpiry = mapping.tombstone.encodeInstant(input.absoluteExpiresAt);
1547
+ const desiredExpiry = sql.param(encodedExpiry, t.absoluteExpiresAt);
1548
+
1549
+ const write = database
1550
+ .insert(mapping.tombstone.table)
1551
+ .values(
1552
+ mapping.tombstone.encodeInsert({
1553
+ subjectId: nativeSubjectId,
1554
+ sessionId: nativeSessionId,
1555
+ absoluteExpiresAt: input.absoluteExpiresAt,
1556
+ }),
1557
+ )
1558
+ .onConflictDoUpdate({
1559
+ target: [t.subjectId, t.sessionId],
1560
+ set: updateValues<any>([
1561
+ [
1562
+ mapping.tombstone.absoluteExpiresAt,
1563
+ sql`case when ${t.absoluteExpiresAt} < ${desiredExpiry} then ${desiredExpiry} else ${t.absoluteExpiresAt} end`,
1564
+ ],
1565
+ ]),
1566
+ });
1567
+
1568
+ const receipt = prepare(undefined, journal);
1569
+
1570
+ return {
1571
+ receipt,
1572
+ statements: [yield* assertion(condition), yield* statement(write)],
1573
+ };
1574
+ }),
1575
+ undefined,
1576
+ Effect.fail(stale()),
1577
+ ),
1578
+ revokeAll: <A>(
1579
+ input: Parameters<SignedSessionValidity["revokeAll"]>[0],
1580
+ prepare: PrepareSessionCommit<void, A>,
1581
+ ) =>
1582
+ runPlanned(
1583
+ Effect.gen(function* () {
1584
+ const database = yield* CurrentD1PlanningDatabase;
1585
+ const journal = yield* CurrentCommitJournal;
1586
+ const nativeSubjectId = yield* mapping.subjectId.toNative(input.subjectId);
1587
+
1588
+ const next = yield* allocate(
1589
+ mapping.subject.nextSecurityRevision?.(input.expectedSecurityRevision),
1590
+ mapping.subject.nextSecurityRevisionSync === undefined
1591
+ ? undefined
1592
+ : () => mapping.subject.nextSecurityRevisionSync!(input.expectedSecurityRevision),
1593
+ );
1594
+
1595
+ const condition = and(
1596
+ eq(sc.id, nativeSubjectId),
1597
+ eq(sc.status, mapping.d1.activeSubjectStatusValue),
1598
+ eq(sc.revision, input.expectedSecurityRevision),
1599
+ )!;
1600
+
1601
+ const updateSubject = database
1602
+ .update(mapping.subject.table)
1603
+ .set(updateValues<any>([[mapping.subject.securityRevision, next]]))
1604
+ .where(condition);
1605
+
1606
+ const receipt = prepare(undefined, journal);
1607
+
1608
+ return {
1609
+ receipt,
1610
+ statements: [
1611
+ yield* assertion(
1612
+ sql`exists(select 1 from ${mapping.subject.table} where ${condition})`,
1613
+ ),
1614
+ yield* statement(updateSubject),
1615
+ ],
1616
+ };
1617
+ }),
1618
+ undefined,
1619
+ Effect.fail(stale()),
1620
+ ),
1621
+ };
1622
+ };
1623
+
1624
+ export const makeD1SessionServiceEffects = {
1625
+ authority: <Claims>(database: Database, mapping: any) =>
1626
+ Effect.gen(function* () {
1627
+ const hooks = yield* LifecycleHooks;
1628
+ const raw = makeD1AuthenticationAuthority<Claims>(mapping);
1629
+
1630
+ const run = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
1631
+ Effect.gen(function* () {
1632
+ if (
1633
+ (yield* Effect.serviceOption(CurrentD1SessionBatch))._tag === "Some" ||
1634
+ (yield* hasCommitScope)
1635
+ )
1636
+ return yield* unavailable();
1637
+
1638
+ return yield* effect.pipe(
1639
+ Effect.provideService(CurrentD1PlanningDatabase, database),
1640
+ Effect.provideService(LifecycleHooks, hooks),
1641
+ );
1642
+ });
1643
+
1644
+ const authenticationAuthority: AuthenticationAuthority["Service"] = {
1645
+ capture: (subjectId, credentialIds) => run(raw.capture(subjectId, credentialIds)),
1646
+ requirements: (evidence) => run(raw.requirements(evidence)),
1647
+ approve: (input, prepare) => run(raw.approve(input, prepare)),
1648
+ };
1649
+
1650
+ return { authenticationAuthority };
1651
+ }),
1652
+ pending: <Claims>(database: Database, mapping: any) =>
1653
+ Effect.gen(function* () {
1654
+ const hooks = yield* LifecycleHooks;
1655
+ const raw = makeD1PendingAuthentication<Claims>(mapping);
1656
+
1657
+ const run = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
1658
+ Effect.gen(function* () {
1659
+ if (
1660
+ (yield* Effect.serviceOption(CurrentD1SessionBatch))._tag === "Some" ||
1661
+ (yield* hasCommitScope)
1662
+ )
1663
+ return yield* unavailable();
1664
+
1665
+ return yield* effect.pipe(
1666
+ Effect.provideService(CurrentD1PlanningDatabase, database),
1667
+ Effect.provideService(LifecycleHooks, hooks),
1668
+ );
1669
+ });
1670
+
1671
+ const pendingAuthentication: PendingAuthentication<Claims> = {
1672
+ create: (input, now, prepare) => run(raw.create(input, now, prepare)),
1673
+ context: (input) => run(raw.context(input)),
1674
+ read: (input) => run(raw.read(input)),
1675
+ reject: (input, prepare) => run(raw.reject(input, prepare)),
1676
+ };
1677
+
1678
+ return { pendingAuthentication };
1679
+ }),
1680
+ stateful: <Claims>(database: Database, mapping: any) =>
1681
+ Effect.gen(function* () {
1682
+ const hooks = yield* LifecycleHooks;
1683
+ const raw = makeD1StatefulSessions<Claims>(mapping);
1684
+
1685
+ const run = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
1686
+ Effect.gen(function* () {
1687
+ if (
1688
+ (yield* Effect.serviceOption(CurrentD1SessionBatch))._tag === "Some" ||
1689
+ (yield* hasCommitScope)
1690
+ )
1691
+ return yield* unavailable();
1692
+
1693
+ return yield* effect.pipe(
1694
+ Effect.provideService(CurrentD1PlanningDatabase, database),
1695
+ Effect.provideService(LifecycleHooks, hooks),
1696
+ );
1697
+ });
1698
+
1699
+ const statefulSessionPersistence: StatefulSessionPersistence<Claims> = {
1700
+ establish: (input, prepare) => run(raw.persistence.establish(input, prepare)),
1701
+ verify: (input) => run(raw.persistence.verify(input)),
1702
+ rotate: (input, prepare) => run(raw.persistence.rotate(input, prepare)),
1703
+ revokeDigest: (digest, prepare) => run(raw.persistence.revokeDigest(digest, prepare)),
1704
+ revoke: (input, prepare) => run(raw.persistence.revoke(input, prepare)),
1705
+ revokeAll: (input, prepare) => run(raw.persistence.revokeAll(input, prepare)),
1706
+ };
1707
+
1708
+ const sessionRepository: SessionRepository = {
1709
+ list: (input) => run(raw.repository.list(input)),
1710
+ };
1711
+
1712
+ return { statefulSessionPersistence, sessionRepository };
1713
+ }),
1714
+ validity: (database: Database, mapping: any) =>
1715
+ Effect.gen(function* () {
1716
+ const hooks = yield* LifecycleHooks;
1717
+ const raw = makeD1SignedValidity(mapping);
1718
+
1719
+ const run = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
1720
+ Effect.gen(function* () {
1721
+ if (
1722
+ (yield* Effect.serviceOption(CurrentD1SessionBatch))._tag === "Some" ||
1723
+ (yield* hasCommitScope)
1724
+ )
1725
+ return yield* unavailable();
1726
+
1727
+ return yield* effect.pipe(
1728
+ Effect.provideService(CurrentD1PlanningDatabase, database),
1729
+ Effect.provideService(LifecycleHooks, hooks),
1730
+ );
1731
+ });
1732
+
1733
+ const signedSessionValidity: SignedSessionValidity = {
1734
+ verify: (session, now) => run(raw.verify(session, now)),
1735
+ revoke: (input, prepare) => run(raw.revoke(input, prepare)),
1736
+ revokeAll: (input, prepare) => run(raw.revokeAll(input, prepare)),
1737
+ };
1738
+
1739
+ return { signedSessionValidity };
1740
+ }),
1741
+ };
1742
+
1743
+ type D1CoordinatorError<E> = E | D1DomainError | HookConfigurationError;
1744
+
1745
+ /**
1746
+ * Owns one atomic D1 persistence transition together with any number of
1747
+ * application statements appended by `owner`. Keeping one persistence
1748
+ * transition makes a failed batch's domain guard recoverable without exposing
1749
+ * a prepared value. Compose additional transitions in later root batches.
1750
+ * Interruption or owner failure before `$client.batch` discards the journal;
1751
+ * the adapter performs no hidden retry and a retry must re-plan the whole owner.
1752
+ * Caller failures remain in the error channel. D1 batch/transport failures that
1753
+ * cannot be tied to this adapter's reserved guard become `SessionUnavailable`.
1754
+ */
1755
+ export const coordinateD1SessionBatch = <Services, A, E, R>(
1756
+ database: Database,
1757
+ make: (batch: D1SessionBatch) => Effect.Effect<Services, never, LifecycleHooks>,
1758
+ owner: (services: Services, batch: D1SessionBatch) => Effect.Effect<A, E, R>,
1759
+ ): Effect.Effect<A, D1CoordinatorError<E>, R | LifecycleHooks> =>
1760
+ Effect.gen(function* (): Effect.fn.Return<A, D1CoordinatorError<E>, R | LifecycleHooks> {
1761
+ if (yield* hasCommitScope) return yield* unavailable();
1762
+
1763
+ const result = yield* coordinateCommit(
1764
+ () =>
1765
+ Effect.gen(function* () {
1766
+ const statements: Statement<any>[] = [];
1767
+ let mutation: Planned<any> | undefined;
1768
+
1769
+ const nativeCollector = D1BatchStatements.of({
1770
+ append: (statement) => Effect.sync(() => statements.push(statement)),
1771
+ });
1772
+
1773
+ const d1Owner = yield* makeD1Owner(unavailable()).pipe(
1774
+ Effect.provideService(D1BatchStatements, nativeCollector),
1775
+ );
1776
+
1777
+ let batch!: D1SessionBatch;
1778
+
1779
+ const appendMutation = (input: Planned<any>) =>
1780
+ d1Owner.run(
1781
+ Effect.suspend(() => {
1782
+ // Guard recovery is unambiguous only for one persistence transition.
1783
+ // The owner may append any number of application statements around it.
1784
+ if (mutation !== undefined) return Effect.fail(unavailable());
1785
+ mutation = input;
1786
+ statements.push(...input.statements);
1787
+
1788
+ return Effect.void;
1789
+ }),
1790
+ );
1791
+
1792
+ batch = { owner: d1Owner, append: appendMutation };
1793
+ const services = yield* make(batch);
1794
+
1795
+ const value = yield* d1Owner.close(
1796
+ Effect.provideService(owner(services, batch), D1BatchStatements, d1Owner.collector),
1797
+ );
1798
+
1799
+ if (mutation?.journalGuard !== undefined) {
1800
+ const status = yield* Effect.result(mutation.journalGuard.read);
1801
+
1802
+ if (status._tag === "Success" || status.failure._tag !== "CommitPending")
1803
+ return yield* unavailable();
1804
+ }
1805
+
1806
+ yield* database.$client
1807
+ .batch(statements)
1808
+ .pipe(
1809
+ Effect.catchCause((cause) =>
1810
+ Effect.fail(
1811
+ new D1BatchFailure({ cause, ...(mutation === undefined ? {} : { mutation }) }),
1812
+ ),
1813
+ ),
1814
+ );
1815
+
1816
+ return value;
1817
+ }),
1818
+ { mode: "batch" },
1819
+ ).pipe(
1820
+ Effect.catchCause(recoverCoordinatedD1BatchCause),
1821
+ Effect.provideService(CurrentD1PlanningDatabase, database),
1822
+ );
1823
+
1824
+ return result.value;
1825
+ });
1826
+
1827
+ export const makeD1AuthenticationAuthorityServices = <
1828
+ Claims,
1829
+ Subject extends AnySQLiteTable,
1830
+ Credential extends AnySQLiteTable,
1831
+ Flow extends AnySQLiteTable,
1832
+ Pending extends AnySQLiteTable,
1833
+ NativeSubjectId,
1834
+ >(
1835
+ database: Database,
1836
+ mapping: D1AuthenticationAuthorityMapping<
1837
+ Claims,
1838
+ Subject,
1839
+ Credential,
1840
+ Flow,
1841
+ Pending,
1842
+ NativeSubjectId
1843
+ >,
1844
+ ) => makeD1SessionServiceEffects.authority<Claims>(database, mapping as any);
1845
+
1846
+ export const makeD1PendingAuthenticationServices = <
1847
+ Claims,
1848
+ Subject extends AnySQLiteTable,
1849
+ Credential extends AnySQLiteTable,
1850
+ Pending extends AnySQLiteTable,
1851
+ Flow extends AnySQLiteTable,
1852
+ NativeSubjectId,
1853
+ >(
1854
+ database: Database,
1855
+ mapping: D1PendingAuthenticationMapping<
1856
+ Claims,
1857
+ Subject,
1858
+ Credential,
1859
+ Pending,
1860
+ Flow,
1861
+ NativeSubjectId
1862
+ >,
1863
+ ) => makeD1SessionServiceEffects.pending<Claims>(database, mapping as any);
1864
+
1865
+ export const makeD1StatefulSessionServices = <
1866
+ Claims,
1867
+ Subject extends AnySQLiteTable,
1868
+ Credential extends AnySQLiteTable,
1869
+ Session extends AnySQLiteTable,
1870
+ Flow extends AnySQLiteTable,
1871
+ Pending extends AnySQLiteTable,
1872
+ NativeSubjectId,
1873
+ NativeSessionId,
1874
+ >(
1875
+ database: Database,
1876
+ mapping: D1StatefulSessionMapping<
1877
+ Claims,
1878
+ Subject,
1879
+ Credential,
1880
+ Session,
1881
+ Flow,
1882
+ Pending,
1883
+ NativeSubjectId,
1884
+ NativeSessionId
1885
+ >,
1886
+ ) => makeD1SessionServiceEffects.stateful<Claims>(database, mapping as any);
1887
+
1888
+ export const makeD1SignedSessionValidityServices = <
1889
+ Subject extends AnySQLiteTable,
1890
+ Tombstone extends AnySQLiteTable,
1891
+ NativeSubjectId,
1892
+ NativeSessionId,
1893
+ >(
1894
+ database: Database,
1895
+ mapping: D1SignedSessionValidityMapping<Subject, Tombstone, NativeSubjectId, NativeSessionId>,
1896
+ ) => makeD1SessionServiceEffects.validity(database, mapping as any);
1897
+
1898
+ export function coordinateD1AuthenticationAuthority<
1899
+ Claims,
1900
+ Subject extends AnySQLiteTable,
1901
+ Credential extends AnySQLiteTable,
1902
+ Flow extends AnySQLiteTable,
1903
+ Pending extends AnySQLiteTable,
1904
+ NativeSubjectId,
1905
+ A,
1906
+ E,
1907
+ R,
1908
+ DatabaseError,
1909
+ DatabaseRequirements,
1910
+ >(
1911
+ acquire: Effect.Effect<Database, DatabaseError, DatabaseRequirements>,
1912
+ options: {
1913
+ readonly mapping: D1AuthenticationAuthorityMapping<
1914
+ Claims,
1915
+ Subject,
1916
+ Credential,
1917
+ Flow,
1918
+ Pending,
1919
+ NativeSubjectId
1920
+ >;
1921
+ },
1922
+ body: Effect.Effect<A, E, R>,
1923
+ ): Effect.Effect<
1924
+ A,
1925
+ D1CoordinatorError<E> | DatabaseError,
1926
+ Exclude<R, AuthenticationAuthority | D1BatchStatements> | LifecycleHooks | DatabaseRequirements
1927
+ > {
1928
+ return Effect.flatMap(acquire, (database) =>
1929
+ coordinateD1SessionBatch(
1930
+ database,
1931
+ (batch) =>
1932
+ Effect.gen(function* () {
1933
+ const hooks = yield* LifecycleHooks;
1934
+ const raw = makeD1AuthenticationAuthority<Claims>(options.mapping as any);
1935
+
1936
+ const run = <Out, Failure, Requirements>(
1937
+ effect: Effect.Effect<Out, Failure, Requirements>,
1938
+ ) =>
1939
+ batch.owner.run(
1940
+ effect.pipe(
1941
+ Effect.provideService(CurrentD1PlanningDatabase, database),
1942
+ Effect.provideService(LifecycleHooks, hooks),
1943
+ Effect.provideService(CurrentD1SessionBatch, batch),
1944
+ ),
1945
+ );
1946
+
1947
+ const authenticationAuthority: AuthenticationAuthority["Service"] = {
1948
+ capture: (subjectId, credentialIds) => run(raw.capture(subjectId, credentialIds)),
1949
+ requirements: (evidence) => run(raw.requirements(evidence)),
1950
+ approve: (input, prepare) => run(raw.approve(input, prepare)),
1951
+ };
1952
+
1953
+ return { authenticationAuthority };
1954
+ }),
1955
+ (
1956
+ ownedServices: { readonly authenticationAuthority: AuthenticationAuthority["Service"] },
1957
+ batch,
1958
+ ) => {
1959
+ const provided = Context.make(
1960
+ AuthenticationAuthority,
1961
+ ownedServices.authenticationAuthority,
1962
+ ).pipe(Context.add(D1BatchStatements, batch.owner.collector));
1963
+
1964
+ return Effect.provideContext(body, provided);
1965
+ },
1966
+ ),
1967
+ );
1968
+ }
1969
+
1970
+ export function coordinateD1PendingAuthentication<
1971
+ TargetId,
1972
+ Claims,
1973
+ Subject extends AnySQLiteTable,
1974
+ Credential extends AnySQLiteTable,
1975
+ Pending extends AnySQLiteTable,
1976
+ Flow extends AnySQLiteTable,
1977
+ NativeSubjectId,
1978
+ A,
1979
+ E,
1980
+ R,
1981
+ DatabaseError,
1982
+ DatabaseRequirements,
1983
+ >(
1984
+ acquire: Effect.Effect<Database, DatabaseError, DatabaseRequirements>,
1985
+ options: {
1986
+ readonly mapping: D1PendingAuthenticationMapping<
1987
+ NoInfer<Claims>,
1988
+ Subject,
1989
+ Credential,
1990
+ Pending,
1991
+ Flow,
1992
+ NativeSubjectId
1993
+ >;
1994
+ readonly target: SuppliedService<TargetId, PendingAuthentication<Claims>>;
1995
+ },
1996
+ body: Effect.Effect<A, E, R>,
1997
+ ): Effect.Effect<
1998
+ A,
1999
+ D1CoordinatorError<E> | DatabaseError,
2000
+ Exclude<R, TargetId | D1BatchStatements> | LifecycleHooks | DatabaseRequirements
2001
+ > {
2002
+ return Effect.flatMap(acquire, (database) =>
2003
+ coordinateD1SessionBatch(
2004
+ database,
2005
+ (batch) =>
2006
+ Effect.gen(function* () {
2007
+ const hooks = yield* LifecycleHooks;
2008
+ const raw = makeD1PendingAuthentication<Claims>(options.mapping as any);
2009
+
2010
+ const run = <Out, Failure, Requirements>(
2011
+ effect: Effect.Effect<Out, Failure, Requirements>,
2012
+ ) =>
2013
+ batch.owner.run(
2014
+ effect.pipe(
2015
+ Effect.provideService(CurrentD1PlanningDatabase, database),
2016
+ Effect.provideService(LifecycleHooks, hooks),
2017
+ Effect.provideService(CurrentD1SessionBatch, batch),
2018
+ ),
2019
+ );
2020
+
2021
+ const pendingAuthentication: PendingAuthentication<Claims> = {
2022
+ create: (input, now, prepare) => run(raw.create(input, now, prepare)),
2023
+ context: (input) => run(raw.context(input)),
2024
+ read: (input) => run(raw.read(input)),
2025
+ reject: (input, prepare) => run(raw.reject(input, prepare)),
2026
+ };
2027
+
2028
+ return { pendingAuthentication };
2029
+ }),
2030
+ (ownedServices: { readonly pendingAuthentication: PendingAuthentication<Claims> }, batch) => {
2031
+ const provided = Context.make(options.target, ownedServices.pendingAuthentication).pipe(
2032
+ Context.add(D1BatchStatements, batch.owner.collector),
2033
+ );
2034
+
2035
+ return Effect.provideContext(body, provided);
2036
+ },
2037
+ ),
2038
+ );
2039
+ }
2040
+
2041
+ export function coordinateD1StatefulSessions<
2042
+ PersistenceId,
2043
+ RepositoryId,
2044
+ Claims,
2045
+ Subject extends AnySQLiteTable,
2046
+ Credential extends AnySQLiteTable,
2047
+ Session extends AnySQLiteTable,
2048
+ Flow extends AnySQLiteTable,
2049
+ Pending extends AnySQLiteTable,
2050
+ NativeSubjectId,
2051
+ NativeSessionId,
2052
+ A,
2053
+ E,
2054
+ R,
2055
+ DatabaseError,
2056
+ DatabaseRequirements,
2057
+ >(
2058
+ acquire: Effect.Effect<Database, DatabaseError, DatabaseRequirements>,
2059
+ options: {
2060
+ readonly mapping: D1StatefulSessionMapping<
2061
+ NoInfer<Claims>,
2062
+ Subject,
2063
+ Credential,
2064
+ Session,
2065
+ Flow,
2066
+ Pending,
2067
+ NativeSubjectId,
2068
+ NativeSessionId
2069
+ >;
2070
+ readonly persistence: SuppliedService<PersistenceId, StatefulSessionPersistence<Claims>>;
2071
+ readonly repository: SuppliedService<RepositoryId, SessionRepository>;
2072
+ },
2073
+ body: Effect.Effect<A, E, R>,
2074
+ ): Effect.Effect<
2075
+ A,
2076
+ D1CoordinatorError<E> | DatabaseError,
2077
+ | Exclude<R, PersistenceId | RepositoryId | D1BatchStatements>
2078
+ | LifecycleHooks
2079
+ | DatabaseRequirements
2080
+ > {
2081
+ return Effect.flatMap(acquire, (database) =>
2082
+ coordinateD1SessionBatch(
2083
+ database,
2084
+ (batch) =>
2085
+ Effect.gen(function* () {
2086
+ const hooks = yield* LifecycleHooks;
2087
+ const raw = makeD1StatefulSessions<Claims>(options.mapping as any);
2088
+
2089
+ const run = <Out, Failure, Requirements>(
2090
+ effect: Effect.Effect<Out, Failure, Requirements>,
2091
+ ) =>
2092
+ batch.owner.run(
2093
+ effect.pipe(
2094
+ Effect.provideService(CurrentD1PlanningDatabase, database),
2095
+ Effect.provideService(LifecycleHooks, hooks),
2096
+ Effect.provideService(CurrentD1SessionBatch, batch),
2097
+ ),
2098
+ );
2099
+
2100
+ const statefulSessionPersistence: StatefulSessionPersistence<Claims> = {
2101
+ establish: (input, prepare) => run(raw.persistence.establish(input, prepare)),
2102
+ verify: (input) => run(raw.persistence.verify(input)),
2103
+ rotate: (input, prepare) => run(raw.persistence.rotate(input, prepare)),
2104
+ revokeDigest: (digest, prepare) => run(raw.persistence.revokeDigest(digest, prepare)),
2105
+ revoke: (input, prepare) => run(raw.persistence.revoke(input, prepare)),
2106
+ revokeAll: (input, prepare) => run(raw.persistence.revokeAll(input, prepare)),
2107
+ };
2108
+
2109
+ const sessionRepository: SessionRepository = {
2110
+ list: (input) => run(raw.repository.list(input)),
2111
+ };
2112
+
2113
+ return { statefulSessionPersistence, sessionRepository };
2114
+ }),
2115
+ (
2116
+ ownedServices: {
2117
+ readonly statefulSessionPersistence: StatefulSessionPersistence<Claims>;
2118
+ readonly sessionRepository: SessionRepository;
2119
+ },
2120
+ batch,
2121
+ ) => {
2122
+ const provided = Context.make(
2123
+ options.persistence,
2124
+ ownedServices.statefulSessionPersistence,
2125
+ ).pipe(
2126
+ Context.add(options.repository, ownedServices.sessionRepository),
2127
+ Context.add(D1BatchStatements, batch.owner.collector),
2128
+ );
2129
+
2130
+ return Effect.provideContext(body, provided);
2131
+ },
2132
+ ),
2133
+ );
2134
+ }
2135
+
2136
+ export function coordinateD1SignedSessionValidity<
2137
+ TargetId,
2138
+ Subject extends AnySQLiteTable,
2139
+ Tombstone extends AnySQLiteTable,
2140
+ NativeSubjectId,
2141
+ NativeSessionId,
2142
+ A,
2143
+ E,
2144
+ R,
2145
+ DatabaseError,
2146
+ DatabaseRequirements,
2147
+ >(
2148
+ acquire: Effect.Effect<Database, DatabaseError, DatabaseRequirements>,
2149
+ options: {
2150
+ readonly mapping: D1SignedSessionValidityMapping<
2151
+ Subject,
2152
+ Tombstone,
2153
+ NativeSubjectId,
2154
+ NativeSessionId
2155
+ >;
2156
+ readonly target: SuppliedService<TargetId, SignedSessionValidity>;
2157
+ },
2158
+ body: Effect.Effect<A, E, R>,
2159
+ ): Effect.Effect<
2160
+ A,
2161
+ D1CoordinatorError<E> | DatabaseError,
2162
+ Exclude<R, TargetId | D1BatchStatements> | LifecycleHooks | DatabaseRequirements
2163
+ > {
2164
+ return Effect.flatMap(acquire, (database) =>
2165
+ coordinateD1SessionBatch(
2166
+ database,
2167
+ (batch) =>
2168
+ Effect.gen(function* () {
2169
+ const hooks = yield* LifecycleHooks;
2170
+ const raw = makeD1SignedValidity(options.mapping as any);
2171
+
2172
+ const run = <Out, Failure, Requirements>(
2173
+ effect: Effect.Effect<Out, Failure, Requirements>,
2174
+ ) =>
2175
+ batch.owner.run(
2176
+ effect.pipe(
2177
+ Effect.provideService(CurrentD1PlanningDatabase, database),
2178
+ Effect.provideService(LifecycleHooks, hooks),
2179
+ Effect.provideService(CurrentD1SessionBatch, batch),
2180
+ ),
2181
+ );
2182
+
2183
+ const signedSessionValidity: SignedSessionValidity = {
2184
+ verify: (session, now) => run(raw.verify(session, now)),
2185
+ revoke: (input, prepare) => run(raw.revoke(input, prepare)),
2186
+ revokeAll: (input, prepare) => run(raw.revokeAll(input, prepare)),
2187
+ };
2188
+
2189
+ return { signedSessionValidity };
2190
+ }),
2191
+ (ownedServices: { readonly signedSessionValidity: SignedSessionValidity }, batch) => {
2192
+ const provided = Context.make(options.target, ownedServices.signedSessionValidity).pipe(
2193
+ Context.add(D1BatchStatements, batch.owner.collector),
2194
+ );
2195
+
2196
+ return Effect.provideContext(body, provided);
2197
+ },
2198
+ ),
2199
+ );
2200
+ }
2201
+
2202
+ const stepUpColumns = (mapping: any) =>
2203
+ Object.fromEntries(
2204
+ [
2205
+ "digest",
2206
+ "version",
2207
+ "flowId",
2208
+ "subjectId",
2209
+ "bindingDigest",
2210
+ "snapshot",
2211
+ "expiresAt",
2212
+ "attemptLimit",
2213
+ "failedAttempts",
2214
+ "consumed",
2215
+ ].map((key) => [key, column(mapping.intent.table, mapping.intent[key])]),
2216
+ );
2217
+
2218
+ const stepUpSourceCondition = (
2219
+ mapping: any,
2220
+ intent: SessionStepUpIntent,
2221
+ nativeSubjectId: unknown,
2222
+ nativeSessionId: unknown,
2223
+ plan?: SessionStepUpCompletionPlan<unknown>,
2224
+ ): SQL => {
2225
+ const source = mapping.source;
2226
+
2227
+ if (source.kind !== intent.sourceKind) return sql`false`;
2228
+
2229
+ const time = and(
2230
+ sql`${mapping.d1.engineNowMillis} >= ${DateTime.toEpochMillis(intent.sourceAuthenticatedAt)}`,
2231
+ sql`${mapping.d1.engineNowMillis} < ${DateTime.toEpochMillis(intent.sourceExpiresAt)}`,
2232
+ sql`${mapping.d1.engineNowMillis} < ${DateTime.toEpochMillis(intent.sourceAbsoluteExpiresAt)}`,
2233
+ )!;
2234
+
2235
+ if (source.kind === "StatelessSigned") return time;
2236
+ if (source.kind === "StateAssistedSigned") {
2237
+ const t = source.tombstone,
2238
+ owner = column(t.table, t.subjectId),
2239
+ id = column(t.table, t.sessionId),
2240
+ expiry = column(t.table, t.absoluteExpiresAt);
2241
+
2242
+ return and(
2243
+ time,
2244
+ not(
2245
+ sql`exists(select 1 from ${t.table} where ${owner}=${sql.param(nativeSubjectId, owner)} and ${id}=${sql.param(nativeSessionId, id)} and ${expiry}>${mapping.d1.engineNow})`,
2246
+ ),
2247
+ )!;
2248
+ }
2249
+
2250
+ const s = source.session,
2251
+ c = sessionColumns(source);
2252
+
2253
+ const condition = and(
2254
+ eq(c.sessionId, nativeSessionId),
2255
+ eq(c.subjectId, nativeSubjectId),
2256
+ eq(c.securityRevision, intent.revision.securityRevision),
2257
+ eq(column(s.table, s.credentialVersion), intent.sourceCredentialVersion),
2258
+ eq(column(s.table, s.authenticatedAt), s.encodeInstant(intent.sourceAuthenticatedAt)),
2259
+ eq(c.expiresAt, s.encodeInstant(intent.sourceExpiresAt)),
2260
+ eq(c.absoluteExpiresAt, s.encodeInstant(intent.sourceAbsoluteExpiresAt)),
2261
+ gt(c.expiresAt, mapping.d1.engineNow),
2262
+ gt(c.absoluteExpiresAt, mapping.d1.engineNow),
2263
+ plan?.replacement._tag === "Stateful"
2264
+ ? eq(c.digest, plan.replacement.expectedDigest)
2265
+ : undefined,
2266
+ plan?.replacement._tag === "Stateful"
2267
+ ? eq(c.version, plan.replacement.expectedRowVersion)
2268
+ : undefined,
2269
+ )!;
2270
+
2271
+ return and(time, sql`exists(select 1 from ${s.table} where ${condition})`)!;
2272
+ };
2273
+
2274
+ const stepUpRowCondition = (
2275
+ mapping: any,
2276
+ row: any,
2277
+ intent: SessionStepUpIntent,
2278
+ nativeSubjectId: unknown,
2279
+ ): SQL => {
2280
+ const c = stepUpColumns(mapping),
2281
+ m = mapping.intent;
2282
+
2283
+ return and(
2284
+ eq(c.digest, intent.digest),
2285
+ eq(c.version, intent.version),
2286
+ eq(c.flowId, intent.flowId),
2287
+ eq(c.subjectId, nativeSubjectId),
2288
+ eq(c.bindingDigest, intent.bindingDigest),
2289
+ eq(c.snapshot, row[m.snapshot]),
2290
+ eq(c.expiresAt, m.encodeInstant(intent.expiresAt)),
2291
+ eq(c.attemptLimit, intent.attemptLimit),
2292
+ eq(c.consumed, false),
2293
+ eq(c.failedAttempts, row[m.failedAttempts]),
2294
+ sql`${c.failedAttempts} >= 0`,
2295
+ sql`${c.failedAttempts} < ${c.attemptLimit}`,
2296
+ gt(c.expiresAt, mapping.d1.engineNow),
2297
+ )!;
2298
+ };
2299
+
2300
+ const readD1StepUp = Effect.fn("DrizzleD1StepUp.read")(function* (
2301
+ mapping: AnyD1StepUpMapping,
2302
+ digest: TokenDigest,
2303
+ ) {
2304
+ const database = yield* CurrentD1PlanningDatabase;
2305
+
2306
+ const m = mapping.intent,
2307
+ c = stepUpColumns(mapping);
2308
+
2309
+ const row = (yield* database.select().from(m.table).where(eq(c.digest, digest)).limit(1))[0];
2310
+
2311
+ if (row === undefined) return yield* SessionStepUpInvalid.make({});
2312
+ const intent = yield* decodeStepUpIntent(row[m.snapshot]);
2313
+
2314
+ if (
2315
+ intent.digest !== digest ||
2316
+ !stepUpIntentLive(intent, mapping.source.kind, yield* DateTime.now) ||
2317
+ !Schema.is(Schema.Natural)(row[m.failedAttempts])
2318
+ )
2319
+ return yield* SessionStepUpInvalid.make({});
2320
+ const nativeSubjectId = yield* mapping.subjectId.toNative(intent.revision.subjectId);
2321
+
2322
+ const nativeSessionId =
2323
+ mapping.source.kind === "StatelessSigned"
2324
+ ? undefined
2325
+ : yield* mapping.source.sessionId.toNative(intent.sourceSessionId);
2326
+
2327
+ const condition = and(
2328
+ authorityCondition(mapping, nativeSubjectId, intent),
2329
+ stepUpSourceCondition(mapping, intent, nativeSubjectId, nativeSessionId),
2330
+ sql`exists(select 1 from ${m.table} where ${stepUpRowCondition(mapping, row, intent, nativeSubjectId)})`,
2331
+ sql`${mapping.d1.engineNowMillis} < ${DateTime.toEpochMillis(intent.expiresAt)}`,
2332
+ )!;
2333
+
2334
+ if (!(yield* conditionMatches(condition))) return yield* SessionStepUpInvalid.make({});
2335
+
2336
+ return { row, intent, nativeSubjectId, nativeSessionId };
2337
+ });
2338
+
2339
+ const isD1StepUpRejectAbsence = (error: unknown): error is SessionStepUpInvalid =>
2340
+ typeof error === "object" &&
2341
+ error !== null &&
2342
+ Reflect.get(error, "_tag") === "SessionStepUpInvalid";
2343
+
2344
+ type D1StepUpReadFailure = D1AdapterFailure | SessionStepUpInvalid | SessionUnavailable;
2345
+
2346
+ const recoverD1StepUpRejectAbsence = <A, B, R>(
2347
+ effect: Effect.Effect<A, D1StepUpReadFailure, R>,
2348
+ rejected: B,
2349
+ ): Effect.Effect<A | B, D1AdapterFailure | SessionUnavailable, R> =>
2350
+ effect.pipe(
2351
+ Effect.catchCause((cause) => {
2352
+ if (
2353
+ cause.reasons.length > 0 &&
2354
+ cause.reasons.every(
2355
+ (reason) => Cause.isFailReason(reason) && isD1StepUpRejectAbsence(reason.error),
2356
+ )
2357
+ )
2358
+ return Effect.succeed(rejected);
2359
+
2360
+ return Effect.failCause(
2361
+ Cause.map(cause, (error) =>
2362
+ isD1StepUpRejectAbsence(error) ? SessionUnavailable.make({}) : error,
2363
+ ),
2364
+ );
2365
+ }),
2366
+ );
2367
+
2368
+ export const makeD1SessionStepUp = <Claims>(
2369
+ mapping: D1SessionStepUpMapping<Claims, any, any, any, any, any, any, any>,
2370
+ ) => {
2371
+ const m = mapping.intent,
2372
+ c = stepUpColumns(mapping);
2373
+
2374
+ const recover = Effect.fail(SessionStepUpInvalid.make({}));
2375
+
2376
+ return {
2377
+ create: <A>(
2378
+ input: Parameters<SessionStepUpPersistence<Claims>["create"]>[0],
2379
+ _now: DateTime.Utc,
2380
+ prepare: PrepareSessionCommit<SessionStepUpIntent, A>,
2381
+ ) =>
2382
+ runPlanned(
2383
+ Effect.gen(function* () {
2384
+ const database = yield* CurrentD1PlanningDatabase;
2385
+ const journal = yield* CurrentCommitJournal;
2386
+
2387
+ const version = yield* allocate(m.allocateVersion, m.allocateVersionSync),
2388
+ record: SessionStepUpIntent = { ...input, version };
2389
+
2390
+ if (!stepUpIntentLive(record, mapping.source.kind, yield* DateTime.now))
2391
+ return yield* stale();
2392
+ const nativeSubjectId = yield* mapping.subjectId.toNative(input.revision.subjectId);
2393
+
2394
+ const nativeSessionId =
2395
+ mapping.source.kind === "StatelessSigned"
2396
+ ? undefined
2397
+ : yield* mapping.source.sessionId.toNative(input.sourceSessionId);
2398
+
2399
+ const existing = yield* database
2400
+ .select()
2401
+ .from(m.table)
2402
+ .where(or(eq(c.digest, input.digest), eq(c.flowId, input.flowId)))
2403
+ .limit(1);
2404
+
2405
+ if (existing.length > 0) return yield* SessionConflict.make({});
2406
+
2407
+ const condition = and(
2408
+ authorityCondition(mapping, nativeSubjectId, record),
2409
+ stepUpSourceCondition(mapping, record, nativeSubjectId, nativeSessionId),
2410
+ sql`${mapping.d1.engineNowMillis} < ${DateTime.toEpochMillis(input.expiresAt)}`,
2411
+ )!;
2412
+
2413
+ if (!(yield* conditionMatches(condition))) return yield* stale();
2414
+ const snapshot = yield* encodeStepUpIntent(record);
2415
+
2416
+ const values = {
2417
+ ...m.encodeInsert(record, { subjectId: nativeSubjectId }),
2418
+ ...updateValues<any>([
2419
+ [m.digest, record.digest],
2420
+ [m.version, version],
2421
+ [m.flowId, record.flowId],
2422
+ [m.subjectId, nativeSubjectId],
2423
+ [m.bindingDigest, record.bindingDigest],
2424
+ [m.snapshot, snapshot],
2425
+ [m.expiresAt, m.encodeInstant(record.expiresAt)],
2426
+ [m.attemptLimit, record.attemptLimit],
2427
+ [m.failedAttempts, 0],
2428
+ [m.consumed, false],
2429
+ ]),
2430
+ };
2431
+
2432
+ const receipt = prepare(record, journal);
2433
+
2434
+ const exact = and(
2435
+ eq(c.digest, record.digest),
2436
+ eq(c.version, version),
2437
+ eq(c.flowId, record.flowId),
2438
+ eq(c.subjectId, nativeSubjectId),
2439
+ eq(c.bindingDigest, record.bindingDigest),
2440
+ eq(c.snapshot, snapshot),
2441
+ eq(c.expiresAt, m.encodeInstant(record.expiresAt)),
2442
+ eq(c.attemptLimit, record.attemptLimit),
2443
+ eq(c.failedAttempts, 0),
2444
+ eq(c.consumed, false),
2445
+ )!;
2446
+
2447
+ return {
2448
+ receipt,
2449
+ statements: [
2450
+ yield* assertion(condition),
2451
+ yield* statement(database.insert(m.table).values(values)),
2452
+ yield* assertion(sql`changes() = 1`),
2453
+ yield* assertion(
2454
+ and(condition, sql`exists(select 1 from ${m.table} where ${exact})`)!,
2455
+ ),
2456
+ ],
2457
+ };
2458
+ }),
2459
+ mapping.isConstraintConflict,
2460
+ Effect.fail(stale()),
2461
+ ),
2462
+ context: (input: Parameters<SessionStepUpPersistence<Claims>["context"]>[0]) =>
2463
+ mapFailureCause(
2464
+ readD1StepUp(mapping, input.digest).pipe(
2465
+ Effect.flatMap(({ intent }) =>
2466
+ snapshotPendingAuthenticationContext({
2467
+ flowId: intent.flowId,
2468
+ bindingDigest: intent.bindingDigest,
2469
+ revision: intent.revision,
2470
+ expiresAtMillis: DateTime.toEpochMillis(intent.expiresAt),
2471
+ }),
2472
+ ),
2473
+ ),
2474
+ (error) => (error._tag === "PendingAuthenticationInvalid" ? unavailable() : error),
2475
+ ).pipe(terminalD1),
2476
+ read: (input: Parameters<SessionStepUpPersistence<Claims>["read"]>[0]) =>
2477
+ readD1StepUp(mapping, input.digest).pipe(
2478
+ Effect.flatMap(({ intent }) =>
2479
+ intent.bindingDigest === input.bindingDigest
2480
+ ? Effect.succeed(intent)
2481
+ : Effect.fail(SessionStepUpInvalid.make({})),
2482
+ ),
2483
+ terminalD1,
2484
+ ),
2485
+ reject: <A>(
2486
+ input: Parameters<SessionStepUpPersistence<Claims>["reject"]>[0],
2487
+ prepare: PrepareSessionCommit<{ readonly _tag: "Rejected" }, A>,
2488
+ ) =>
2489
+ runPlanned(
2490
+ Effect.gen(function* () {
2491
+ const database = yield* CurrentD1PlanningDatabase;
2492
+ const journal = yield* CurrentCommitJournal;
2493
+
2494
+ const selected = yield* recoverD1StepUpRejectAbsence(
2495
+ readD1StepUp(mapping, input.digest).pipe(
2496
+ Effect.map((value) => ({ _tag: "Found" as const, value })),
2497
+ ),
2498
+ { _tag: "Rejected" as const },
2499
+ );
2500
+
2501
+ if (selected._tag === "Rejected")
2502
+ return { receipt: prepare({ _tag: "Rejected" }, journal), statements: [] };
2503
+ const { row, intent, nativeSubjectId, nativeSessionId } = selected.value;
2504
+
2505
+ // Rejection does not replace immutable intent identity. Concurrent contenders
2506
+ // increment the authoritative counter under the same cap; completion pins it.
2507
+ const condition = and(
2508
+ eq(c.digest, intent.digest),
2509
+ eq(c.version, intent.version),
2510
+ eq(c.snapshot, row[m.snapshot]),
2511
+ eq(c.subjectId, nativeSubjectId),
2512
+ eq(c.bindingDigest, intent.bindingDigest),
2513
+ eq(c.flowId, intent.flowId),
2514
+ eq(c.expiresAt, m.encodeInstant(intent.expiresAt)),
2515
+ eq(c.attemptLimit, intent.attemptLimit),
2516
+ eq(c.consumed, false),
2517
+ sql`${c.failedAttempts} >= 0`,
2518
+ sql`${c.failedAttempts} < ${c.attemptLimit}`,
2519
+ gt(c.expiresAt, mapping.d1.engineNow),
2520
+ authorityCondition(mapping, nativeSubjectId, intent),
2521
+ stepUpSourceCondition(mapping, intent, nativeSubjectId, nativeSessionId),
2522
+ )!;
2523
+
2524
+ const write = database
2525
+ .update(m.table)
2526
+ .set(updateValues<any>([[m.failedAttempts, sql`${c.failedAttempts} + 1`]]))
2527
+ .where(condition);
2528
+
2529
+ // A valid still-chargeable row cannot survive a zero-write trigger. A row
2530
+ // consumed/expired/exhausted while queued legitimately returns Rejected.
2531
+ const settled = sql`changes() = 1 or not exists(select 1 from ${m.table} where ${condition})`;
2532
+
2533
+ return {
2534
+ receipt: prepare({ _tag: "Rejected" }, journal),
2535
+ statements: [yield* statement(write), yield* assertion(settled)],
2536
+ };
2537
+ }),
2538
+ undefined,
2539
+ Effect.fail(unavailable()),
2540
+ ),
2541
+ complete: <A>(
2542
+ plan: SessionStepUpCompletionPlan<Claims>,
2543
+ prepare: PrepareSessionCommit<PreparedValue<SessionStepUpPersistence<Claims>["complete"]>, A>,
2544
+ ) =>
2545
+ runPlanned(
2546
+ Effect.gen(function* () {
2547
+ const database = yield* CurrentD1PlanningDatabase;
2548
+ const journal = yield* CurrentCommitJournal;
2549
+
2550
+ yield* validateStepUpPlan(plan);
2551
+ const stored = yield* readD1StepUp(mapping, plan.intent.digest);
2552
+
2553
+ if (
2554
+ (yield* encodeStepUpIntent(stored.intent)) !== (yield* encodeStepUpIntent(plan.intent))
2555
+ )
2556
+ return yield* SessionStepUpInvalid.make({});
2557
+ const base = yield* requirement(mapping, plan.evidence);
2558
+
2559
+ if (
2560
+ !(yield* assessAuthentication(plan.evidence, base)).satisfied ||
2561
+ !(yield* assessAuthentication(plan.evidence, stored.intent.requirement)).satisfied
2562
+ )
2563
+ return yield* stale();
2564
+
2565
+ const { row, intent, nativeSubjectId, nativeSessionId } = stored,
2566
+ replacement = plan.replacement;
2567
+
2568
+ const target = replacement.inspection.session;
2569
+
2570
+ if (
2571
+ DateTime.toEpochMillis(target.expiresAt) >
2572
+ DateTime.toEpochMillis(intent.sourceAbsoluteExpiresAt)
2573
+ )
2574
+ return yield* SessionStepUpInvalid.make({});
2575
+
2576
+ const live = and(
2577
+ authorityCondition(mapping, nativeSubjectId, plan.evidence),
2578
+ freshnessCondition(mapping, plan.evidence, base, true),
2579
+ freshnessCondition(mapping, plan.evidence, intent.requirement, true),
2580
+ sql`${mapping.d1.engineNowMillis} >= ${DateTime.toEpochMillis(intent.sourceAuthenticatedAt)}`,
2581
+ sql`${mapping.d1.engineNowMillis} < ${DateTime.toEpochMillis(intent.expiresAt)}`,
2582
+ sql`${mapping.d1.engineNowMillis} < ${DateTime.toEpochMillis(intent.sourceExpiresAt)}`,
2583
+ sql`${mapping.d1.engineNowMillis} < ${DateTime.toEpochMillis(intent.sourceAbsoluteExpiresAt)}`,
2584
+ sql`${mapping.d1.engineNowMillis} >= ${DateTime.toEpochMillis(target.issuedAt)}`,
2585
+ sql`${mapping.d1.engineNowMillis} < ${DateTime.toEpochMillis(target.expiresAt)}`,
2586
+ )!;
2587
+
2588
+ const source = stepUpSourceCondition(
2589
+ mapping,
2590
+ intent,
2591
+ nativeSubjectId,
2592
+ nativeSessionId,
2593
+ plan,
2594
+ );
2595
+
2596
+ const version = yield* allocate(m.allocateVersion, m.allocateVersionSync);
2597
+
2598
+ if (version === intent.version) return yield* unavailable();
2599
+ const snapshot = yield* encodeStepUpIntent({ ...intent, version });
2600
+
2601
+ const consume = database
2602
+ .update(m.table)
2603
+ .set(
2604
+ updateValues<any>([
2605
+ [m.consumed, true],
2606
+ [m.version, version],
2607
+ [m.snapshot, snapshot],
2608
+ ]),
2609
+ )
2610
+ .where(and(stepUpRowCondition(mapping, row, intent, nativeSubjectId), live, source));
2611
+
2612
+ const marker = sql`exists(select 1 from ${m.table} where ${c.digest}=${sql.param(intent.digest, c.digest)} and ${c.version}=${sql.param(version, c.version)} and ${c.snapshot}=${sql.param(snapshot, c.snapshot)} and ${c.consumed}=${sql.param(true, c.consumed)} and ${c.failedAttempts}=${row[m.failedAttempts]})`;
2613
+
2614
+ const statements: Statement<any>[] = [
2615
+ yield* statement(consume),
2616
+ yield* assertion(sql`changes() = 1`),
2617
+ ];
2618
+
2619
+ let post: SQL = sql`true`;
2620
+
2621
+ if (replacement._tag === "Stateful") {
2622
+ if (mapping.source.kind !== "Stateful") return yield* SessionStepUpInvalid.make({});
2623
+
2624
+ const s = mapping.source.session,
2625
+ sc = sessionColumns(mapping.source);
2626
+
2627
+ const rowVersion = yield* allocate(s.allocateVersion, s.allocateVersionSync);
2628
+
2629
+ const next = {
2630
+ ...target,
2631
+ provenance: replacement.inspection.provenance,
2632
+ credentialVersion: replacement.inspection.credentialVersion,
2633
+ digest: replacement.nextDigest,
2634
+ version: rowVersion,
2635
+ };
2636
+
2637
+ const rotation = {
2638
+ ...s.encodeRotation(next),
2639
+ ...updateValues<any>([
2640
+ [s.version, rowVersion],
2641
+ [s.digest, next.digest],
2642
+ [s.credentialVersion, next.credentialVersion],
2643
+ [s.authenticatedAt, s.encodeInstant(target.assurance.authenticatedAt)],
2644
+ [s.issuedAt, s.encodeInstant(target.issuedAt)],
2645
+ [s.expiresAt, s.encodeInstant(target.expiresAt)],
2646
+ [s.absoluteExpiresAt, s.encodeInstant(target.absoluteExpiresAt)],
2647
+ ]),
2648
+ };
2649
+
2650
+ const write = database
2651
+ .update(s.table)
2652
+ .set(rotation)
2653
+ .where(
2654
+ and(
2655
+ eq(sc.sessionId, nativeSessionId),
2656
+ eq(sc.digest, replacement.expectedDigest),
2657
+ eq(sc.version, replacement.expectedRowVersion),
2658
+ marker,
2659
+ source,
2660
+ live,
2661
+ ),
2662
+ );
2663
+
2664
+ statements.push(yield* statement(write), yield* assertion(sql`changes() = 1`));
2665
+
2666
+ const exact = and(
2667
+ eq(sc.sessionId, nativeSessionId),
2668
+ eq(sc.subjectId, nativeSubjectId),
2669
+ eq(sc.version, rowVersion),
2670
+ eq(sc.digest, next.digest),
2671
+ eq(sc.securityRevision, target.securityRevision),
2672
+ eq(column(s.table, s.credentialVersion), next.credentialVersion),
2673
+ eq(
2674
+ column(s.table, s.authenticatedAt),
2675
+ s.encodeInstant(target.assurance.authenticatedAt),
2676
+ ),
2677
+ eq(sc.issuedAt, s.encodeInstant(target.issuedAt)),
2678
+ eq(sc.expiresAt, s.encodeInstant(target.expiresAt)),
2679
+ eq(sc.absoluteExpiresAt, s.encodeInstant(target.absoluteExpiresAt)),
2680
+ )!;
2681
+
2682
+ post = sql`exists(select 1 from ${s.table} where ${exact})`;
2683
+ } else if (replacement._tag === "StateAssistedSigned") {
2684
+ if (mapping.source.kind !== "StateAssistedSigned")
2685
+ return yield* SessionStepUpInvalid.make({});
2686
+
2687
+ const t = mapping.source.tombstone,
2688
+ owner = column(t.table, t.subjectId),
2689
+ id = column(t.table, t.sessionId),
2690
+ expiry = column(t.table, t.absoluteExpiresAt);
2691
+
2692
+ statements.push(
2693
+ yield* statement(
2694
+ database
2695
+ .delete(t.table)
2696
+ .where(
2697
+ and(
2698
+ eq(owner, nativeSubjectId),
2699
+ eq(id, nativeSessionId),
2700
+ sql`${expiry} <= ${mapping.d1.engineNow}`,
2701
+ marker,
2702
+ ),
2703
+ ),
2704
+ ),
2705
+ );
2706
+
2707
+ const values = t.encodeInsert({
2708
+ subjectId: nativeSubjectId,
2709
+ sessionId: nativeSessionId,
2710
+ absoluteExpiresAt: replacement.tombstoneUntil,
2711
+ });
2712
+
2713
+ statements.push(
2714
+ yield* statement(database.insert(t.table).values(values)),
2715
+ yield* assertion(sql`changes() = 1`),
2716
+ );
2717
+
2718
+ const exact = and(
2719
+ eq(owner, nativeSubjectId),
2720
+ eq(id, nativeSessionId),
2721
+ eq(expiry, t.encodeInstant(replacement.tombstoneUntil)),
2722
+ )!;
2723
+
2724
+ post = sql`exists(select 1 from ${t.table} where ${exact})`;
2725
+ }
2726
+ statements.push(yield* assertion(and(marker, post, live)!));
2727
+
2728
+ return { receipt: prepare(replacement.inspection, journal), statements };
2729
+ }),
2730
+ mapping.isConstraintConflict,
2731
+ recover,
2732
+ ),
2733
+ };
2734
+ };
2735
+
2736
+ export const makeD1SessionStepUpServices = <
2737
+ Claims,
2738
+ Id,
2739
+ S extends AnySQLiteTable,
2740
+ C extends AnySQLiteTable,
2741
+ I extends AnySQLiteTable,
2742
+ Session extends AnySQLiteTable,
2743
+ T extends AnySQLiteTable,
2744
+ NativeId,
2745
+ NativeSessionId,
2746
+ >(
2747
+ database: Database,
2748
+ mapping: D1SessionStepUpMapping<NoInfer<Claims>, S, C, I, Session, T, NativeId, NativeSessionId>,
2749
+ target: Context.Service<Id, SessionStepUpPersistence<Claims>>,
2750
+ ) =>
2751
+ Effect.gen(function* () {
2752
+ const hooks = yield* LifecycleHooks;
2753
+ const raw = makeD1SessionStepUp<Claims>(mapping as any);
2754
+
2755
+ const run = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
2756
+ Effect.gen(function* () {
2757
+ if (
2758
+ (yield* Effect.serviceOption(CurrentD1SessionBatch))._tag === "Some" ||
2759
+ (yield* hasCommitScope)
2760
+ )
2761
+ return yield* unavailable();
2762
+
2763
+ return yield* effect.pipe(
2764
+ Effect.provideService(CurrentD1PlanningDatabase, database),
2765
+ Effect.provideService(LifecycleHooks, hooks),
2766
+ );
2767
+ });
2768
+
2769
+ const sessionStepUpPersistence = target.of({
2770
+ create: (input, now, prepare) => run(raw.create(input, now, prepare)),
2771
+ context: (input) => run(raw.context(input)),
2772
+ read: (input) => run(raw.read(input)),
2773
+ reject: (input, prepare) => run(raw.reject(input, prepare)),
2774
+ complete: (plan, prepare) => run(raw.complete(plan, prepare)),
2775
+ });
2776
+
2777
+ return { sessionStepUpPersistence };
2778
+ });
2779
+
2780
+ export function coordinateD1SessionStepUp<
2781
+ Claims,
2782
+ Id,
2783
+ S extends AnySQLiteTable,
2784
+ C extends AnySQLiteTable,
2785
+ I extends AnySQLiteTable,
2786
+ Session extends AnySQLiteTable,
2787
+ T extends AnySQLiteTable,
2788
+ NativeId,
2789
+ NativeSessionId,
2790
+ A,
2791
+ E,
2792
+ R,
2793
+ DatabaseError,
2794
+ DatabaseRequirements,
2795
+ >(
2796
+ acquire: Effect.Effect<Database, DatabaseError, DatabaseRequirements>,
2797
+ options: {
2798
+ readonly mapping: D1SessionStepUpMapping<
2799
+ NoInfer<Claims>,
2800
+ S,
2801
+ C,
2802
+ I,
2803
+ Session,
2804
+ T,
2805
+ NativeId,
2806
+ NativeSessionId
2807
+ >;
2808
+ readonly target: SuppliedService<Id, SessionStepUpPersistence<Claims>>;
2809
+ },
2810
+ body: Effect.Effect<A, E, R>,
2811
+ ): Effect.Effect<
2812
+ A,
2813
+ D1CoordinatorError<E> | DatabaseError,
2814
+ Exclude<R, Id | D1BatchStatements> | LifecycleHooks | DatabaseRequirements
2815
+ > {
2816
+ return Effect.flatMap(acquire, (database) =>
2817
+ coordinateD1SessionBatch(
2818
+ database,
2819
+ (batch) =>
2820
+ Effect.gen(function* () {
2821
+ const hooks = yield* LifecycleHooks;
2822
+ const raw = makeD1SessionStepUp<Claims>(options.mapping as any);
2823
+
2824
+ const run = <Out, Failure, Requirements>(
2825
+ effect: Effect.Effect<Out, Failure, Requirements>,
2826
+ ) =>
2827
+ batch.owner.run(
2828
+ effect.pipe(
2829
+ Effect.provideService(CurrentD1PlanningDatabase, database),
2830
+ Effect.provideService(LifecycleHooks, hooks),
2831
+ Effect.provideService(CurrentD1SessionBatch, batch),
2832
+ ),
2833
+ );
2834
+
2835
+ const sessionStepUpPersistence = options.target.of({
2836
+ create: (input, now, prepare) => run(raw.create(input, now, prepare)),
2837
+ context: (input) => run(raw.context(input)),
2838
+ read: (input) => run(raw.read(input)),
2839
+ reject: (input, prepare) => run(raw.reject(input, prepare)),
2840
+ complete: (plan, prepare) => run(raw.complete(plan, prepare)),
2841
+ });
2842
+
2843
+ return { sessionStepUpPersistence };
2844
+ }),
2845
+ (
2846
+ ownedServices: { readonly sessionStepUpPersistence: SessionStepUpPersistence<Claims> },
2847
+ batch,
2848
+ ) => {
2849
+ const provided = Context.make(options.target, ownedServices.sessionStepUpPersistence).pipe(
2850
+ Context.add(D1BatchStatements, batch.owner.collector),
2851
+ );
2852
+
2853
+ return Effect.provideContext(body, provided);
2854
+ },
2855
+ ),
2856
+ );
2857
+ }