@edge-base/server 0.1.1

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 (309) hide show
  1. package/admin-build/.gitkeep +0 -0
  2. package/admin-build/_app/env.js +1 -0
  3. package/admin-build/_app/immutable/assets/0.Bm6cF078.css +1 -0
  4. package/admin-build/_app/immutable/assets/1.BfW3pUNa.css +1 -0
  5. package/admin-build/_app/immutable/assets/11.CVmQOewb.css +1 -0
  6. package/admin-build/_app/immutable/assets/12.B1EhbRZT.css +1 -0
  7. package/admin-build/_app/immutable/assets/13.BvwYeuwE.css +1 -0
  8. package/admin-build/_app/immutable/assets/14.CdVfcO0R.css +1 -0
  9. package/admin-build/_app/immutable/assets/15.2yeZ66b-.css +1 -0
  10. package/admin-build/_app/immutable/assets/17.BVg0JEVu.css +1 -0
  11. package/admin-build/_app/immutable/assets/18.Rwnl3x_i.css +1 -0
  12. package/admin-build/_app/immutable/assets/20.DsPWA9AV.css +1 -0
  13. package/admin-build/_app/immutable/assets/21.Dz2RJ56c.css +1 -0
  14. package/admin-build/_app/immutable/assets/22.DwNLk5Ai.css +1 -0
  15. package/admin-build/_app/immutable/assets/23.CFpu0gOO.css +1 -0
  16. package/admin-build/_app/immutable/assets/24.Cy5LBeoJ.css +1 -0
  17. package/admin-build/_app/immutable/assets/25.pUyLVf-h.css +1 -0
  18. package/admin-build/_app/immutable/assets/26.DBcGrlXa.css +1 -0
  19. package/admin-build/_app/immutable/assets/27.BswYyAJD.css +1 -0
  20. package/admin-build/_app/immutable/assets/28.B4ueB1Kf.css +1 -0
  21. package/admin-build/_app/immutable/assets/29.B-qU6PdF.css +1 -0
  22. package/admin-build/_app/immutable/assets/3.Dg81Pgmd.css +1 -0
  23. package/admin-build/_app/immutable/assets/30.CsdWum94.css +1 -0
  24. package/admin-build/_app/immutable/assets/31.U6OwIp50.css +1 -0
  25. package/admin-build/_app/immutable/assets/4.CyawCCux.css +1 -0
  26. package/admin-build/_app/immutable/assets/5.C0YO2HTk.css +1 -0
  27. package/admin-build/_app/immutable/assets/8.Br5jd6kD.css +1 -0
  28. package/admin-build/_app/immutable/assets/Badge.EMYLHBxE.css +1 -0
  29. package/admin-build/_app/immutable/assets/Button.DpzMRTjK.css +1 -0
  30. package/admin-build/_app/immutable/assets/ConfirmDialog.DAnaWRRk.css +1 -0
  31. package/admin-build/_app/immutable/assets/EmptyState.CwKsu57Y.css +1 -0
  32. package/admin-build/_app/immutable/assets/Input.BDUSenmU.css +1 -0
  33. package/admin-build/_app/immutable/assets/Modal.Dm5B0Xie.css +1 -0
  34. package/admin-build/_app/immutable/assets/PageShell.CmU-Xh-b.css +1 -0
  35. package/admin-build/_app/immutable/assets/SchemaFieldEditor.g4NsCdno.css +1 -0
  36. package/admin-build/_app/immutable/assets/Select.BW4Keufm.css +1 -0
  37. package/admin-build/_app/immutable/assets/Skeleton.KWUulTKJ.css +1 -0
  38. package/admin-build/_app/immutable/assets/Tabs.CniGYb67.css +1 -0
  39. package/admin-build/_app/immutable/assets/TimeChart.BTCDAvmT.css +1 -0
  40. package/admin-build/_app/immutable/assets/Toggle.Cy_K12OM.css +1 -0
  41. package/admin-build/_app/immutable/assets/TopList.ClFzmPlA.css +1 -0
  42. package/admin-build/_app/immutable/chunks/7B47DvSx.js +1 -0
  43. package/admin-build/_app/immutable/chunks/7f08Id8e.js +1 -0
  44. package/admin-build/_app/immutable/chunks/8wJeQ7LN.js +1 -0
  45. package/admin-build/_app/immutable/chunks/B-h2afW5.js +1 -0
  46. package/admin-build/_app/immutable/chunks/B8vJP3wz.js +1 -0
  47. package/admin-build/_app/immutable/chunks/BR_fL5Yv.js +1 -0
  48. package/admin-build/_app/immutable/chunks/BY92tFS2.js +1 -0
  49. package/admin-build/_app/immutable/chunks/BcR-Rdj9.js +1 -0
  50. package/admin-build/_app/immutable/chunks/BdrwyZv8.js +1 -0
  51. package/admin-build/_app/immutable/chunks/Bh56EfQ_.js +1 -0
  52. package/admin-build/_app/immutable/chunks/BkrCkgYp.js +1 -0
  53. package/admin-build/_app/immutable/chunks/BmRjiP5k.js +1 -0
  54. package/admin-build/_app/immutable/chunks/BsokvhWC.js +1 -0
  55. package/admin-build/_app/immutable/chunks/C4D51vTW.js +1 -0
  56. package/admin-build/_app/immutable/chunks/C6puvcoR.js +2 -0
  57. package/admin-build/_app/immutable/chunks/CCKNu7m7.js +1 -0
  58. package/admin-build/_app/immutable/chunks/CWj6FrbW.js +1 -0
  59. package/admin-build/_app/immutable/chunks/Ce-ngf4p.js +5 -0
  60. package/admin-build/_app/immutable/chunks/Cs0GwzJA.js +1 -0
  61. package/admin-build/_app/immutable/chunks/CwROoZK0.js +1 -0
  62. package/admin-build/_app/immutable/chunks/CxCPv_Ut.js +1 -0
  63. package/admin-build/_app/immutable/chunks/CxbRue-5.js +1 -0
  64. package/admin-build/_app/immutable/chunks/CyqB6g-D.js +1 -0
  65. package/admin-build/_app/immutable/chunks/D5h5A1cc.js +2 -0
  66. package/admin-build/_app/immutable/chunks/DnyL7Zq-.js +1 -0
  67. package/admin-build/_app/immutable/chunks/DoPXzH7F.js +1 -0
  68. package/admin-build/_app/immutable/chunks/DrQSgw-f.js +1 -0
  69. package/admin-build/_app/immutable/chunks/DttM2zNO.js +1 -0
  70. package/admin-build/_app/immutable/chunks/DuXuUBWN.js +1 -0
  71. package/admin-build/_app/immutable/chunks/MdeqaOQx.js +10 -0
  72. package/admin-build/_app/immutable/chunks/NuUjtcO2.js +1 -0
  73. package/admin-build/_app/immutable/chunks/Q2nPFxS6.js +1 -0
  74. package/admin-build/_app/immutable/chunks/R6arueIl.js +1 -0
  75. package/admin-build/_app/immutable/chunks/UUazaC_N.js +1 -0
  76. package/admin-build/_app/immutable/chunks/cOYbrQxx.js +1 -0
  77. package/admin-build/_app/immutable/chunks/eFQHTGwA.js +1 -0
  78. package/admin-build/_app/immutable/chunks/ehbppgYb.js +1 -0
  79. package/admin-build/_app/immutable/chunks/glwixJlP.js +1 -0
  80. package/admin-build/_app/immutable/chunks/vApWTCBs.js +1 -0
  81. package/admin-build/_app/immutable/chunks/w89G9Xpi.js +1 -0
  82. package/admin-build/_app/immutable/chunks/wJsUhbfZ.js +1 -0
  83. package/admin-build/_app/immutable/chunks/zfauFM8P.js +1 -0
  84. package/admin-build/_app/immutable/entry/app.CcO-Uos3.js +2 -0
  85. package/admin-build/_app/immutable/entry/start.COebYq3I.js +1 -0
  86. package/admin-build/_app/immutable/nodes/0.CjtHKU-6.js +1 -0
  87. package/admin-build/_app/immutable/nodes/1.DEisjlM0.js +1 -0
  88. package/admin-build/_app/immutable/nodes/10.CvhdyWVB.js +1 -0
  89. package/admin-build/_app/immutable/nodes/11.DjHqcOvy.js +1 -0
  90. package/admin-build/_app/immutable/nodes/12.mQLz4Mj_.js +1 -0
  91. package/admin-build/_app/immutable/nodes/13.CBonZZyP.js +110 -0
  92. package/admin-build/_app/immutable/nodes/14.d-oiZL0j.js +3 -0
  93. package/admin-build/_app/immutable/nodes/15.CKPQsUYF.js +1 -0
  94. package/admin-build/_app/immutable/nodes/16.wPzAPQGx.js +1 -0
  95. package/admin-build/_app/immutable/nodes/17.DayhKyEZ.js +1 -0
  96. package/admin-build/_app/immutable/nodes/18.DKwS0Ir0.js +1 -0
  97. package/admin-build/_app/immutable/nodes/19.wPzAPQGx.js +1 -0
  98. package/admin-build/_app/immutable/nodes/2.BKoKrw1i.js +1 -0
  99. package/admin-build/_app/immutable/nodes/20.BvIkkkrW.js +1 -0
  100. package/admin-build/_app/immutable/nodes/21.DMaFhdHk.js +128 -0
  101. package/admin-build/_app/immutable/nodes/22.3xdgwuK1.js +1 -0
  102. package/admin-build/_app/immutable/nodes/23.8Bvgjbsl.js +112 -0
  103. package/admin-build/_app/immutable/nodes/24.DzSSzRhG.js +2 -0
  104. package/admin-build/_app/immutable/nodes/25.9KKYBnAE.js +2 -0
  105. package/admin-build/_app/immutable/nodes/26.Bhn9dfhY.js +1 -0
  106. package/admin-build/_app/immutable/nodes/27.kRLiC24G.js +1 -0
  107. package/admin-build/_app/immutable/nodes/28.BVIN1-7N.js +1 -0
  108. package/admin-build/_app/immutable/nodes/29.3yabZWj4.js +1 -0
  109. package/admin-build/_app/immutable/nodes/3.BFtSOkX7.js +2 -0
  110. package/admin-build/_app/immutable/nodes/30.CyCQlwaP.js +1 -0
  111. package/admin-build/_app/immutable/nodes/31.C4LDXjES.js +1 -0
  112. package/admin-build/_app/immutable/nodes/4.CvbiMlCa.js +1 -0
  113. package/admin-build/_app/immutable/nodes/5.C6BLv2eM.js +1 -0
  114. package/admin-build/_app/immutable/nodes/6.BcXvfl2P.js +1 -0
  115. package/admin-build/_app/immutable/nodes/7.CIuqhPiK.js +1 -0
  116. package/admin-build/_app/immutable/nodes/8.BQOR_JfO.js +1 -0
  117. package/admin-build/_app/immutable/nodes/9.NZqXQxPy.js +1 -0
  118. package/admin-build/_app/version.json +1 -0
  119. package/admin-build/favicon.svg +26 -0
  120. package/admin-build/index.html +45 -0
  121. package/openapi.json +19543 -0
  122. package/package.json +66 -0
  123. package/src/__tests__/admin-assets.test.ts +55 -0
  124. package/src/__tests__/admin-data-routes.test.ts +488 -0
  125. package/src/__tests__/admin-db-target.test.ts +103 -0
  126. package/src/__tests__/admin-routing.test.ts +31 -0
  127. package/src/__tests__/admin-user-management.test.ts +311 -0
  128. package/src/__tests__/analytics-query.test.ts +75 -0
  129. package/src/__tests__/auth-d1.test.ts +749 -0
  130. package/src/__tests__/auth-db-adapter.test.ts +73 -0
  131. package/src/__tests__/auth-jwt.test.ts +440 -0
  132. package/src/__tests__/auth-oauth.test.ts +389 -0
  133. package/src/__tests__/auth-password.test.ts +367 -0
  134. package/src/__tests__/auth-redirect.test.ts +87 -0
  135. package/src/__tests__/backup-restore.test.ts +711 -0
  136. package/src/__tests__/broadcast.test.ts +128 -0
  137. package/src/__tests__/cli.test.ts +178 -0
  138. package/src/__tests__/cloudflare-realtime.test.ts +113 -0
  139. package/src/__tests__/config.test.ts +469 -0
  140. package/src/__tests__/cors.test.ts +154 -0
  141. package/src/__tests__/cron.test.ts +302 -0
  142. package/src/__tests__/d1-handler.test.ts +402 -0
  143. package/src/__tests__/d1-sql.test.ts +120 -0
  144. package/src/__tests__/database-live-config.test.ts +42 -0
  145. package/src/__tests__/database-live-emitter.test.ts +56 -0
  146. package/src/__tests__/database-live-filters.test.ts +63 -0
  147. package/src/__tests__/database-live-route.test.ts +113 -0
  148. package/src/__tests__/db-sql.test.ts +163 -0
  149. package/src/__tests__/do-lifecycle.test.ts +263 -0
  150. package/src/__tests__/do-router.test.ts +729 -0
  151. package/src/__tests__/email-provider.test.ts +128 -0
  152. package/src/__tests__/email-templates.test.ts +528 -0
  153. package/src/__tests__/error-format.test.ts +250 -0
  154. package/src/__tests__/field-ops.test.ts +242 -0
  155. package/src/__tests__/functions-context.test.ts +334 -0
  156. package/src/__tests__/functions-d1-proxy.test.ts +229 -0
  157. package/src/__tests__/functions-registry-runtime-config.test.ts +17 -0
  158. package/src/__tests__/functions-route.test.ts +139 -0
  159. package/src/__tests__/internal-request.test.ts +77 -0
  160. package/src/__tests__/log-writer.test.ts +44 -0
  161. package/src/__tests__/logger.test.ts +58 -0
  162. package/src/__tests__/meta-admin-proxy.test.ts +48 -0
  163. package/src/__tests__/meta-export-coverage.test.ts +191 -0
  164. package/src/__tests__/meta-route-registration.test.ts +47 -0
  165. package/src/__tests__/namespace-dump.test.ts +28 -0
  166. package/src/__tests__/oauth-providers.test.ts +337 -0
  167. package/src/__tests__/openapi-coverage.test.ts +144 -0
  168. package/src/__tests__/pagination.test.ts +59 -0
  169. package/src/__tests__/password-policy.test.ts +191 -0
  170. package/src/__tests__/plugin-migrations.test.ts +379 -0
  171. package/src/__tests__/postgres-batch-compat.test.ts +133 -0
  172. package/src/__tests__/postgres-dialect.test.ts +328 -0
  173. package/src/__tests__/postgres-executor.test.ts +79 -0
  174. package/src/__tests__/postgres-field-ops-compat.test.ts +222 -0
  175. package/src/__tests__/postgres-schema-init.test.ts +105 -0
  176. package/src/__tests__/postgres-table-utils.test.ts +107 -0
  177. package/src/__tests__/presence.test.ts +199 -0
  178. package/src/__tests__/provider.test.ts +550 -0
  179. package/src/__tests__/public-user-profile.test.ts +339 -0
  180. package/src/__tests__/push-handlers.test.ts +179 -0
  181. package/src/__tests__/push-provider.test.ts +80 -0
  182. package/src/__tests__/push-token.test.ts +418 -0
  183. package/src/__tests__/query.test.ts +771 -0
  184. package/src/__tests__/rate-limit.test.ts +260 -0
  185. package/src/__tests__/room-access-policy.test.ts +101 -0
  186. package/src/__tests__/room-handler-context.test.ts +130 -0
  187. package/src/__tests__/room-monitoring.test.ts +138 -0
  188. package/src/__tests__/room-runtime-routing.test.ts +222 -0
  189. package/src/__tests__/room.test.ts +254 -0
  190. package/src/__tests__/route-parser.test.ts +490 -0
  191. package/src/__tests__/rules.test.ts +234 -0
  192. package/src/__tests__/runtime-surface-accounting.test.ts +120 -0
  193. package/src/__tests__/scheduled.test.ts +80 -0
  194. package/src/__tests__/schema.test.ts +1273 -0
  195. package/src/__tests__/security-hardening.test.ts +312 -0
  196. package/src/__tests__/server.unit.test.ts +333 -0
  197. package/src/__tests__/service-key-db-proxy.test.ts +650 -0
  198. package/src/__tests__/service-key-provider-bypass.test.ts +138 -0
  199. package/src/__tests__/service-key.test.ts +757 -0
  200. package/src/__tests__/smoke-skip-report.test.ts +72 -0
  201. package/src/__tests__/sms-provider.test.ts +39 -0
  202. package/src/__tests__/sql-route.test.ts +218 -0
  203. package/src/__tests__/storage-hook-context.test.ts +115 -0
  204. package/src/__tests__/totp.test.ts +200 -0
  205. package/src/__tests__/uuid.test.ts +144 -0
  206. package/src/__tests__/validation.test.ts +773 -0
  207. package/src/__tests__/websocket-pending.test.ts +163 -0
  208. package/src/_functions-registry.ts +51 -0
  209. package/src/bench-entry.ts +9 -0
  210. package/src/cloudflare-test.d.ts +1 -0
  211. package/src/durable-objects/auth-do.ts +49 -0
  212. package/src/durable-objects/database-do.ts +2240 -0
  213. package/src/durable-objects/database-live-do.ts +949 -0
  214. package/src/durable-objects/logs-do.ts +1200 -0
  215. package/src/durable-objects/room-runtime-base.ts +1604 -0
  216. package/src/durable-objects/rooms-do.ts +2191 -0
  217. package/src/generated-config.ts +6 -0
  218. package/src/index.ts +382 -0
  219. package/src/lib/admin-assets.ts +54 -0
  220. package/src/lib/admin-db-target.ts +301 -0
  221. package/src/lib/admin-routing.ts +35 -0
  222. package/src/lib/admin-user-management.ts +464 -0
  223. package/src/lib/analytics-adapter.ts +103 -0
  224. package/src/lib/analytics-query.ts +579 -0
  225. package/src/lib/auth-d1-service.ts +1193 -0
  226. package/src/lib/auth-d1.ts +1056 -0
  227. package/src/lib/auth-db-adapter.ts +289 -0
  228. package/src/lib/auth-redirect.ts +116 -0
  229. package/src/lib/cidr.ts +115 -0
  230. package/src/lib/client-ip.ts +51 -0
  231. package/src/lib/cloudflare-realtime.ts +251 -0
  232. package/src/lib/control-db.ts +36 -0
  233. package/src/lib/cron.ts +163 -0
  234. package/src/lib/d1-handler.ts +1425 -0
  235. package/src/lib/d1-schema-init.ts +255 -0
  236. package/src/lib/d1-sql.ts +33 -0
  237. package/src/lib/database-live-config.ts +24 -0
  238. package/src/lib/database-live-emitter.ts +111 -0
  239. package/src/lib/db-sql.ts +66 -0
  240. package/src/lib/do-retry.ts +36 -0
  241. package/src/lib/do-router.ts +270 -0
  242. package/src/lib/do-sql.ts +73 -0
  243. package/src/lib/email-provider.ts +379 -0
  244. package/src/lib/email-templates.ts +285 -0
  245. package/src/lib/email-translations.ts +422 -0
  246. package/src/lib/errors.ts +151 -0
  247. package/src/lib/functions.ts +2091 -0
  248. package/src/lib/hono.ts +56 -0
  249. package/src/lib/internal-request.ts +56 -0
  250. package/src/lib/jwt.ts +354 -0
  251. package/src/lib/log-writer.ts +272 -0
  252. package/src/lib/namespace-dump.ts +125 -0
  253. package/src/lib/oauth-providers.ts +1225 -0
  254. package/src/lib/op-parser.ts +99 -0
  255. package/src/lib/openapi.ts +146 -0
  256. package/src/lib/pagination.ts +19 -0
  257. package/src/lib/password-policy.ts +102 -0
  258. package/src/lib/password.ts +145 -0
  259. package/src/lib/plugin-migrations.ts +612 -0
  260. package/src/lib/postgres-executor.ts +203 -0
  261. package/src/lib/postgres-handler.ts +1102 -0
  262. package/src/lib/postgres-schema-init.ts +341 -0
  263. package/src/lib/postgres-table-utils.ts +87 -0
  264. package/src/lib/public-user-profile.ts +187 -0
  265. package/src/lib/push-provider.ts +409 -0
  266. package/src/lib/push-token.ts +294 -0
  267. package/src/lib/query-engine.ts +768 -0
  268. package/src/lib/room-monitoring.ts +97 -0
  269. package/src/lib/room-runtime.ts +14 -0
  270. package/src/lib/route-parser.ts +434 -0
  271. package/src/lib/schema.ts +538 -0
  272. package/src/lib/schemas.ts +152 -0
  273. package/src/lib/service-key.ts +419 -0
  274. package/src/lib/sms-provider.ts +230 -0
  275. package/src/lib/startup-config.ts +99 -0
  276. package/src/lib/totp.ts +242 -0
  277. package/src/lib/uuid.ts +87 -0
  278. package/src/lib/validation.ts +205 -0
  279. package/src/lib/version.ts +2 -0
  280. package/src/lib/websocket-pending.ts +40 -0
  281. package/src/middleware/auth.ts +169 -0
  282. package/src/middleware/captcha-verify.ts +217 -0
  283. package/src/middleware/cors.ts +159 -0
  284. package/src/middleware/error-handler.ts +54 -0
  285. package/src/middleware/internal-guard.ts +26 -0
  286. package/src/middleware/logger.ts +126 -0
  287. package/src/middleware/rate-limit.ts +283 -0
  288. package/src/middleware/rules.ts +475 -0
  289. package/src/routes/admin-auth.ts +447 -0
  290. package/src/routes/admin.ts +3501 -0
  291. package/src/routes/analytics-api.ts +290 -0
  292. package/src/routes/auth.ts +4222 -0
  293. package/src/routes/backup.ts +1466 -0
  294. package/src/routes/config.ts +53 -0
  295. package/src/routes/d1.ts +109 -0
  296. package/src/routes/database-live.ts +281 -0
  297. package/src/routes/functions.ts +155 -0
  298. package/src/routes/health.ts +32 -0
  299. package/src/routes/kv.ts +167 -0
  300. package/src/routes/oauth.ts +1055 -0
  301. package/src/routes/push.ts +1465 -0
  302. package/src/routes/room.ts +639 -0
  303. package/src/routes/schema-endpoint.ts +76 -0
  304. package/src/routes/sql.ts +176 -0
  305. package/src/routes/storage.ts +1674 -0
  306. package/src/routes/tables.ts +699 -0
  307. package/src/routes/users.ts +21 -0
  308. package/src/routes/vectorize.ts +372 -0
  309. package/src/types.ts +99 -0
@@ -0,0 +1,1193 @@
1
+ /**
2
+ * auth-d1-service.ts — Auth service layer (supports D1 + PostgreSQL backends)
3
+ *
4
+ * Uses the AuthDb adapter interface for provider-agnostic database access.
5
+ * All SQL uses `?` bind params — the adapter converts to `$1, $2, ...` for PostgreSQL.
6
+ *
7
+ * Key patterns:
8
+ * - No `RETURNING *` — re-fetch after INSERT/UPDATE (portable across D1/pg)
9
+ * - `db.batch()` → atomic transaction (D1 batch / pg BEGIN/COMMIT)
10
+ * - `datetime('now')` replaced with JS `new Date().toISOString()` (portable)
11
+ * - `INSERT OR IGNORE` → adapter converts to `ON CONFLICT DO NOTHING` for pg
12
+ *
13
+ * All functions are async (both D1 and pg are HTTP-based).
14
+ */
15
+
16
+ import type { AuthDb } from './auth-db-adapter.js';
17
+
18
+ // ─── Types ───
19
+
20
+ export interface CreateUserInput {
21
+ userId: string;
22
+ email: string | null;
23
+ passwordHash: string;
24
+ displayName?: string | null;
25
+ avatarUrl?: string | null;
26
+ emailVisibility?: string;
27
+ role?: string;
28
+ verified?: boolean;
29
+ locale?: string;
30
+ metadata?: Record<string, unknown> | null;
31
+ appMetadata?: Record<string, unknown> | null;
32
+ }
33
+
34
+ export interface UpdateUserInput {
35
+ email?: string;
36
+ passwordHash?: string;
37
+ displayName?: string | null;
38
+ avatarUrl?: string | null;
39
+ emailVisibility?: string;
40
+ role?: string;
41
+ verified?: boolean | number;
42
+ isAnonymous?: boolean | number;
43
+ customClaims?: Record<string, unknown> | string | null;
44
+ phone?: string | null;
45
+ phoneVerified?: boolean | number;
46
+ metadata?: Record<string, unknown> | string | null;
47
+ appMetadata?: Record<string, unknown> | string | null;
48
+ disabled?: boolean | number;
49
+ status?: string;
50
+ locale?: string;
51
+ }
52
+
53
+ export interface CreateSessionInput {
54
+ id: string;
55
+ userId: string;
56
+ refreshToken: string;
57
+ expiresAt: string;
58
+ metadata?: string | null;
59
+ previousRefreshToken?: string | null;
60
+ rotatedAt?: string | null;
61
+ }
62
+
63
+ export interface CreateOAuthAccountInput {
64
+ id: string;
65
+ userId: string;
66
+ provider: string;
67
+ providerUserId: string;
68
+ }
69
+
70
+ export interface CreateEmailTokenInput {
71
+ token: string;
72
+ userId: string;
73
+ type: string;
74
+ expiresAt: string;
75
+ }
76
+
77
+ export interface CreateMfaFactorInput {
78
+ id: string;
79
+ userId: string;
80
+ type?: string;
81
+ secret: string;
82
+ }
83
+
84
+ export interface CreateWebAuthnCredentialInput {
85
+ id: string;
86
+ userId: string;
87
+ credentialId: string;
88
+ credentialPublicKey: string;
89
+ counter?: number;
90
+ transports?: string | null;
91
+ }
92
+
93
+ // ─── A. User CRUD ───
94
+
95
+ /**
96
+ * Create a new user record.
97
+ * INSERT + re-fetch (portable, works on both D1 and pg).
98
+ */
99
+ export async function createUser(
100
+ db: AuthDb,
101
+ input: CreateUserInput,
102
+ ): Promise<Record<string, unknown>> {
103
+ const now = new Date().toISOString();
104
+ const metadataStr = input.metadata ? JSON.stringify(input.metadata) : null;
105
+ const appMetadataStr = input.appMetadata ? JSON.stringify(input.appMetadata) : null;
106
+
107
+ await db.run(
108
+ `INSERT INTO _users (id, email, passwordHash, displayName, avatarUrl, emailVisibility, role, verified, isAnonymous, locale, metadata, appMetadata, createdAt, updatedAt)
109
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?)`,
110
+ [
111
+ input.userId,
112
+ input.email,
113
+ input.passwordHash,
114
+ input.displayName ?? null,
115
+ input.avatarUrl ?? null,
116
+ input.emailVisibility ?? 'private',
117
+ input.role ?? 'user',
118
+ input.verified ? 1 : 0,
119
+ input.locale ?? 'en',
120
+ metadataStr,
121
+ appMetadataStr,
122
+ now,
123
+ now,
124
+ ],
125
+ );
126
+
127
+ // Re-fetch
128
+ const user = await db.first(`SELECT * FROM _users WHERE id = ?`, [input.userId]);
129
+ return user as Record<string, unknown>;
130
+ }
131
+
132
+ /**
133
+ * Create an anonymous user record.
134
+ * INSERT + re-fetch.
135
+ */
136
+ export async function createAnonymousUser(
137
+ db: AuthDb,
138
+ userId: string,
139
+ ): Promise<Record<string, unknown>> {
140
+ const now = new Date().toISOString();
141
+
142
+ await db.run(
143
+ `INSERT INTO _users (id, email, passwordHash, displayName, avatarUrl, emailVisibility, role, verified, isAnonymous, createdAt, updatedAt)
144
+ VALUES (?, NULL, NULL, NULL, NULL, 'private', 'user', 0, 1, ?, ?)`,
145
+ [userId, now, now],
146
+ );
147
+
148
+ // Re-fetch
149
+ const user = await db.first(`SELECT * FROM _users WHERE id = ?`, [userId]);
150
+ return user as Record<string, unknown>;
151
+ }
152
+
153
+ /**
154
+ * Get user by ID.
155
+ */
156
+ export async function getUserById(
157
+ db: AuthDb,
158
+ userId: string,
159
+ ): Promise<Record<string, unknown> | null> {
160
+ return await db.first(`SELECT * FROM _users WHERE id = ?`, [userId]);
161
+ }
162
+
163
+ /**
164
+ * Get user by email.
165
+ */
166
+ export async function getUserByEmail(
167
+ db: AuthDb,
168
+ email: string,
169
+ ): Promise<Record<string, unknown> | null> {
170
+ return await db.first(`SELECT * FROM _users WHERE email = ?`, [email]);
171
+ }
172
+
173
+ /**
174
+ * Get user by phone.
175
+ */
176
+ export async function getUserByPhone(
177
+ db: AuthDb,
178
+ phone: string,
179
+ ): Promise<Record<string, unknown> | null> {
180
+ return await db.first(`SELECT * FROM _users WHERE phone = ?`, [phone]);
181
+ }
182
+
183
+ /**
184
+ * Update user with dynamic fields.
185
+ * Builds SET clause dynamically. Re-fetches after UPDATE.
186
+ */
187
+ export async function updateUser(
188
+ db: AuthDb,
189
+ userId: string,
190
+ updates: UpdateUserInput,
191
+ ): Promise<Record<string, unknown> | null> {
192
+ // Allowlist of _users columns that can be updated
193
+ const ALLOWED_COLUMNS = new Set([
194
+ 'email', 'passwordHash', 'displayName', 'avatarUrl', 'emailVisibility',
195
+ 'role', 'status', 'verified', 'isAnonymous', 'locale', 'metadata', 'appMetadata',
196
+ 'customClaims', 'phone', 'phoneVerified', 'disabled', 'bannedUntil',
197
+ 'lastSignInAt', 'updatedAt',
198
+ ]);
199
+
200
+ const sets: string[] = [];
201
+ const params: unknown[] = [];
202
+
203
+ const normalizeRoleValue = (value: unknown): string | null => {
204
+ if (typeof value !== 'string') return null;
205
+ const normalized = value.trim();
206
+ return normalized.length > 0 ? normalized : null;
207
+ };
208
+
209
+ for (const [key, value] of Object.entries(updates)) {
210
+ if (key === 'id' || key === 'createdAt') continue;
211
+ // Skip unknown columns to prevent errors from arbitrary input
212
+ if (!ALLOWED_COLUMNS.has(key)) continue;
213
+ // Skip non-bindable values (Symbol, Function, etc.)
214
+ if (typeof value === 'symbol' || typeof value === 'function') continue;
215
+
216
+ // Enum validation: reject invalid values for constrained fields
217
+ if (key === 'status' && !['active', 'suspended', 'banned', 'disabled'].includes(value as string)) continue;
218
+ if (key === 'emailVisibility' && !['public', 'private'].includes(value as string)) continue;
219
+
220
+ if (key === 'role') {
221
+ const normalizedRole = normalizeRoleValue(value);
222
+ if (!normalizedRole) continue;
223
+ sets.push('"role" = ?');
224
+ params.push(normalizedRole);
225
+ continue;
226
+ }
227
+
228
+ // JSON fields: serialize to string
229
+ if ((key === 'customClaims' || key === 'metadata' || key === 'appMetadata') && value !== null && typeof value === 'object') {
230
+ sets.push(`"${key}" = ?`);
231
+ params.push(JSON.stringify(value));
232
+ }
233
+ // Boolean fields: convert to integer
234
+ else if ((key === 'verified' || key === 'isAnonymous' || key === 'phoneVerified' || key === 'disabled') && typeof value === 'boolean') {
235
+ sets.push(`"${key}" = ?`);
236
+ params.push(value ? 1 : 0);
237
+ }
238
+ else {
239
+ sets.push(`"${key}" = ?`);
240
+ params.push(value);
241
+ }
242
+ }
243
+
244
+ if (sets.length === 0) return null;
245
+
246
+ const now = new Date().toISOString();
247
+ sets.push('"updatedAt" = ?');
248
+ params.push(now);
249
+ params.push(userId);
250
+
251
+ await db.run(
252
+ `UPDATE _users SET ${sets.join(', ')} WHERE "id" = ?`,
253
+ params,
254
+ );
255
+
256
+ // Re-fetch
257
+ return await db.first(`SELECT * FROM _users WHERE id = ?`, [userId]);
258
+ }
259
+
260
+ /**
261
+ * Delete user and all related records (cascade).
262
+ * Uses db.batch() for atomic transaction.
263
+ * Returns cleanup info for index cleanup.
264
+ */
265
+ export async function deleteUserCascade(
266
+ db: AuthDb,
267
+ userId: string,
268
+ ): Promise<{
269
+ email: string | null;
270
+ phone: string | null;
271
+ oauthAccounts: Array<{ provider: string; providerUserId: string }>;
272
+ }> {
273
+ // First, gather data needed for external cleanup before deleting
274
+ const user = await db.first<{ email: string | null; phone: string | null }>(
275
+ `SELECT email, phone FROM _users WHERE id = ?`,
276
+ [userId],
277
+ );
278
+
279
+ const oauthAccounts = await db.query<{ provider: string; providerUserId: string }>(
280
+ `SELECT provider, providerUserId FROM _oauth_accounts WHERE userId = ?`,
281
+ [userId],
282
+ );
283
+
284
+ // Batch delete all related records + user
285
+ await db.batch([
286
+ { sql: `DELETE FROM _email_tokens WHERE userId = ?`, params: [userId] },
287
+ { sql: `DELETE FROM _sessions WHERE userId = ?`, params: [userId] },
288
+ { sql: `DELETE FROM _oauth_accounts WHERE userId = ?`, params: [userId] },
289
+ { sql: `DELETE FROM _mfa_recovery_codes WHERE userId = ?`, params: [userId] },
290
+ { sql: `DELETE FROM _mfa_factors WHERE userId = ?`, params: [userId] },
291
+ { sql: `DELETE FROM _webauthn_credentials WHERE userId = ?`, params: [userId] },
292
+ { sql: `DELETE FROM _users WHERE id = ?`, params: [userId] },
293
+ ]);
294
+
295
+ return {
296
+ email: user?.email ?? null,
297
+ phone: user?.phone ?? null,
298
+ oauthAccounts: oauthAccounts.map((a) => ({
299
+ provider: a.provider,
300
+ providerUserId: a.providerUserId,
301
+ })),
302
+ };
303
+ }
304
+
305
+ /**
306
+ * List users with pagination.
307
+ */
308
+ export async function listUsers(
309
+ db: AuthDb,
310
+ limit: number,
311
+ offset: number,
312
+ ): Promise<{ users: Record<string, unknown>[]; total: number }> {
313
+ const countResult = await db.first<{ cnt: number }>(
314
+ `SELECT COUNT(*) as cnt FROM _users`,
315
+ );
316
+ const total = countResult?.cnt ?? 0;
317
+
318
+ const users = await db.query(
319
+ `SELECT * FROM _users ORDER BY createdAt DESC LIMIT ? OFFSET ?`,
320
+ [limit, offset],
321
+ );
322
+
323
+ return { users, total };
324
+ }
325
+
326
+ /**
327
+ * Batch get users by IDs.
328
+ */
329
+ export async function batchGetUsers(
330
+ db: AuthDb,
331
+ userIds: string[],
332
+ ): Promise<Record<string, unknown>[]> {
333
+ if (userIds.length === 0) return [];
334
+
335
+ const placeholders = userIds.map(() => '?').join(', ');
336
+ return await db.query(
337
+ `SELECT * FROM _users WHERE id IN (${placeholders})`,
338
+ userIds,
339
+ );
340
+ }
341
+
342
+ // ─── B. Session CRUD ───
343
+
344
+ /**
345
+ * Create a new session.
346
+ * INSERT + re-fetch.
347
+ */
348
+ export async function createSession(
349
+ db: AuthDb,
350
+ input: CreateSessionInput,
351
+ ): Promise<Record<string, unknown>> {
352
+ const now = new Date().toISOString();
353
+
354
+ await db.run(
355
+ `INSERT INTO _sessions (id, userId, refreshToken, previousRefreshToken, rotatedAt, expiresAt, createdAt, metadata)
356
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
357
+ [
358
+ input.id,
359
+ input.userId,
360
+ input.refreshToken,
361
+ input.previousRefreshToken ?? null,
362
+ input.rotatedAt ?? null,
363
+ input.expiresAt,
364
+ now,
365
+ input.metadata ?? null,
366
+ ],
367
+ );
368
+
369
+ // Re-fetch
370
+ const session = await db.first(`SELECT * FROM _sessions WHERE id = ?`, [input.id]);
371
+ return session as Record<string, unknown>;
372
+ }
373
+
374
+ /**
375
+ * Get session by refresh token.
376
+ * Also checks previousRefreshToken for grace period handling.
377
+ * Returns { session, matchType: 'current' | 'previous' | null }
378
+ */
379
+ export async function getSessionByRefreshToken(
380
+ db: AuthDb,
381
+ token: string,
382
+ userId: string,
383
+ ): Promise<{ session: Record<string, unknown>; matchType: 'current' | 'previous' } | null> {
384
+ // Step 1: Check current refreshToken match
385
+ const currentSession = await db.first(
386
+ `SELECT * FROM _sessions WHERE refreshToken = ? AND userId = ?`,
387
+ [token, userId],
388
+ );
389
+
390
+ if (currentSession) {
391
+ return { session: currentSession, matchType: 'current' };
392
+ }
393
+
394
+ // Step 2: Check previousRefreshToken (Grace Period)
395
+ const prevSession = await db.first(
396
+ `SELECT * FROM _sessions WHERE previousRefreshToken = ? AND userId = ?`,
397
+ [token, userId],
398
+ );
399
+
400
+ if (prevSession) {
401
+ return { session: prevSession, matchType: 'previous' };
402
+ }
403
+
404
+ return null;
405
+ }
406
+
407
+ /**
408
+ * Rotate refresh token on a session.
409
+ * current -> previous, new -> current.
410
+ *
411
+ * Uses dialect-aware SQL for json_set (SQLite) vs jsonb_set (PostgreSQL).
412
+ */
413
+ export async function rotateRefreshToken(
414
+ db: AuthDb,
415
+ sessionId: string,
416
+ newRefreshToken: string,
417
+ oldRefreshToken: string,
418
+ newExpiresAt: string,
419
+ ): Promise<void> {
420
+ const now = new Date().toISOString();
421
+
422
+ if (db.dialect === 'postgres') {
423
+ // PostgreSQL: use jsonb_set + to_jsonb for JSONB column
424
+ await db.run(
425
+ `UPDATE _sessions SET "refreshToken" = ?, "previousRefreshToken" = ?, "rotatedAt" = ?, "expiresAt" = ?, metadata = jsonb_set(COALESCE(metadata::jsonb, '{}'), '{lastActiveAt}', to_jsonb(?::text))::text WHERE id = ?`,
426
+ [newRefreshToken, oldRefreshToken, now, newExpiresAt, now, sessionId],
427
+ );
428
+ } else {
429
+ // SQLite (D1): use json_set
430
+ await db.run(
431
+ `UPDATE _sessions SET refreshToken = ?, previousRefreshToken = ?, rotatedAt = ?, expiresAt = ?, metadata = json_set(COALESCE(metadata, '{}'), '$.lastActiveAt', ?) WHERE id = ?`,
432
+ [newRefreshToken, oldRefreshToken, now, newExpiresAt, now, sessionId],
433
+ );
434
+ }
435
+ }
436
+
437
+ /**
438
+ * Delete a single session by ID.
439
+ */
440
+ export async function deleteSession(
441
+ db: AuthDb,
442
+ sessionId: string,
443
+ ): Promise<void> {
444
+ await db.run(`DELETE FROM _sessions WHERE id = ?`, [sessionId]);
445
+ }
446
+
447
+ /**
448
+ * Delete a session by refresh token (signout).
449
+ * Also checks previousRefreshToken for grace period cleanup.
450
+ */
451
+ export async function deleteSessionByRefreshToken(
452
+ db: AuthDb,
453
+ refreshToken: string,
454
+ ): Promise<void> {
455
+ await db.batch([
456
+ { sql: `DELETE FROM _sessions WHERE refreshToken = ?`, params: [refreshToken] },
457
+ { sql: `DELETE FROM _sessions WHERE previousRefreshToken = ?`, params: [refreshToken] },
458
+ ]);
459
+ }
460
+
461
+ /**
462
+ * Find session userId by refresh token (for signout hooks).
463
+ */
464
+ export async function findSessionUserByRefreshToken(
465
+ db: AuthDb,
466
+ refreshToken: string,
467
+ ): Promise<string | null> {
468
+ const session = await db.first<{ userId: string }>(
469
+ `SELECT userId FROM _sessions WHERE refreshToken = ? OR previousRefreshToken = ?`,
470
+ [refreshToken, refreshToken],
471
+ );
472
+
473
+ return session?.userId ?? null;
474
+ }
475
+
476
+ /**
477
+ * Delete all sessions for a user.
478
+ */
479
+ export async function deleteAllUserSessions(
480
+ db: AuthDb,
481
+ userId: string,
482
+ ): Promise<void> {
483
+ await db.run(`DELETE FROM _sessions WHERE userId = ?`, [userId]);
484
+ }
485
+
486
+ /**
487
+ * List sessions for a user.
488
+ * Parses metadata JSON for ip, userAgent, lastActiveAt.
489
+ */
490
+ export async function listUserSessions(
491
+ db: AuthDb,
492
+ userId: string,
493
+ ): Promise<Array<{
494
+ id: string;
495
+ createdAt: string;
496
+ expiresAt: string;
497
+ ip: string | null;
498
+ userAgent: string | null;
499
+ lastActiveAt: string | null;
500
+ }>> {
501
+ const results = await db.query<{ id: string; createdAt: string; expiresAt: string; metadata: string | null }>(
502
+ `SELECT id, createdAt, expiresAt, metadata FROM _sessions WHERE userId = ? ORDER BY createdAt DESC`,
503
+ [userId],
504
+ );
505
+
506
+ return results.map((s) => {
507
+ const meta = s.metadata ? JSON.parse(s.metadata) : {};
508
+ return {
509
+ id: s.id,
510
+ createdAt: s.createdAt,
511
+ expiresAt: s.expiresAt,
512
+ ip: meta.ip || null,
513
+ userAgent: meta.userAgent || null,
514
+ lastActiveAt: meta.lastActiveAt || null,
515
+ };
516
+ });
517
+ }
518
+
519
+ /**
520
+ * Delete a session for a specific user (ownership check).
521
+ */
522
+ export async function deleteSessionForUser(
523
+ db: AuthDb,
524
+ sessionId: string,
525
+ userId: string,
526
+ ): Promise<void> {
527
+ await db.run(
528
+ `DELETE FROM _sessions WHERE id = ? AND userId = ?`,
529
+ [sessionId, userId],
530
+ );
531
+ }
532
+
533
+ /**
534
+ * Clean expired sessions.
535
+ * Uses JS-computed timestamp (portable across D1/pg).
536
+ */
537
+ export async function cleanExpiredSessions(
538
+ db: AuthDb,
539
+ ): Promise<void> {
540
+ const now = new Date().toISOString();
541
+ await db.run(`DELETE FROM _sessions WHERE expiresAt < ?`, [now]);
542
+ }
543
+
544
+ /**
545
+ * Clean expired sessions for a specific user (lazy cleanup).
546
+ */
547
+ export async function cleanExpiredSessionsForUser(
548
+ db: AuthDb,
549
+ userId: string,
550
+ ): Promise<void> {
551
+ const now = new Date().toISOString();
552
+ await db.run(
553
+ `DELETE FROM _sessions WHERE userId = ? AND expiresAt < ?`,
554
+ [userId, now],
555
+ );
556
+ }
557
+
558
+ /**
559
+ * Evict oldest sessions if maxActiveSessions is exceeded.
560
+ */
561
+ export async function evictOldestSessions(
562
+ db: AuthDb,
563
+ userId: string,
564
+ maxSessions: number,
565
+ ): Promise<void> {
566
+ if (maxSessions <= 0) return;
567
+
568
+ const countResult = await db.first<{ cnt: number }>(
569
+ `SELECT COUNT(*) as cnt FROM _sessions WHERE userId = ?`,
570
+ [userId],
571
+ );
572
+ const currentCount = countResult?.cnt ?? 0;
573
+
574
+ if (currentCount >= maxSessions) {
575
+ const excess = currentCount - maxSessions + 1;
576
+ await db.run(
577
+ `DELETE FROM _sessions WHERE id IN (SELECT id FROM _sessions WHERE userId = ? ORDER BY createdAt ASC LIMIT ?)`,
578
+ [userId, excess],
579
+ );
580
+ }
581
+ }
582
+
583
+ // ─── C. OAuth ───
584
+
585
+ /**
586
+ * Create an OAuth account link.
587
+ * Uses INSERT OR IGNORE (adapter converts to ON CONFLICT DO NOTHING for pg).
588
+ */
589
+ export async function createOAuthAccount(
590
+ db: AuthDb,
591
+ input: CreateOAuthAccountInput,
592
+ ): Promise<void> {
593
+ const now = new Date().toISOString();
594
+
595
+ await db.run(
596
+ `INSERT OR IGNORE INTO _oauth_accounts (id, userId, provider, providerUserId, createdAt)
597
+ VALUES (?, ?, ?, ?, ?)`,
598
+ [input.id, input.userId, input.provider, input.providerUserId, now],
599
+ );
600
+ }
601
+
602
+ /**
603
+ * Get OAuth account by provider + providerUserId.
604
+ */
605
+ export async function getOAuthAccount(
606
+ db: AuthDb,
607
+ provider: string,
608
+ providerUserId: string,
609
+ ): Promise<Record<string, unknown> | null> {
610
+ return await db.first(
611
+ `SELECT * FROM _oauth_accounts WHERE provider = ? AND providerUserId = ?`,
612
+ [provider, providerUserId],
613
+ );
614
+ }
615
+
616
+ /**
617
+ * List OAuth accounts for a user.
618
+ */
619
+ export async function listOAuthAccounts(
620
+ db: AuthDb,
621
+ userId: string,
622
+ ): Promise<Record<string, unknown>[]> {
623
+ return await db.query(
624
+ `SELECT * FROM _oauth_accounts WHERE userId = ?`,
625
+ [userId],
626
+ );
627
+ }
628
+
629
+ /**
630
+ * Delete an OAuth account by ID.
631
+ */
632
+ export async function deleteOAuthAccount(
633
+ db: AuthDb,
634
+ id: string,
635
+ ): Promise<void> {
636
+ await db.run(`DELETE FROM _oauth_accounts WHERE id = ?`, [id]);
637
+ }
638
+
639
+ /**
640
+ * Delete an OAuth account by provider + providerUserId.
641
+ */
642
+ export async function deleteOAuthAccountByProvider(
643
+ db: AuthDb,
644
+ provider: string,
645
+ providerUserId: string,
646
+ ): Promise<void> {
647
+ await db.run(
648
+ `DELETE FROM _oauth_accounts WHERE provider = ? AND providerUserId = ?`,
649
+ [provider, providerUserId],
650
+ );
651
+ }
652
+
653
+ // ─── D. Email Tokens ───
654
+
655
+ /**
656
+ * Create an email token (verify, password-reset, magic-link).
657
+ */
658
+ export async function createEmailToken(
659
+ db: AuthDb,
660
+ input: CreateEmailTokenInput,
661
+ ): Promise<void> {
662
+ const now = new Date().toISOString();
663
+
664
+ await db.run(
665
+ `INSERT INTO _email_tokens (token, userId, type, expiresAt, createdAt)
666
+ VALUES (?, ?, ?, ?, ?)`,
667
+ [input.token, input.userId, input.type, input.expiresAt, now],
668
+ );
669
+ }
670
+
671
+ /**
672
+ * Get email token by token string.
673
+ * Returns null if not found or expired.
674
+ */
675
+ export async function getEmailToken(
676
+ db: AuthDb,
677
+ token: string,
678
+ ): Promise<Record<string, unknown> | null> {
679
+ const row = await db.first(
680
+ `SELECT * FROM _email_tokens WHERE token = ?`,
681
+ [token],
682
+ );
683
+
684
+ if (!row) return null;
685
+
686
+ // Check expiration
687
+ if (new Date(row.expiresAt as string) < new Date()) {
688
+ // Clean up expired token
689
+ await db.run(`DELETE FROM _email_tokens WHERE token = ?`, [token]);
690
+ return null;
691
+ }
692
+
693
+ return row;
694
+ }
695
+
696
+ /**
697
+ * Get email token by token string and type.
698
+ * Does NOT auto-delete on expiry (caller decides).
699
+ */
700
+ export async function getEmailTokenByType(
701
+ db: AuthDb,
702
+ token: string,
703
+ type: string,
704
+ ): Promise<Record<string, unknown> | null> {
705
+ return await db.first(
706
+ `SELECT * FROM _email_tokens WHERE token = ? AND type = ?`,
707
+ [token, type],
708
+ );
709
+ }
710
+
711
+ /**
712
+ * Delete a specific email token.
713
+ */
714
+ export async function deleteEmailToken(
715
+ db: AuthDb,
716
+ token: string,
717
+ ): Promise<void> {
718
+ await db.run(`DELETE FROM _email_tokens WHERE token = ?`, [token]);
719
+ }
720
+
721
+ /**
722
+ * Delete all email tokens for a user (by type).
723
+ */
724
+ export async function deleteEmailTokensByUserAndType(
725
+ db: AuthDb,
726
+ userId: string,
727
+ type: string,
728
+ ): Promise<void> {
729
+ await db.run(
730
+ `DELETE FROM _email_tokens WHERE userId = ? AND type = ?`,
731
+ [userId, type],
732
+ );
733
+ }
734
+
735
+ /**
736
+ * Delete all email tokens for a user (all types).
737
+ */
738
+ export async function deleteEmailTokensByUser(
739
+ db: AuthDb,
740
+ userId: string,
741
+ ): Promise<void> {
742
+ await db.run(`DELETE FROM _email_tokens WHERE userId = ?`, [userId]);
743
+ }
744
+
745
+ // ─── E. MFA ───
746
+
747
+ /**
748
+ * Create an MFA factor (unverified by default).
749
+ * INSERT + re-fetch.
750
+ */
751
+ export async function createMfaFactor(
752
+ db: AuthDb,
753
+ input: CreateMfaFactorInput,
754
+ ): Promise<Record<string, unknown>> {
755
+ const now = new Date().toISOString();
756
+
757
+ await db.run(
758
+ `INSERT INTO _mfa_factors (id, userId, type, secret, verified, createdAt)
759
+ VALUES (?, ?, ?, ?, 0, ?)`,
760
+ [input.id, input.userId, input.type ?? 'totp', input.secret, now],
761
+ );
762
+
763
+ // Re-fetch
764
+ const factor = await db.first(`SELECT * FROM _mfa_factors WHERE id = ?`, [input.id]);
765
+ return factor as Record<string, unknown>;
766
+ }
767
+
768
+ /**
769
+ * Get MFA factor by ID.
770
+ */
771
+ export async function getMfaFactor(
772
+ db: AuthDb,
773
+ factorId: string,
774
+ ): Promise<Record<string, unknown> | null> {
775
+ return await db.first(`SELECT * FROM _mfa_factors WHERE id = ?`, [factorId]);
776
+ }
777
+
778
+ /**
779
+ * Get MFA factor by ID and userId (ownership check).
780
+ */
781
+ export async function getMfaFactorForUser(
782
+ db: AuthDb,
783
+ factorId: string,
784
+ userId: string,
785
+ type?: string,
786
+ ): Promise<Record<string, unknown> | null> {
787
+ let query = `SELECT * FROM _mfa_factors WHERE id = ? AND userId = ?`;
788
+ const params: unknown[] = [factorId, userId];
789
+
790
+ if (type) {
791
+ query += ` AND type = ?`;
792
+ params.push(type);
793
+ }
794
+
795
+ return await db.first(query, params);
796
+ }
797
+
798
+ /**
799
+ * Get the first verified MFA factor for a user (by type).
800
+ */
801
+ export async function getMfaFactorByUser(
802
+ db: AuthDb,
803
+ userId: string,
804
+ type: string = 'totp',
805
+ verifiedOnly: boolean = true,
806
+ ): Promise<Record<string, unknown> | null> {
807
+ const verifiedClause = verifiedOnly ? ` AND verified = 1` : '';
808
+ return await db.first(
809
+ `SELECT * FROM _mfa_factors WHERE userId = ? AND type = ?${verifiedClause} LIMIT 1`,
810
+ [userId, type],
811
+ );
812
+ }
813
+
814
+ /**
815
+ * List MFA factors for a user (id, type, verified, createdAt).
816
+ */
817
+ export async function listMfaFactors(
818
+ db: AuthDb,
819
+ userId: string,
820
+ ): Promise<Array<{ id: string; type: string; verified: boolean; createdAt: string }>> {
821
+ const results = await db.query<{ id: string; type: string; verified: number; createdAt: string }>(
822
+ `SELECT id, type, verified, createdAt FROM _mfa_factors WHERE userId = ?`,
823
+ [userId],
824
+ );
825
+
826
+ return results.map((f) => ({
827
+ id: f.id,
828
+ type: f.type,
829
+ verified: f.verified === 1,
830
+ createdAt: f.createdAt,
831
+ }));
832
+ }
833
+
834
+ /**
835
+ * List verified MFA factors (id + type only, for MFA check during signin).
836
+ */
837
+ export async function listVerifiedMfaFactors(
838
+ db: AuthDb,
839
+ userId: string,
840
+ ): Promise<Array<{ id: string; type: string }>> {
841
+ return await db.query<{ id: string; type: string }>(
842
+ `SELECT id, type FROM _mfa_factors WHERE userId = ? AND verified = 1`,
843
+ [userId],
844
+ );
845
+ }
846
+
847
+ /**
848
+ * Verify (confirm) an MFA factor. UPDATE verified=1.
849
+ */
850
+ export async function verifyMfaFactor(
851
+ db: AuthDb,
852
+ factorId: string,
853
+ ): Promise<void> {
854
+ await db.run(`UPDATE _mfa_factors SET verified = 1 WHERE id = ?`, [factorId]);
855
+ }
856
+
857
+ /**
858
+ * Delete an MFA factor by ID.
859
+ */
860
+ export async function deleteMfaFactor(
861
+ db: AuthDb,
862
+ factorId: string,
863
+ ): Promise<void> {
864
+ await db.run(`DELETE FROM _mfa_factors WHERE id = ?`, [factorId]);
865
+ }
866
+
867
+ /**
868
+ * Delete all MFA factors for a user (used by disable MFA).
869
+ */
870
+ export async function deleteAllMfaFactors(
871
+ db: AuthDb,
872
+ userId: string,
873
+ ): Promise<void> {
874
+ await db.run(`DELETE FROM _mfa_factors WHERE userId = ?`, [userId]);
875
+ }
876
+
877
+ /**
878
+ * Delete unverified (pending) MFA factors for a user by type.
879
+ */
880
+ export async function deleteUnverifiedMfaFactors(
881
+ db: AuthDb,
882
+ userId: string,
883
+ type: string = 'totp',
884
+ ): Promise<void> {
885
+ await db.run(
886
+ `DELETE FROM _mfa_factors WHERE userId = ? AND type = ? AND verified = 0`,
887
+ [userId, type],
888
+ );
889
+ }
890
+
891
+ /**
892
+ * Delete all MFA factors AND recovery codes for a user (atomic disable).
893
+ * Uses db.batch() for atomic transaction.
894
+ */
895
+ export async function disableMfa(
896
+ db: AuthDb,
897
+ userId: string,
898
+ ): Promise<void> {
899
+ await db.batch([
900
+ { sql: `DELETE FROM _mfa_factors WHERE userId = ?`, params: [userId] },
901
+ { sql: `DELETE FROM _mfa_recovery_codes WHERE userId = ?`, params: [userId] },
902
+ ]);
903
+ }
904
+
905
+ /**
906
+ * Create recovery codes (batch insert).
907
+ */
908
+ export async function createRecoveryCodes(
909
+ db: AuthDb,
910
+ userId: string,
911
+ codes: Array<{ id: string; codeHash: string }>,
912
+ ): Promise<void> {
913
+ if (codes.length === 0) return;
914
+
915
+ const now = new Date().toISOString();
916
+
917
+ await db.batch(
918
+ codes.map((code) => ({
919
+ sql: `INSERT INTO _mfa_recovery_codes (id, userId, codeHash, used, createdAt)
920
+ VALUES (?, ?, ?, 0, ?)`,
921
+ params: [code.id, userId, code.codeHash, now],
922
+ })),
923
+ );
924
+ }
925
+
926
+ /**
927
+ * List unused recovery codes for a user.
928
+ */
929
+ export async function listRecoveryCodes(
930
+ db: AuthDb,
931
+ userId: string,
932
+ ): Promise<Record<string, unknown>[]> {
933
+ return await db.query(
934
+ `SELECT * FROM _mfa_recovery_codes WHERE userId = ? AND used = 0`,
935
+ [userId],
936
+ );
937
+ }
938
+
939
+ /**
940
+ * Mark a recovery code as used. UPDATE used=1.
941
+ */
942
+ export async function useRecoveryCode(
943
+ db: AuthDb,
944
+ codeId: string,
945
+ ): Promise<void> {
946
+ await db.run(`UPDATE _mfa_recovery_codes SET used = 1 WHERE id = ?`, [codeId]);
947
+ }
948
+
949
+ // ─── F. WebAuthn ───
950
+
951
+ /**
952
+ * Create a WebAuthn credential.
953
+ * INSERT + re-fetch.
954
+ */
955
+ export async function createWebAuthnCredential(
956
+ db: AuthDb,
957
+ input: CreateWebAuthnCredentialInput,
958
+ ): Promise<Record<string, unknown>> {
959
+ const now = new Date().toISOString();
960
+
961
+ await db.run(
962
+ `INSERT INTO _webauthn_credentials (id, userId, credentialId, credentialPublicKey, counter, transports, createdAt)
963
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
964
+ [
965
+ input.id,
966
+ input.userId,
967
+ input.credentialId,
968
+ input.credentialPublicKey,
969
+ input.counter ?? 0,
970
+ input.transports ?? null,
971
+ now,
972
+ ],
973
+ );
974
+
975
+ // Re-fetch
976
+ const cred = await db.first(`SELECT * FROM _webauthn_credentials WHERE id = ?`, [input.id]);
977
+ return cred as Record<string, unknown>;
978
+ }
979
+
980
+ /**
981
+ * Get WebAuthn credential by credentialId.
982
+ */
983
+ export async function getWebAuthnCredential(
984
+ db: AuthDb,
985
+ credentialId: string,
986
+ ): Promise<Record<string, unknown> | null> {
987
+ return await db.first(
988
+ `SELECT * FROM _webauthn_credentials WHERE credentialId = ?`,
989
+ [credentialId],
990
+ );
991
+ }
992
+
993
+ /**
994
+ * List WebAuthn credentials for a user.
995
+ */
996
+ export async function listWebAuthnCredentials(
997
+ db: AuthDb,
998
+ userId: string,
999
+ ): Promise<Array<{
1000
+ id: string;
1001
+ credentialId: string;
1002
+ credentialPublicKey: string;
1003
+ counter: number;
1004
+ transports: string | null;
1005
+ createdAt: string;
1006
+ }>> {
1007
+ return await db.query<{
1008
+ id: string;
1009
+ credentialId: string;
1010
+ credentialPublicKey: string;
1011
+ counter: number;
1012
+ transports: string | null;
1013
+ createdAt: string;
1014
+ }>(
1015
+ `SELECT id, credentialId, credentialPublicKey, counter, transports, createdAt FROM _webauthn_credentials WHERE userId = ?`,
1016
+ [userId],
1017
+ );
1018
+ }
1019
+
1020
+ /**
1021
+ * Update WebAuthn credential counter.
1022
+ */
1023
+ export async function updateWebAuthnCounter(
1024
+ db: AuthDb,
1025
+ credentialId: string,
1026
+ counter: number,
1027
+ ): Promise<void> {
1028
+ await db.run(
1029
+ `UPDATE _webauthn_credentials SET counter = ? WHERE credentialId = ?`,
1030
+ [counter, credentialId],
1031
+ );
1032
+ }
1033
+
1034
+ /**
1035
+ * Delete a WebAuthn credential by credentialId + userId (ownership check).
1036
+ */
1037
+ export async function deleteWebAuthnCredential(
1038
+ db: AuthDb,
1039
+ credentialId: string,
1040
+ userId?: string,
1041
+ ): Promise<void> {
1042
+ if (userId) {
1043
+ await db.run(
1044
+ `DELETE FROM _webauthn_credentials WHERE credentialId = ? AND userId = ?`,
1045
+ [credentialId, userId],
1046
+ );
1047
+ } else {
1048
+ await db.run(
1049
+ `DELETE FROM _webauthn_credentials WHERE credentialId = ?`,
1050
+ [credentialId],
1051
+ );
1052
+ }
1053
+ }
1054
+
1055
+ // ─── G. Cleanup ───
1056
+
1057
+ /**
1058
+ * Clean stale anonymous accounts older than retentionDays.
1059
+ * Finds anonymous users, batch deletes sessions + users.
1060
+ * Uses JS-computed cutoff date (portable across D1/pg).
1061
+ */
1062
+ export async function cleanStaleAnonymousAccounts(
1063
+ db: AuthDb,
1064
+ retentionDays: number,
1065
+ ): Promise<string[]> {
1066
+ // Compute cutoff date in JS (portable)
1067
+ const cutoff = new Date(Date.now() - retentionDays * 86_400_000).toISOString();
1068
+
1069
+ // Find stale anonymous user IDs
1070
+ const staleUsers = await db.query<{ id: string }>(
1071
+ `SELECT id FROM _users WHERE isAnonymous = 1 AND updatedAt < ?`,
1072
+ [cutoff],
1073
+ );
1074
+
1075
+ if (staleUsers.length === 0) return [];
1076
+
1077
+ const userIds = staleUsers.map((u) => u.id);
1078
+
1079
+ // Batch delete in chunks of 50
1080
+ for (let i = 0; i < userIds.length; i += 50) {
1081
+ const chunk = userIds.slice(i, i + 50);
1082
+ const stmts: { sql: string; params: unknown[] }[] = [];
1083
+
1084
+ for (const uid of chunk) {
1085
+ stmts.push({ sql: `DELETE FROM _sessions WHERE userId = ?`, params: [uid] });
1086
+ stmts.push({ sql: `DELETE FROM _email_tokens WHERE userId = ?`, params: [uid] });
1087
+ stmts.push({ sql: `DELETE FROM _oauth_accounts WHERE userId = ?`, params: [uid] });
1088
+ stmts.push({ sql: `DELETE FROM _mfa_recovery_codes WHERE userId = ?`, params: [uid] });
1089
+ stmts.push({ sql: `DELETE FROM _mfa_factors WHERE userId = ?`, params: [uid] });
1090
+ stmts.push({ sql: `DELETE FROM _webauthn_credentials WHERE userId = ?`, params: [uid] });
1091
+ stmts.push({ sql: `DELETE FROM _users WHERE id = ?`, params: [uid] });
1092
+ }
1093
+
1094
+ await db.batch(stmts);
1095
+ }
1096
+
1097
+ return userIds;
1098
+ }
1099
+
1100
+ // ─── H. Sanitization ───
1101
+
1102
+ /**
1103
+ * Remove sensitive fields from user record for client response.
1104
+ * - Strips passwordHash
1105
+ * - Strips appMetadata (unless includeAppMetadata is true)
1106
+ * - Parses JSON TEXT fields (customClaims, metadata, appMetadata)
1107
+ * - Converts INTEGER booleans (0/1) to true/false
1108
+ */
1109
+ export function sanitizeUser(
1110
+ user: Record<string, unknown>,
1111
+ opts?: { includeAppMetadata?: boolean },
1112
+ ): Record<string, unknown> {
1113
+ const {
1114
+ passwordHash: _passwordHash,
1115
+ appMetadata: rawAppMetadata,
1116
+ ...safe
1117
+ } = user;
1118
+
1119
+ // Parse customClaims JSON
1120
+ if (safe.customClaims && typeof safe.customClaims === 'string') {
1121
+ try {
1122
+ safe.customClaims = JSON.parse(safe.customClaims as string);
1123
+ } catch {
1124
+ safe.customClaims = null;
1125
+ }
1126
+ }
1127
+
1128
+ // Parse metadata JSON
1129
+ if (safe.metadata && typeof safe.metadata === 'string') {
1130
+ try {
1131
+ safe.metadata = JSON.parse(safe.metadata as string);
1132
+ } catch {
1133
+ safe.metadata = null;
1134
+ }
1135
+ }
1136
+
1137
+ // Include appMetadata only for admin requests
1138
+ if (opts?.includeAppMetadata && rawAppMetadata) {
1139
+ try {
1140
+ safe.appMetadata = typeof rawAppMetadata === 'string'
1141
+ ? JSON.parse(rawAppMetadata as string)
1142
+ : rawAppMetadata;
1143
+ } catch {
1144
+ safe.appMetadata = null;
1145
+ }
1146
+ }
1147
+
1148
+ // Convert isAnonymous from INTEGER (0/1) to boolean
1149
+ if (typeof safe.isAnonymous === 'number') {
1150
+ safe.isAnonymous = safe.isAnonymous === 1;
1151
+ }
1152
+
1153
+ // Convert verified from INTEGER (0/1) to boolean
1154
+ if (typeof safe.verified === 'number') {
1155
+ safe.verified = safe.verified === 1;
1156
+ }
1157
+
1158
+ // Convert phoneVerified from INTEGER (0/1) to boolean
1159
+ if (typeof safe.phoneVerified === 'number') {
1160
+ safe.phoneVerified = safe.phoneVerified === 1;
1161
+ }
1162
+
1163
+ // Convert disabled from INTEGER (0/1) to boolean
1164
+ if (typeof safe.disabled === 'number') {
1165
+ safe.disabled = safe.disabled === 1;
1166
+ }
1167
+
1168
+ return safe;
1169
+ }
1170
+
1171
+ /**
1172
+ * Build public user data for _users_public sync.
1173
+ * Only exposes email if emailVisibility is 'public'.
1174
+ */
1175
+ export function buildPublicUserData(user: Record<string, unknown>): Record<string, unknown> {
1176
+ const data: Record<string, unknown> = {
1177
+ displayName: user.displayName ?? null,
1178
+ avatarUrl: user.avatarUrl ?? null,
1179
+ role: user.role ?? 'user',
1180
+ isAnonymous: user.isAnonymous ?? 0,
1181
+ createdAt: user.createdAt,
1182
+ updatedAt: user.updatedAt,
1183
+ };
1184
+
1185
+ // Email only if emailVisibility is 'public'
1186
+ if (user.emailVisibility === 'public' && user.email) {
1187
+ data.email = user.email;
1188
+ } else {
1189
+ data.email = null;
1190
+ }
1191
+
1192
+ return data;
1193
+ }