@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,1056 @@
1
+ /**
2
+ * Auth D1 Control Plane — query helpers
3
+ *
4
+ * Replaces the former Registry DO with D1-based indexing for:
5
+ * - Email uniqueness check + userId mapping
6
+ * - OAuth provider uniqueness + userId mapping
7
+ * - Anonymous userId mapping
8
+ * - Admin accounts + sessions (with Lazy expiry cleanup)
9
+ *
10
+ * All functions use the AuthDb adapter interface so they work with
11
+ * both D1 (SQLite) and PostgreSQL backends transparently.
12
+ */
13
+
14
+ import type { AuthDb } from './auth-db-adapter.js';
15
+
16
+ // ─── Constants ───
17
+
18
+ export const AUTH_SHARD_COUNT = 16;
19
+ const PENDING_EXPIRY_MINUTES = 5;
20
+
21
+ // ─── Schema ───
22
+
23
+ export const AUTH_D1_SCHEMA = `
24
+ CREATE TABLE IF NOT EXISTS _email_index (
25
+ email TEXT PRIMARY KEY,
26
+ userId TEXT NOT NULL,
27
+ shardId INTEGER NOT NULL,
28
+ status TEXT NOT NULL DEFAULT 'pending',
29
+ createdAt TEXT NOT NULL DEFAULT (datetime('now'))
30
+ );
31
+
32
+ CREATE TABLE IF NOT EXISTS _oauth_index (
33
+ provider TEXT NOT NULL,
34
+ providerUserId TEXT NOT NULL,
35
+ userId TEXT NOT NULL,
36
+ shardId INTEGER NOT NULL,
37
+ status TEXT NOT NULL DEFAULT 'pending',
38
+ createdAt TEXT NOT NULL DEFAULT (datetime('now')),
39
+ PRIMARY KEY (provider, providerUserId)
40
+ );
41
+
42
+ CREATE TABLE IF NOT EXISTS _anon_index (
43
+ userId TEXT PRIMARY KEY,
44
+ shardId INTEGER NOT NULL,
45
+ status TEXT NOT NULL DEFAULT 'pending',
46
+ createdAt TEXT NOT NULL DEFAULT (datetime('now'))
47
+ );
48
+
49
+ CREATE TABLE IF NOT EXISTS _admins (
50
+ id TEXT PRIMARY KEY,
51
+ email TEXT UNIQUE NOT NULL,
52
+ passwordHash TEXT NOT NULL,
53
+ createdAt TEXT NOT NULL,
54
+ updatedAt TEXT NOT NULL
55
+ );
56
+
57
+ CREATE TABLE IF NOT EXISTS _admin_sessions (
58
+ id TEXT PRIMARY KEY,
59
+ adminId TEXT NOT NULL,
60
+ refreshToken TEXT NOT NULL,
61
+ expiresAt TEXT NOT NULL,
62
+ createdAt TEXT NOT NULL,
63
+ FOREIGN KEY (adminId) REFERENCES _admins(id)
64
+ );
65
+
66
+ CREATE TABLE IF NOT EXISTS _push_devices (
67
+ userId TEXT NOT NULL,
68
+ deviceId TEXT NOT NULL,
69
+ token TEXT NOT NULL,
70
+ platform TEXT NOT NULL,
71
+ updatedAt TEXT NOT NULL,
72
+ deviceInfo TEXT,
73
+ metadata TEXT,
74
+ PRIMARY KEY (userId, deviceId)
75
+ );
76
+ CREATE INDEX IF NOT EXISTS idx_push_devices_userId ON _push_devices(userId);
77
+
78
+ CREATE TABLE IF NOT EXISTS _phone_index (
79
+ phone TEXT PRIMARY KEY,
80
+ userId TEXT NOT NULL,
81
+ shardId INTEGER NOT NULL,
82
+ status TEXT NOT NULL DEFAULT 'pending',
83
+ createdAt TEXT NOT NULL DEFAULT (datetime('now'))
84
+ );
85
+
86
+ CREATE TABLE IF NOT EXISTS _passkey_index (
87
+ credentialId TEXT PRIMARY KEY,
88
+ userId TEXT NOT NULL,
89
+ shardId INTEGER NOT NULL,
90
+ createdAt TEXT NOT NULL DEFAULT (datetime('now'))
91
+ );
92
+
93
+ CREATE TABLE IF NOT EXISTS _users_public (
94
+ id TEXT PRIMARY KEY,
95
+ email TEXT,
96
+ displayName TEXT,
97
+ avatarUrl TEXT,
98
+ role TEXT DEFAULT 'user',
99
+ isAnonymous INTEGER DEFAULT 0,
100
+ createdAt TEXT NOT NULL,
101
+ updatedAt TEXT NOT NULL
102
+ );
103
+ CREATE INDEX IF NOT EXISTS idx_users_public_email ON _users_public(email);
104
+
105
+ CREATE TABLE IF NOT EXISTS _meta (
106
+ key TEXT PRIMARY KEY,
107
+ value TEXT NOT NULL
108
+ );
109
+
110
+ CREATE TABLE IF NOT EXISTS _users (
111
+ id TEXT PRIMARY KEY,
112
+ email TEXT,
113
+ passwordHash TEXT,
114
+ displayName TEXT,
115
+ avatarUrl TEXT,
116
+ emailVisibility TEXT DEFAULT 'private',
117
+ role TEXT DEFAULT 'user',
118
+ verified INTEGER DEFAULT 0,
119
+ isAnonymous INTEGER DEFAULT 0,
120
+ customClaims TEXT,
121
+ phone TEXT,
122
+ phoneVerified INTEGER DEFAULT 0,
123
+ metadata TEXT,
124
+ appMetadata TEXT,
125
+ disabled INTEGER DEFAULT 0,
126
+ status TEXT DEFAULT 'active',
127
+ locale TEXT DEFAULT 'en',
128
+ createdAt TEXT NOT NULL,
129
+ updatedAt TEXT NOT NULL
130
+ );
131
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email ON _users(email) WHERE email IS NOT NULL;
132
+ CREATE INDEX IF NOT EXISTS idx_users_phone ON _users(phone) WHERE phone IS NOT NULL;
133
+
134
+ CREATE TABLE IF NOT EXISTS _sessions (
135
+ id TEXT PRIMARY KEY,
136
+ userId TEXT NOT NULL,
137
+ refreshToken TEXT NOT NULL,
138
+ previousRefreshToken TEXT,
139
+ rotatedAt TEXT,
140
+ expiresAt TEXT NOT NULL,
141
+ createdAt TEXT NOT NULL,
142
+ metadata TEXT,
143
+ FOREIGN KEY (userId) REFERENCES _users(id)
144
+ );
145
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_refreshToken ON _sessions(refreshToken);
146
+ CREATE INDEX IF NOT EXISTS idx_sessions_userId ON _sessions(userId);
147
+ CREATE INDEX IF NOT EXISTS idx_sessions_expiresAt ON _sessions(expiresAt);
148
+
149
+ CREATE TABLE IF NOT EXISTS _oauth_accounts (
150
+ id TEXT PRIMARY KEY,
151
+ userId TEXT NOT NULL,
152
+ provider TEXT NOT NULL,
153
+ providerUserId TEXT NOT NULL,
154
+ createdAt TEXT NOT NULL,
155
+ FOREIGN KEY (userId) REFERENCES _users(id)
156
+ );
157
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_oauth_provider_user ON _oauth_accounts(provider, providerUserId);
158
+ CREATE INDEX IF NOT EXISTS idx_oauth_userId ON _oauth_accounts(userId);
159
+
160
+ CREATE TABLE IF NOT EXISTS _email_tokens (
161
+ token TEXT PRIMARY KEY,
162
+ userId TEXT NOT NULL,
163
+ type TEXT NOT NULL,
164
+ expiresAt TEXT NOT NULL,
165
+ createdAt TEXT NOT NULL,
166
+ FOREIGN KEY (userId) REFERENCES _users(id)
167
+ );
168
+ CREATE INDEX IF NOT EXISTS idx_email_tokens_userId ON _email_tokens(userId);
169
+ CREATE INDEX IF NOT EXISTS idx_email_tokens_expiresAt ON _email_tokens(expiresAt);
170
+
171
+ CREATE TABLE IF NOT EXISTS _mfa_factors (
172
+ id TEXT PRIMARY KEY,
173
+ userId TEXT NOT NULL,
174
+ type TEXT NOT NULL DEFAULT 'totp',
175
+ secret TEXT NOT NULL,
176
+ verified INTEGER DEFAULT 0,
177
+ createdAt TEXT NOT NULL,
178
+ FOREIGN KEY (userId) REFERENCES _users(id)
179
+ );
180
+ CREATE INDEX IF NOT EXISTS idx_mfa_factors_userId ON _mfa_factors(userId);
181
+
182
+ CREATE TABLE IF NOT EXISTS _mfa_recovery_codes (
183
+ id TEXT PRIMARY KEY,
184
+ userId TEXT NOT NULL,
185
+ codeHash TEXT NOT NULL,
186
+ used INTEGER DEFAULT 0,
187
+ createdAt TEXT NOT NULL,
188
+ FOREIGN KEY (userId) REFERENCES _users(id)
189
+ );
190
+ CREATE INDEX IF NOT EXISTS idx_mfa_recovery_userId ON _mfa_recovery_codes(userId);
191
+
192
+ CREATE TABLE IF NOT EXISTS _webauthn_credentials (
193
+ id TEXT PRIMARY KEY,
194
+ userId TEXT NOT NULL,
195
+ credentialId TEXT NOT NULL,
196
+ credentialPublicKey TEXT NOT NULL,
197
+ counter INTEGER DEFAULT 0,
198
+ transports TEXT,
199
+ createdAt TEXT NOT NULL,
200
+ FOREIGN KEY (userId) REFERENCES _users(id)
201
+ );
202
+ CREATE INDEX IF NOT EXISTS idx_webauthn_userId ON _webauthn_credentials(userId);
203
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_webauthn_credentialId ON _webauthn_credentials(credentialId);
204
+
205
+ `;
206
+
207
+ export const AUTH_PG_SCHEMA = `
208
+ CREATE TABLE IF NOT EXISTS _email_index (
209
+ email TEXT PRIMARY KEY,
210
+ userId TEXT NOT NULL,
211
+ shardId INTEGER NOT NULL,
212
+ status TEXT NOT NULL DEFAULT 'pending',
213
+ createdAt TEXT NOT NULL DEFAULT NOW()
214
+ );
215
+
216
+ CREATE TABLE IF NOT EXISTS _oauth_index (
217
+ provider TEXT NOT NULL,
218
+ providerUserId TEXT NOT NULL,
219
+ userId TEXT NOT NULL,
220
+ shardId INTEGER NOT NULL,
221
+ status TEXT NOT NULL DEFAULT 'pending',
222
+ createdAt TEXT NOT NULL DEFAULT NOW(),
223
+ PRIMARY KEY (provider, providerUserId)
224
+ );
225
+
226
+ CREATE TABLE IF NOT EXISTS _anon_index (
227
+ userId TEXT PRIMARY KEY,
228
+ shardId INTEGER NOT NULL,
229
+ status TEXT NOT NULL DEFAULT 'pending',
230
+ createdAt TEXT NOT NULL DEFAULT NOW()
231
+ );
232
+
233
+ CREATE TABLE IF NOT EXISTS _admins (
234
+ id TEXT PRIMARY KEY,
235
+ email TEXT UNIQUE NOT NULL,
236
+ passwordHash TEXT NOT NULL,
237
+ createdAt TEXT NOT NULL,
238
+ updatedAt TEXT NOT NULL
239
+ );
240
+
241
+ CREATE TABLE IF NOT EXISTS _admin_sessions (
242
+ id TEXT PRIMARY KEY,
243
+ adminId TEXT NOT NULL,
244
+ refreshToken TEXT NOT NULL,
245
+ expiresAt TEXT NOT NULL,
246
+ createdAt TEXT NOT NULL,
247
+ FOREIGN KEY (adminId) REFERENCES _admins(id)
248
+ );
249
+
250
+ CREATE TABLE IF NOT EXISTS _push_devices (
251
+ userId TEXT NOT NULL,
252
+ deviceId TEXT NOT NULL,
253
+ token TEXT NOT NULL,
254
+ platform TEXT NOT NULL,
255
+ updatedAt TEXT NOT NULL,
256
+ deviceInfo TEXT,
257
+ metadata TEXT,
258
+ PRIMARY KEY (userId, deviceId)
259
+ );
260
+ CREATE INDEX IF NOT EXISTS idx_push_devices_userId ON _push_devices(userId);
261
+
262
+ CREATE TABLE IF NOT EXISTS _phone_index (
263
+ phone TEXT PRIMARY KEY,
264
+ userId TEXT NOT NULL,
265
+ shardId INTEGER NOT NULL,
266
+ status TEXT NOT NULL DEFAULT 'pending',
267
+ createdAt TEXT NOT NULL DEFAULT NOW()
268
+ );
269
+
270
+ CREATE TABLE IF NOT EXISTS _passkey_index (
271
+ credentialId TEXT PRIMARY KEY,
272
+ userId TEXT NOT NULL,
273
+ shardId INTEGER NOT NULL,
274
+ createdAt TEXT NOT NULL DEFAULT NOW()
275
+ );
276
+
277
+ CREATE TABLE IF NOT EXISTS _users_public (
278
+ id TEXT PRIMARY KEY,
279
+ email TEXT,
280
+ displayName TEXT,
281
+ avatarUrl TEXT,
282
+ role TEXT DEFAULT 'user',
283
+ isAnonymous INTEGER DEFAULT 0,
284
+ createdAt TEXT NOT NULL,
285
+ updatedAt TEXT NOT NULL
286
+ );
287
+ CREATE INDEX IF NOT EXISTS idx_users_public_email ON _users_public(email);
288
+
289
+ CREATE TABLE IF NOT EXISTS _meta (
290
+ key TEXT PRIMARY KEY,
291
+ value TEXT NOT NULL
292
+ );
293
+
294
+ CREATE TABLE IF NOT EXISTS _users (
295
+ id TEXT PRIMARY KEY,
296
+ email TEXT,
297
+ passwordHash TEXT,
298
+ displayName TEXT,
299
+ avatarUrl TEXT,
300
+ emailVisibility TEXT DEFAULT 'private',
301
+ role TEXT DEFAULT 'user',
302
+ verified INTEGER DEFAULT 0,
303
+ isAnonymous INTEGER DEFAULT 0,
304
+ customClaims TEXT,
305
+ phone TEXT,
306
+ phoneVerified INTEGER DEFAULT 0,
307
+ metadata TEXT,
308
+ appMetadata TEXT,
309
+ disabled INTEGER DEFAULT 0,
310
+ status TEXT DEFAULT 'active',
311
+ locale TEXT DEFAULT 'en',
312
+ createdAt TEXT NOT NULL,
313
+ updatedAt TEXT NOT NULL
314
+ );
315
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email ON _users(email) WHERE email IS NOT NULL;
316
+ CREATE INDEX IF NOT EXISTS idx_users_phone ON _users(phone) WHERE phone IS NOT NULL;
317
+
318
+ CREATE TABLE IF NOT EXISTS _sessions (
319
+ id TEXT PRIMARY KEY,
320
+ userId TEXT NOT NULL,
321
+ refreshToken TEXT NOT NULL,
322
+ previousRefreshToken TEXT,
323
+ rotatedAt TEXT,
324
+ expiresAt TEXT NOT NULL,
325
+ createdAt TEXT NOT NULL,
326
+ metadata TEXT,
327
+ FOREIGN KEY (userId) REFERENCES _users(id)
328
+ );
329
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_refreshToken ON _sessions(refreshToken);
330
+ CREATE INDEX IF NOT EXISTS idx_sessions_userId ON _sessions(userId);
331
+ CREATE INDEX IF NOT EXISTS idx_sessions_expiresAt ON _sessions(expiresAt);
332
+
333
+ CREATE TABLE IF NOT EXISTS _oauth_accounts (
334
+ id TEXT PRIMARY KEY,
335
+ userId TEXT NOT NULL,
336
+ provider TEXT NOT NULL,
337
+ providerUserId TEXT NOT NULL,
338
+ createdAt TEXT NOT NULL,
339
+ FOREIGN KEY (userId) REFERENCES _users(id)
340
+ );
341
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_oauth_provider_user ON _oauth_accounts(provider, providerUserId);
342
+ CREATE INDEX IF NOT EXISTS idx_oauth_userId ON _oauth_accounts(userId);
343
+
344
+ CREATE TABLE IF NOT EXISTS _email_tokens (
345
+ token TEXT PRIMARY KEY,
346
+ userId TEXT NOT NULL,
347
+ type TEXT NOT NULL,
348
+ expiresAt TEXT NOT NULL,
349
+ createdAt TEXT NOT NULL,
350
+ FOREIGN KEY (userId) REFERENCES _users(id)
351
+ );
352
+ CREATE INDEX IF NOT EXISTS idx_email_tokens_userId ON _email_tokens(userId);
353
+ CREATE INDEX IF NOT EXISTS idx_email_tokens_expiresAt ON _email_tokens(expiresAt);
354
+
355
+ CREATE TABLE IF NOT EXISTS _mfa_factors (
356
+ id TEXT PRIMARY KEY,
357
+ userId TEXT NOT NULL,
358
+ type TEXT NOT NULL DEFAULT 'totp',
359
+ secret TEXT NOT NULL,
360
+ verified INTEGER DEFAULT 0,
361
+ createdAt TEXT NOT NULL,
362
+ FOREIGN KEY (userId) REFERENCES _users(id)
363
+ );
364
+ CREATE INDEX IF NOT EXISTS idx_mfa_factors_userId ON _mfa_factors(userId);
365
+
366
+ CREATE TABLE IF NOT EXISTS _mfa_recovery_codes (
367
+ id TEXT PRIMARY KEY,
368
+ userId TEXT NOT NULL,
369
+ codeHash TEXT NOT NULL,
370
+ used INTEGER DEFAULT 0,
371
+ createdAt TEXT NOT NULL,
372
+ FOREIGN KEY (userId) REFERENCES _users(id)
373
+ );
374
+ CREATE INDEX IF NOT EXISTS idx_mfa_recovery_userId ON _mfa_recovery_codes(userId);
375
+
376
+ CREATE TABLE IF NOT EXISTS _webauthn_credentials (
377
+ id TEXT PRIMARY KEY,
378
+ userId TEXT NOT NULL,
379
+ credentialId TEXT NOT NULL,
380
+ credentialPublicKey TEXT NOT NULL,
381
+ counter INTEGER DEFAULT 0,
382
+ transports TEXT,
383
+ createdAt TEXT NOT NULL,
384
+ FOREIGN KEY (userId) REFERENCES _users(id)
385
+ );
386
+ CREATE INDEX IF NOT EXISTS idx_webauthn_userId ON _webauthn_credentials(userId);
387
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_webauthn_credentialId ON _webauthn_credentials(credentialId);
388
+
389
+ `;
390
+
391
+ // ─── Schema Initialization ───
392
+
393
+ let schemaInitialized = false;
394
+
395
+ export async function ensureAuthSchema(db: AuthDb): Promise<void> {
396
+ if (schemaInitialized) return;
397
+
398
+ const schemaText = db.dialect === 'postgres' ? AUTH_PG_SCHEMA : AUTH_D1_SCHEMA;
399
+
400
+ const statements = schemaText
401
+ .split(/;\s*\n/)
402
+ .map((s) => s.trim())
403
+ .filter((s) => s.length > 0);
404
+
405
+ await db.batch(statements.map((sql) => ({ sql })));
406
+ schemaInitialized = true;
407
+ }
408
+
409
+ // Reset for testing
410
+ export function resetSchemaInit(): void {
411
+ schemaInitialized = false;
412
+ }
413
+
414
+ // ─── Email Index ───
415
+
416
+ export async function lookupEmail(
417
+ db: AuthDb,
418
+ email: string,
419
+ ): Promise<{ userId: string; shardId: number } | null> {
420
+ // Clean stale pending first
421
+ const pendingCutoff = new Date(Date.now() - PENDING_EXPIRY_MINUTES * 60000).toISOString();
422
+ await db.run(
423
+ `DELETE FROM _email_index WHERE email = ? AND status = 'pending' AND createdAt < ?`,
424
+ [email, pendingCutoff],
425
+ );
426
+ // Also clean up to 10 stale records for other emails
427
+ await db.run(
428
+ `DELETE FROM _email_index WHERE status = 'pending' AND createdAt < ? LIMIT 10`,
429
+ [pendingCutoff],
430
+ );
431
+
432
+ const result = await db.first<{ userId: string; shardId: number }>(
433
+ `SELECT userId, shardId FROM _email_index WHERE email = ? AND status = 'confirmed'`,
434
+ [email],
435
+ );
436
+
437
+ return result || null;
438
+ }
439
+
440
+ export async function registerEmailPending(
441
+ db: AuthDb,
442
+ email: string,
443
+ userId: string,
444
+ ): Promise<void> {
445
+ // Check for existing confirmed
446
+ const existing = await db.first<{ email: string; status: string }>(
447
+ `SELECT email, status FROM _email_index WHERE email = ?`,
448
+ [email],
449
+ );
450
+
451
+ if (existing) {
452
+ if (existing.status === 'confirmed') {
453
+ throw new Error('EMAIL_ALREADY_REGISTERED');
454
+ }
455
+ // Still pending — clean it and proceed
456
+ await db.run(
457
+ `DELETE FROM _email_index WHERE email = ? AND status = 'pending'`,
458
+ [email],
459
+ );
460
+ }
461
+
462
+ const now = new Date().toISOString();
463
+ await db.run(
464
+ `INSERT INTO _email_index (email, userId, shardId, status, createdAt) VALUES (?, ?, ?, 'pending', ?)`,
465
+ [email, userId, 0, now],
466
+ );
467
+ }
468
+
469
+ export async function confirmEmail(
470
+ db: AuthDb,
471
+ email: string,
472
+ userId: string,
473
+ ): Promise<void> {
474
+ await db.run(
475
+ `UPDATE _email_index SET status = 'confirmed' WHERE email = ? AND userId = ?`,
476
+ [email, userId],
477
+ );
478
+ }
479
+
480
+ export async function deleteEmailPending(
481
+ db: AuthDb,
482
+ email: string,
483
+ ): Promise<void> {
484
+ await db.run(
485
+ `DELETE FROM _email_index WHERE email = ? AND status = 'pending'`,
486
+ [email],
487
+ );
488
+ }
489
+
490
+ export async function deleteEmail(
491
+ db: AuthDb,
492
+ email: string,
493
+ ): Promise<void> {
494
+ await db.run(
495
+ `DELETE FROM _email_index WHERE email = ?`,
496
+ [email],
497
+ );
498
+ }
499
+
500
+ // ─── OAuth Index ───
501
+
502
+ export async function lookupOAuth(
503
+ db: AuthDb,
504
+ provider: string,
505
+ providerUserId: string,
506
+ ): Promise<{ userId: string; shardId: number } | null> {
507
+ // Clean stale pending
508
+ const pendingCutoff = new Date(Date.now() - PENDING_EXPIRY_MINUTES * 60000).toISOString();
509
+ await db.run(
510
+ `DELETE FROM _oauth_index WHERE provider = ? AND providerUserId = ? AND status = 'pending' AND createdAt < ?`,
511
+ [provider, providerUserId, pendingCutoff],
512
+ );
513
+
514
+ const result = await db.first<{ userId: string; shardId: number }>(
515
+ `SELECT userId, shardId FROM _oauth_index WHERE provider = ? AND providerUserId = ? AND status = 'confirmed'`,
516
+ [provider, providerUserId],
517
+ );
518
+
519
+ return result || null;
520
+ }
521
+
522
+ export async function registerOAuthPending(
523
+ db: AuthDb,
524
+ provider: string,
525
+ providerUserId: string,
526
+ userId: string,
527
+ ): Promise<void> {
528
+ // Clean stale pending (5 min)
529
+ const pendingCutoff = new Date(Date.now() - PENDING_EXPIRY_MINUTES * 60000).toISOString();
530
+ await db.run(
531
+ `DELETE FROM _oauth_index WHERE provider = ? AND providerUserId = ? AND status = 'pending' AND createdAt < ?`,
532
+ [provider, providerUserId, pendingCutoff],
533
+ );
534
+ // Also clean other stale pending (max 10)
535
+ await db.run(
536
+ `DELETE FROM _oauth_index WHERE status = 'pending' AND createdAt < ? LIMIT 10`,
537
+ [pendingCutoff],
538
+ );
539
+ const existing = await db.first<{ userId: string; status: string }>(
540
+ `SELECT userId, status FROM _oauth_index WHERE provider = ? AND providerUserId = ?`,
541
+ [provider, providerUserId],
542
+ );
543
+ if (existing) {
544
+ if (existing.status === 'confirmed') {
545
+ throw new Error('OAUTH_ALREADY_LINKED');
546
+ }
547
+ // Allow immediate retry after a partially failed OAuth signup/link flow.
548
+ await db.run(
549
+ `DELETE FROM _oauth_index WHERE provider = ? AND providerUserId = ? AND status = 'pending'`,
550
+ [provider, providerUserId],
551
+ );
552
+ }
553
+ const now = new Date().toISOString();
554
+ await db.run(
555
+ `INSERT INTO _oauth_index (provider, providerUserId, userId, shardId, status, createdAt) VALUES (?, ?, ?, ?, 'pending', ?)`,
556
+ [provider, providerUserId, userId, 0, now],
557
+ );
558
+ }
559
+
560
+ export async function confirmOAuth(
561
+ db: AuthDb,
562
+ provider: string,
563
+ providerUserId: string,
564
+ ): Promise<void> {
565
+ await db.run(
566
+ `UPDATE _oauth_index SET status = 'confirmed' WHERE provider = ? AND providerUserId = ?`,
567
+ [provider, providerUserId],
568
+ );
569
+ }
570
+
571
+ export async function deleteOAuth(
572
+ db: AuthDb,
573
+ provider: string,
574
+ providerUserId: string,
575
+ ): Promise<void> {
576
+ await db.run(
577
+ `DELETE FROM _oauth_index WHERE provider = ? AND providerUserId = ?`,
578
+ [provider, providerUserId],
579
+ );
580
+ }
581
+
582
+ // ─── Anonymous Index ───
583
+
584
+ export async function registerAnonPending(
585
+ db: AuthDb,
586
+ userId: string,
587
+ ): Promise<void> {
588
+ // Clean stale anon pending records
589
+ const pendingCutoff = new Date(Date.now() - PENDING_EXPIRY_MINUTES * 60000).toISOString();
590
+ await db.run(
591
+ `DELETE FROM _anon_index WHERE status = 'pending' AND createdAt < ? LIMIT 10`,
592
+ [pendingCutoff],
593
+ );
594
+
595
+ const now = new Date().toISOString();
596
+ await db.run(
597
+ `INSERT INTO _anon_index (userId, shardId, status, createdAt) VALUES (?, ?, 'pending', ?)`,
598
+ [userId, 0, now],
599
+ );
600
+ }
601
+
602
+ export async function confirmAnon(
603
+ db: AuthDb,
604
+ userId: string,
605
+ ): Promise<void> {
606
+ await db.run(
607
+ `UPDATE _anon_index SET status = 'confirmed' WHERE userId = ?`,
608
+ [userId],
609
+ );
610
+ }
611
+
612
+ export async function deleteAnon(
613
+ db: AuthDb,
614
+ userId: string,
615
+ ): Promise<void> {
616
+ await db.run(
617
+ `DELETE FROM _anon_index WHERE userId = ?`,
618
+ [userId],
619
+ );
620
+ }
621
+
622
+ export async function batchDeleteAnon(
623
+ db: AuthDb,
624
+ userIds: string[],
625
+ ): Promise<void> {
626
+ if (userIds.length === 0) return;
627
+ await db.batch(
628
+ userIds.map((uid) => ({
629
+ sql: `DELETE FROM _anon_index WHERE userId = ?`,
630
+ params: [uid],
631
+ })),
632
+ );
633
+ }
634
+
635
+ // ─── Admin ───
636
+
637
+ export async function getAdminByEmail(
638
+ db: AuthDb,
639
+ email: string,
640
+ ): Promise<{ id: string; email: string; passwordHash: string; createdAt: string; updatedAt: string } | null> {
641
+ // Lazy delete expired sessions
642
+ const now = new Date().toISOString();
643
+ await db.run(
644
+ `DELETE FROM _admin_sessions WHERE expiresAt < ?`,
645
+ [now],
646
+ );
647
+
648
+ const result = await db.first<{ id: string; email: string; passwordHash: string; createdAt: string; updatedAt: string }>(
649
+ `SELECT * FROM _admins WHERE email = ?`,
650
+ [email],
651
+ );
652
+ return result || null;
653
+ }
654
+
655
+ export async function getAdminById(
656
+ db: AuthDb,
657
+ id: string,
658
+ ): Promise<{ id: string; email: string; passwordHash: string; createdAt: string; updatedAt: string } | null> {
659
+ const result = await db.first<{ id: string; email: string; passwordHash: string; createdAt: string; updatedAt: string }>(
660
+ `SELECT * FROM _admins WHERE id = ?`,
661
+ [id],
662
+ );
663
+ return result || null;
664
+ }
665
+
666
+ export async function adminExists(db: AuthDb): Promise<boolean> {
667
+ const result = await db.first<{ cnt: number }>(`SELECT COUNT(*) as cnt FROM _admins`);
668
+ return (result?.cnt ?? 0) > 0;
669
+ }
670
+
671
+ export async function createAdmin(
672
+ db: AuthDb,
673
+ id: string,
674
+ email: string,
675
+ passwordHash: string,
676
+ ): Promise<void> {
677
+ const now = new Date().toISOString();
678
+ await db.run(
679
+ `INSERT INTO _admins (id, email, passwordHash, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?)`,
680
+ [id, email, passwordHash, now, now],
681
+ );
682
+ }
683
+
684
+ export async function getAdminSession(
685
+ db: AuthDb,
686
+ refreshToken: string,
687
+ ): Promise<{ id: string; adminId: string; refreshToken: string; expiresAt: string; createdAt: string } | null> {
688
+ const now = new Date().toISOString();
689
+ const result = await db.first<{ id: string; adminId: string; refreshToken: string; expiresAt: string; createdAt: string }>(
690
+ `SELECT * FROM _admin_sessions WHERE refreshToken = ? AND expiresAt > ?`,
691
+ [refreshToken, now],
692
+ );
693
+ return result || null;
694
+ }
695
+
696
+ export async function createAdminSession(
697
+ db: AuthDb,
698
+ id: string,
699
+ adminId: string,
700
+ refreshToken: string,
701
+ expiresAt: string,
702
+ ): Promise<void> {
703
+ const now = new Date().toISOString();
704
+ await db.run(
705
+ `INSERT INTO _admin_sessions (id, adminId, refreshToken, expiresAt, createdAt) VALUES (?, ?, ?, ?, ?)`,
706
+ [id, adminId, refreshToken, expiresAt, now],
707
+ );
708
+ }
709
+
710
+ export async function deleteAdminSession(
711
+ db: AuthDb,
712
+ sessionId: string,
713
+ ): Promise<void> {
714
+ await db.run(
715
+ `DELETE FROM _admin_sessions WHERE id = ?`,
716
+ [sessionId],
717
+ );
718
+ }
719
+
720
+ // ─── Admin Management ───
721
+
722
+ export async function listAdmins(
723
+ db: AuthDb,
724
+ ): Promise<Array<{ id: string; email: string; createdAt: string; updatedAt: string }>> {
725
+ return db.query<{ id: string; email: string; createdAt: string; updatedAt: string }>(
726
+ `SELECT id, email, createdAt, updatedAt FROM _admins ORDER BY createdAt ASC`,
727
+ );
728
+ }
729
+
730
+ export async function deleteAdmin(
731
+ db: AuthDb,
732
+ id: string,
733
+ ): Promise<void> {
734
+ await db.run(`DELETE FROM _admin_sessions WHERE adminId = ?`, [id]);
735
+ await db.run(`DELETE FROM _admins WHERE id = ?`, [id]);
736
+ }
737
+
738
+ export async function updateAdminPassword(
739
+ db: AuthDb,
740
+ id: string,
741
+ passwordHash: string,
742
+ ): Promise<void> {
743
+ const now = new Date().toISOString();
744
+ await db.batch([
745
+ {
746
+ sql: `UPDATE _admins SET passwordHash = ?, updatedAt = ? WHERE id = ?`,
747
+ params: [passwordHash, now, id],
748
+ },
749
+ {
750
+ sql: `DELETE FROM _admin_sessions WHERE adminId = ?`,
751
+ params: [id],
752
+ },
753
+ ]);
754
+ }
755
+
756
+ // ─── User Listing (for Admin API) ───
757
+
758
+ export async function listUserMappings(
759
+ db: AuthDb,
760
+ limit: number,
761
+ offset: number,
762
+ ): Promise<{ mappings: { userId: string; shardId: number }[]; total: number }> {
763
+ const total = await countUsers(db);
764
+
765
+ // UNION ALL across all index tables, deduplicate by userId
766
+ const results = await db.query<{ userId: string; shardId: number }>(`
767
+ SELECT DISTINCT userId, shardId FROM (
768
+ SELECT userId, shardId FROM _email_index WHERE status = 'confirmed'
769
+ UNION ALL
770
+ SELECT userId, shardId FROM _oauth_index WHERE status = 'confirmed'
771
+ UNION ALL
772
+ SELECT userId, shardId FROM _anon_index WHERE status = 'confirmed'
773
+ UNION ALL
774
+ SELECT userId, shardId FROM _phone_index WHERE status = 'confirmed'
775
+ ) ORDER BY userId DESC LIMIT ? OFFSET ?
776
+ `, [limit + 1, offset]);
777
+
778
+ const mappings = results.slice(0, limit);
779
+ return { mappings, total };
780
+ }
781
+
782
+ export async function searchUserMappingsByEmail(
783
+ db: AuthDb,
784
+ emailQuery: string,
785
+ limit: number,
786
+ offset: number,
787
+ ): Promise<{ mappings: { userId: string; shardId: number }[]; total: number }> {
788
+ const likePattern = `%${emailQuery.toLowerCase()}%`;
789
+ const countResult = await db.first<{ count: number }>(`
790
+ SELECT COUNT(DISTINCT userId) as count FROM _email_index
791
+ WHERE email LIKE ? AND status = 'confirmed'
792
+ `, [likePattern]);
793
+ const results = await db.query<{ userId: string; shardId: number }>(`
794
+ SELECT DISTINCT userId, shardId FROM _email_index
795
+ WHERE email LIKE ? AND status = 'confirmed'
796
+ ORDER BY userId DESC LIMIT ? OFFSET ?
797
+ `, [likePattern, limit + 1, offset]);
798
+
799
+ const mappings = results.slice(0, limit);
800
+ return { mappings, total: countResult?.count ?? 0 };
801
+ }
802
+
803
+ export async function countUsers(db: AuthDb): Promise<number> {
804
+ const result = await db.first<{ count: number }>(`
805
+ SELECT COUNT(DISTINCT userId) as count FROM (
806
+ SELECT userId FROM _email_index WHERE status = 'confirmed'
807
+ UNION ALL
808
+ SELECT userId FROM _oauth_index WHERE status = 'confirmed'
809
+ UNION ALL
810
+ SELECT userId FROM _anon_index WHERE status = 'confirmed'
811
+ UNION ALL
812
+ SELECT userId FROM _phone_index WHERE status = 'confirmed'
813
+ )
814
+ `);
815
+ return result?.count ?? 0;
816
+ }
817
+
818
+ // ─── Phone Index ───
819
+
820
+ export async function lookupPhone(
821
+ db: AuthDb,
822
+ phone: string,
823
+ ): Promise<{ userId: string; shardId: number } | null> {
824
+ // Clean stale pending first
825
+ const pendingCutoff = new Date(Date.now() - PENDING_EXPIRY_MINUTES * 60000).toISOString();
826
+ await db.run(
827
+ `DELETE FROM _phone_index WHERE phone = ? AND status = 'pending' AND createdAt < ?`,
828
+ [phone, pendingCutoff],
829
+ );
830
+
831
+ const result = await db.first<{ userId: string; shardId: number }>(
832
+ `SELECT userId, shardId FROM _phone_index WHERE phone = ? AND status = 'confirmed'`,
833
+ [phone],
834
+ );
835
+
836
+ return result || null;
837
+ }
838
+
839
+ export async function registerPhonePending(
840
+ db: AuthDb,
841
+ phone: string,
842
+ userId: string,
843
+ ): Promise<void> {
844
+ const existing = await db.first<{ phone: string; status: string }>(
845
+ `SELECT phone, status FROM _phone_index WHERE phone = ?`,
846
+ [phone],
847
+ );
848
+
849
+ if (existing) {
850
+ if (existing.status === 'confirmed') {
851
+ throw new Error('PHONE_ALREADY_REGISTERED');
852
+ }
853
+ await db.run(
854
+ `DELETE FROM _phone_index WHERE phone = ? AND status = 'pending'`,
855
+ [phone],
856
+ );
857
+ }
858
+
859
+ const now = new Date().toISOString();
860
+ await db.run(
861
+ `INSERT INTO _phone_index (phone, userId, shardId, status, createdAt) VALUES (?, ?, ?, 'pending', ?)`,
862
+ [phone, userId, 0, now],
863
+ );
864
+ }
865
+
866
+ export async function confirmPhone(
867
+ db: AuthDb,
868
+ phone: string,
869
+ userId: string,
870
+ ): Promise<void> {
871
+ await db.run(
872
+ `UPDATE _phone_index SET status = 'confirmed' WHERE phone = ? AND userId = ?`,
873
+ [phone, userId],
874
+ );
875
+ }
876
+
877
+ export async function deletePhone(
878
+ db: AuthDb,
879
+ phone: string,
880
+ ): Promise<void> {
881
+ await db.run(
882
+ `DELETE FROM _phone_index WHERE phone = ?`,
883
+ [phone],
884
+ );
885
+ }
886
+
887
+ // ─── KV Token Mapping Helpers ───
888
+
889
+ export async function lookupTokenShard(
890
+ kv: KVNamespace,
891
+ token: string,
892
+ ): Promise<number | null> {
893
+ const value = await kv.get(`email-token:${token}`);
894
+ if (!value) return null;
895
+ try {
896
+ const parsed = JSON.parse(value) as { shardId: number };
897
+ return parsed.shardId;
898
+ } catch {
899
+ return null;
900
+ }
901
+ }
902
+
903
+ export async function deleteTokenMapping(
904
+ kv: KVNamespace,
905
+ token: string,
906
+ ): Promise<void> {
907
+ await kv.delete(`email-token:${token}`);
908
+ }
909
+
910
+ // ─── Passkey Index ───
911
+
912
+ export async function lookupPasskey(
913
+ db: AuthDb,
914
+ credentialId: string,
915
+ ): Promise<{ userId: string; shardId: number } | null> {
916
+ const result = await db.first<{ userId: string; shardId: number }>(
917
+ `SELECT userId, shardId FROM _passkey_index WHERE credentialId = ?`,
918
+ [credentialId],
919
+ );
920
+ return result || null;
921
+ }
922
+
923
+ export async function registerPasskey(
924
+ db: AuthDb,
925
+ credentialId: string,
926
+ userId: string,
927
+ ): Promise<void> {
928
+ const now = new Date().toISOString();
929
+ await db.run(
930
+ `INSERT INTO _passkey_index (credentialId, userId, shardId, createdAt) VALUES (?, ?, ?, ?)`,
931
+ [credentialId, userId, 0, now],
932
+ );
933
+ }
934
+
935
+ export async function deletePasskey(
936
+ db: AuthDb,
937
+ credentialId: string,
938
+ ): Promise<void> {
939
+ await db.run(
940
+ `DELETE FROM _passkey_index WHERE credentialId = ?`,
941
+ [credentialId],
942
+ );
943
+ }
944
+
945
+ export async function deletePasskeysByUser(
946
+ db: AuthDb,
947
+ userId: string,
948
+ ): Promise<void> {
949
+ await db.run(
950
+ `DELETE FROM _passkey_index WHERE userId = ?`,
951
+ [userId],
952
+ );
953
+ }
954
+
955
+ // ─── Users Public (denormalized public profiles) ───
956
+
957
+ export interface UserPublicData {
958
+ email?: string | null;
959
+ displayName?: string | null;
960
+ avatarUrl?: string | null;
961
+ role?: string;
962
+ isAnonymous?: boolean | number;
963
+ createdAt: string;
964
+ updatedAt: string;
965
+ }
966
+
967
+ export async function upsertUserPublic(
968
+ db: AuthDb,
969
+ userId: string,
970
+ data: UserPublicData,
971
+ ): Promise<void> {
972
+ const params = [
973
+ userId,
974
+ data.email ?? null,
975
+ data.displayName ?? null,
976
+ data.avatarUrl ?? null,
977
+ data.role ?? 'user',
978
+ data.isAnonymous ? 1 : 0,
979
+ data.createdAt,
980
+ data.updatedAt,
981
+ ];
982
+
983
+ if (db.dialect === 'postgres') {
984
+ await db.run(`
985
+ INSERT INTO _users_public (id, email, displayName, avatarUrl, role, isAnonymous, createdAt, updatedAt)
986
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
987
+ ON CONFLICT (id) DO UPDATE SET
988
+ email = EXCLUDED.email,
989
+ displayName = EXCLUDED.displayName,
990
+ avatarUrl = EXCLUDED.avatarUrl,
991
+ role = EXCLUDED.role,
992
+ isAnonymous = EXCLUDED.isAnonymous,
993
+ createdAt = EXCLUDED.createdAt,
994
+ updatedAt = EXCLUDED.updatedAt
995
+ `, params);
996
+ } else {
997
+ await db.run(`
998
+ INSERT OR REPLACE INTO _users_public (id, email, displayName, avatarUrl, role, isAnonymous, createdAt, updatedAt)
999
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1000
+ `, params);
1001
+ }
1002
+ }
1003
+
1004
+ export async function deleteUserPublic(
1005
+ db: AuthDb,
1006
+ userId: string,
1007
+ ): Promise<void> {
1008
+ await db.run(`DELETE FROM _users_public WHERE id = ?`, [userId]);
1009
+ }
1010
+
1011
+ export async function batchDeleteUserPublic(
1012
+ db: AuthDb,
1013
+ userIds: string[],
1014
+ ): Promise<void> {
1015
+ if (userIds.length === 0) return;
1016
+ await db.batch(
1017
+ userIds.map((uid) => ({
1018
+ sql: `DELETE FROM _users_public WHERE id = ?`,
1019
+ params: [uid],
1020
+ })),
1021
+ );
1022
+ }
1023
+
1024
+ export async function getUserPublic(
1025
+ db: AuthDb,
1026
+ userId: string,
1027
+ ): Promise<Record<string, unknown> | null> {
1028
+ const result = await db.first(
1029
+ `SELECT * FROM _users_public WHERE id = ?`,
1030
+ [userId],
1031
+ );
1032
+ return result || null;
1033
+ }
1034
+
1035
+ export async function listUsersPublic(
1036
+ db: AuthDb,
1037
+ limit: number,
1038
+ offset: number,
1039
+ ): Promise<{ users: Record<string, unknown>[]; total: number }> {
1040
+ const countResult = await db.first<{ cnt: number }>(`SELECT COUNT(*) as cnt FROM _users_public`);
1041
+ const total = countResult?.cnt ?? 0;
1042
+
1043
+ const users = await db.query<Record<string, unknown>>(
1044
+ `SELECT * FROM _users_public ORDER BY createdAt DESC LIMIT ? OFFSET ?`,
1045
+ [limit, offset],
1046
+ );
1047
+
1048
+ return { users, total };
1049
+ }
1050
+
1051
+ // Signup rate limiting removed — now handled by AUTH_SIGNUP_RATE_LIMITER Binding
1052
+
1053
+ // Note: _isolated_do_registry table and helper functions removed.
1054
+ // Orphan Isolated DO cleanup is now handled by Auth Shard Config-Scan + DATABASE binding.
1055
+ // Auth Shard enumerates user-namespaced DB blocks (e.g. user:{id}) from the bundled config
1056
+ // and drops their DOs directly at user deletion time (#133 §2). No D1 registry or KV signaling needed.