@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,464 @@
1
+ import { EdgeBaseError } from '@edge-base/shared';
2
+ import type { AuthDb } from './auth-db-adapter.js';
3
+ import {
4
+ confirmEmail,
5
+ confirmPhone,
6
+ deleteEmail,
7
+ deleteEmailPending,
8
+ deletePhone,
9
+ registerEmailPending,
10
+ registerPhonePending,
11
+ } from './auth-d1.js';
12
+ import * as authService from './auth-d1-service.js';
13
+ import { deletePublicUserProjection, invalidatePublicUserCache, syncPublicUserProjection } from './public-user-profile.js';
14
+ import { unregisterAllTokens } from './push-token.js';
15
+ import { hashPassword, isPasswordHash } from './password.js';
16
+
17
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
18
+ const VALID_STATUSES = new Set(['active', 'suspended', 'banned', 'disabled']);
19
+ const VALID_EMAIL_VISIBILITY = new Set(['public', 'private']);
20
+ const UPDATABLE_USER_FIELDS = new Set([
21
+ 'email',
22
+ 'passwordHash',
23
+ 'displayName',
24
+ 'avatarUrl',
25
+ 'emailVisibility',
26
+ 'role',
27
+ 'status',
28
+ 'verified',
29
+ 'isAnonymous',
30
+ 'customClaims',
31
+ 'phone',
32
+ 'phoneVerified',
33
+ 'metadata',
34
+ 'appMetadata',
35
+ 'disabled',
36
+ 'locale',
37
+ ]);
38
+
39
+ interface ManagedUserOptions {
40
+ executionCtx?: ExecutionContext;
41
+ kv?: KVNamespace;
42
+ }
43
+
44
+ export interface CreateManagedUserInput {
45
+ userId: string;
46
+ email: string;
47
+ passwordHash: string;
48
+ displayName?: string | null;
49
+ avatarUrl?: string | null;
50
+ role?: string;
51
+ verified?: boolean;
52
+ locale?: string;
53
+ metadata?: Record<string, unknown> | null;
54
+ appMetadata?: Record<string, unknown> | null;
55
+ }
56
+
57
+ function hasOwn<T extends object>(value: T, key: string): boolean {
58
+ return Object.prototype.hasOwnProperty.call(value, key);
59
+ }
60
+
61
+ function normalizeOptionalRole(role: unknown): string | undefined {
62
+ if (role === undefined) return undefined;
63
+ if (typeof role !== 'string') {
64
+ throw new EdgeBaseError(400, 'Role must be a non-empty string.');
65
+ }
66
+ const normalized = role.trim();
67
+ if (!normalized) {
68
+ throw new EdgeBaseError(400, 'Role must be a non-empty string.');
69
+ }
70
+ if (normalized.length > 100) {
71
+ throw new EdgeBaseError(400, 'Role must not exceed 100 characters.');
72
+ }
73
+ return normalized;
74
+ }
75
+
76
+ function normalizePhone(phone: string): string {
77
+ const cleaned = phone.replace(/[\s\-()]/g, '');
78
+ if (!/^\+[1-9]\d{6,14}$/.test(cleaned)) {
79
+ throw new EdgeBaseError(400, 'Invalid phone number. Must be in E.164 format (e.g. +15551234567).');
80
+ }
81
+ return cleaned;
82
+ }
83
+
84
+ async function ensureConfirmedEmailIndex(
85
+ db: AuthDb,
86
+ email: string,
87
+ userId: string,
88
+ ): Promise<void> {
89
+ try {
90
+ await registerEmailPending(db, email, userId);
91
+ } catch (err) {
92
+ if ((err as Error).message !== 'EMAIL_ALREADY_REGISTERED') {
93
+ throw err;
94
+ }
95
+ }
96
+ await confirmEmail(db, email, userId);
97
+ }
98
+
99
+ async function ensureConfirmedPhoneIndex(
100
+ db: AuthDb,
101
+ phone: string,
102
+ userId: string,
103
+ ): Promise<void> {
104
+ try {
105
+ await registerPhonePending(db, phone, userId);
106
+ } catch (err) {
107
+ if ((err as Error).message !== 'PHONE_ALREADY_REGISTERED') {
108
+ throw err;
109
+ }
110
+ }
111
+ await confirmPhone(db, phone, userId);
112
+ }
113
+
114
+ function toNullableString(value: unknown): string | null {
115
+ return typeof value === 'string' && value.length > 0 ? value : null;
116
+ }
117
+
118
+ function toRollbackValue(existing: Record<string, unknown>, key: string): unknown {
119
+ if (hasOwn(existing, key)) return existing[key];
120
+ return null;
121
+ }
122
+
123
+ function toEdgeBaseError(
124
+ error: unknown,
125
+ fallbackCode: number,
126
+ fallbackMessage: string,
127
+ ): EdgeBaseError {
128
+ if (error instanceof EdgeBaseError) return error;
129
+ const message = error instanceof Error && error.message ? error.message : fallbackMessage;
130
+ return new EdgeBaseError(fallbackCode, message);
131
+ }
132
+
133
+ function invalidateManagedUserCaches(
134
+ userId: string,
135
+ options: ManagedUserOptions,
136
+ ): void {
137
+ const profileTask = invalidatePublicUserCache(userId, {
138
+ kv: options.kv,
139
+ executionCtx: options.executionCtx,
140
+ awaitCacheWrites: false,
141
+ }).catch((err) => {
142
+ console.error(`[EdgeBase] Failed to invalidate public profile cache for ${userId}:`, err);
143
+ });
144
+
145
+ if (!options.kv) {
146
+ void profileTask;
147
+ return;
148
+ }
149
+
150
+ const pushTask = unregisterAllTokens(options.kv, userId).catch((err) => {
151
+ console.error(`[EdgeBase] Failed to invalidate push token cache for ${userId}:`, err);
152
+ });
153
+ const task = Promise.all([profileTask, pushTask]).then(() => undefined);
154
+
155
+ if (options.executionCtx) {
156
+ options.executionCtx.waitUntil(task);
157
+ return;
158
+ }
159
+
160
+ void task;
161
+ }
162
+
163
+ export async function normalizeAdminUserUpdates(
164
+ raw: Record<string, unknown>,
165
+ ): Promise<authService.UpdateUserInput> {
166
+ const updates = { ...raw } as Record<string, unknown>;
167
+
168
+ if (hasOwn(updates, 'email')) {
169
+ if (typeof updates.email !== 'string' || !EMAIL_RE.test(updates.email.trim())) {
170
+ throw new EdgeBaseError(400, 'Invalid email format.');
171
+ }
172
+ updates.email = updates.email.trim().toLowerCase();
173
+ }
174
+
175
+ if (hasOwn(updates, 'password')) {
176
+ if (typeof updates.password !== 'string') {
177
+ throw new EdgeBaseError(400, 'Password must be a string.');
178
+ }
179
+ if (updates.password.length < 8) {
180
+ throw new EdgeBaseError(400, 'Password must be at least 8 characters.');
181
+ }
182
+ if (updates.password.length > 256) {
183
+ throw new EdgeBaseError(400, 'Password must not exceed 256 characters.');
184
+ }
185
+ updates.passwordHash = await hashPassword(updates.password);
186
+ delete updates.password;
187
+ }
188
+
189
+ if (hasOwn(updates, 'passwordHash')) {
190
+ if (typeof updates.passwordHash !== 'string' || updates.passwordHash.length === 0) {
191
+ throw new EdgeBaseError(400, 'Password hash must be a non-empty string.');
192
+ }
193
+ }
194
+
195
+ if (hasOwn(updates, 'role')) {
196
+ updates.role = normalizeOptionalRole(updates.role);
197
+ }
198
+
199
+ if (hasOwn(updates, 'status')) {
200
+ if (typeof updates.status !== 'string' || !VALID_STATUSES.has(updates.status)) {
201
+ throw new EdgeBaseError(400, 'Invalid status. Must be "active", "suspended", "banned", or "disabled".');
202
+ }
203
+ }
204
+
205
+ if (hasOwn(updates, 'displayName')) {
206
+ if (updates.displayName !== null && (typeof updates.displayName !== 'string' || updates.displayName.length > 200)) {
207
+ throw new EdgeBaseError(400, 'Display name must not exceed 200 characters.');
208
+ }
209
+ }
210
+
211
+ if (hasOwn(updates, 'avatarUrl')) {
212
+ if (updates.avatarUrl !== null && (typeof updates.avatarUrl !== 'string' || updates.avatarUrl.length > 2048)) {
213
+ throw new EdgeBaseError(400, 'Avatar URL must not exceed 2048 characters.');
214
+ }
215
+ }
216
+
217
+ if (hasOwn(updates, 'emailVisibility')) {
218
+ if (typeof updates.emailVisibility !== 'string' || !VALID_EMAIL_VISIBILITY.has(updates.emailVisibility)) {
219
+ throw new EdgeBaseError(400, 'emailVisibility must be "public" or "private".');
220
+ }
221
+ }
222
+
223
+ if (hasOwn(updates, 'phone')) {
224
+ if (updates.phone === null) {
225
+ updates.phoneVerified = false;
226
+ } else if (typeof updates.phone === 'string') {
227
+ updates.phone = normalizePhone(updates.phone);
228
+ if (!hasOwn(updates, 'phoneVerified')) {
229
+ updates.phoneVerified = false;
230
+ }
231
+ } else {
232
+ throw new EdgeBaseError(400, 'Phone must be a string in E.164 format or null.');
233
+ }
234
+ }
235
+
236
+ if (hasOwn(updates, 'locale')) {
237
+ if (updates.locale !== null && (typeof updates.locale !== 'string' || !/^[a-z]{2}(-[A-Z]{2})?$/.test(updates.locale))) {
238
+ throw new EdgeBaseError(400, 'Invalid locale format. Expected format: "en" or "en-US".');
239
+ }
240
+ }
241
+
242
+ const hasSupportedField = Object.keys(updates).some((key) => UPDATABLE_USER_FIELDS.has(key));
243
+ if (!hasSupportedField) {
244
+ throw new EdgeBaseError(
245
+ 400,
246
+ 'No valid fields to update. Allowed fields include email, phone, password, displayName, avatarUrl, role, status, metadata, and appMetadata.',
247
+ );
248
+ }
249
+
250
+ return updates as authService.UpdateUserInput;
251
+ }
252
+
253
+ async function cleanupCreatedUser(
254
+ db: AuthDb,
255
+ userId: string,
256
+ email: string,
257
+ options: ManagedUserOptions,
258
+ ): Promise<void> {
259
+ await authService.deleteUserCascade(db, userId).catch(() => {});
260
+ await deletePublicUserProjection(db, userId, {
261
+ executionCtx: options.executionCtx,
262
+ kv: options.kv,
263
+ awaitCacheWrites: true,
264
+ }).catch(() => {});
265
+ await deleteEmailPending(db, email).catch(() => {});
266
+ await deleteEmail(db, email).catch(() => {});
267
+ }
268
+
269
+ export async function createManagedAdminUser(
270
+ db: AuthDb,
271
+ input: CreateManagedUserInput,
272
+ options: ManagedUserOptions = {},
273
+ ): Promise<Record<string, unknown>> {
274
+ try {
275
+ await registerEmailPending(db, input.email, input.userId);
276
+ } catch (err) {
277
+ if ((err as Error).message === 'EMAIL_ALREADY_REGISTERED') {
278
+ throw new EdgeBaseError(409, 'Email already registered.');
279
+ }
280
+ throw new EdgeBaseError(500, 'User creation failed.');
281
+ }
282
+
283
+ let user: Record<string, unknown> | null = null;
284
+ try {
285
+ user = await authService.createUser(db, {
286
+ userId: input.userId,
287
+ email: input.email,
288
+ passwordHash: input.passwordHash,
289
+ displayName: input.displayName,
290
+ avatarUrl: input.avatarUrl,
291
+ role: input.role || 'user',
292
+ verified: input.verified ?? true,
293
+ locale: input.locale,
294
+ metadata: input.metadata,
295
+ appMetadata: input.appMetadata,
296
+ });
297
+
298
+ await confirmEmail(db, input.email, input.userId);
299
+ await syncPublicUserProjection(db, input.userId, authService.buildPublicUserData(user), {
300
+ executionCtx: options.executionCtx,
301
+ kv: options.kv,
302
+ awaitCacheWrites: false,
303
+ });
304
+
305
+ return user;
306
+ } catch (err) {
307
+ await cleanupCreatedUser(db, input.userId, input.email, options);
308
+ throw toEdgeBaseError(err, 500, 'User creation failed.');
309
+ }
310
+ }
311
+
312
+ export async function deleteManagedAdminUser(
313
+ db: AuthDb,
314
+ userId: string,
315
+ options: ManagedUserOptions = {},
316
+ ): Promise<boolean> {
317
+ const user = await authService.getUserById(db, userId);
318
+ if (!user) return false;
319
+
320
+ await db.batch([
321
+ { sql: `DELETE FROM _email_tokens WHERE userId = ?`, params: [userId] },
322
+ { sql: `DELETE FROM _sessions WHERE userId = ?`, params: [userId] },
323
+ { sql: `DELETE FROM _oauth_accounts WHERE userId = ?`, params: [userId] },
324
+ { sql: `DELETE FROM _mfa_recovery_codes WHERE userId = ?`, params: [userId] },
325
+ { sql: `DELETE FROM _mfa_factors WHERE userId = ?`, params: [userId] },
326
+ { sql: `DELETE FROM _webauthn_credentials WHERE userId = ?`, params: [userId] },
327
+ { sql: `DELETE FROM _passkey_index WHERE userId = ?`, params: [userId] },
328
+ { sql: `DELETE FROM _email_index WHERE userId = ?`, params: [userId] },
329
+ { sql: `DELETE FROM _phone_index WHERE userId = ?`, params: [userId] },
330
+ { sql: `DELETE FROM _oauth_index WHERE userId = ?`, params: [userId] },
331
+ { sql: `DELETE FROM _anon_index WHERE userId = ?`, params: [userId] },
332
+ { sql: `DELETE FROM _push_devices WHERE userId = ?`, params: [userId] },
333
+ { sql: `DELETE FROM _users_public WHERE id = ?`, params: [userId] },
334
+ { sql: `DELETE FROM _users WHERE id = ?`, params: [userId] },
335
+ ]);
336
+
337
+ invalidateManagedUserCaches(userId, options);
338
+
339
+ return true;
340
+ }
341
+
342
+ export async function updateManagedAdminUser(
343
+ db: AuthDb,
344
+ userId: string,
345
+ rawUpdates: Record<string, unknown>,
346
+ options: ManagedUserOptions = {},
347
+ ): Promise<Record<string, unknown> | null> {
348
+ const updates = await normalizeAdminUserUpdates(rawUpdates);
349
+ const existing = await authService.getUserById(db, userId);
350
+ if (!existing) return null;
351
+
352
+ const oldEmail = toNullableString(existing.email);
353
+ const oldPhone = toNullableString(existing.phone);
354
+ const newEmail = hasOwn(updates as Record<string, unknown>, 'email')
355
+ ? toNullableString((updates as Record<string, unknown>).email)
356
+ : undefined;
357
+ const newPhone = hasOwn(updates as Record<string, unknown>, 'phone')
358
+ ? ((updates as Record<string, unknown>).phone as string | null)
359
+ : undefined;
360
+ const emailChanged = newEmail !== undefined && newEmail !== oldEmail;
361
+ const phoneChanged = newPhone !== undefined && newPhone !== oldPhone;
362
+
363
+ if (emailChanged && newEmail) {
364
+ try {
365
+ await registerEmailPending(db, newEmail, userId);
366
+ } catch (err) {
367
+ if ((err as Error).message === 'EMAIL_ALREADY_REGISTERED') {
368
+ throw new EdgeBaseError(409, 'Email already registered.');
369
+ }
370
+ throw new EdgeBaseError(500, 'User update failed.');
371
+ }
372
+ }
373
+
374
+ if (phoneChanged && typeof newPhone === 'string') {
375
+ try {
376
+ await registerPhonePending(db, newPhone, userId);
377
+ } catch (err) {
378
+ if (newEmail) {
379
+ await deleteEmailPending(db, newEmail).catch(() => {});
380
+ }
381
+ if ((err as Error).message === 'PHONE_ALREADY_REGISTERED') {
382
+ throw new EdgeBaseError(409, 'Phone number is already registered.');
383
+ }
384
+ throw new EdgeBaseError(500, 'User update failed.');
385
+ }
386
+ }
387
+
388
+ const user = await authService.updateUser(db, userId, updates);
389
+ if (!user) {
390
+ if (newEmail) await deleteEmailPending(db, newEmail).catch(() => {});
391
+ if (typeof newPhone === 'string') await deletePhone(db, newPhone).catch(() => {});
392
+ return null;
393
+ }
394
+
395
+ const rollbackUpdates: authService.UpdateUserInput = {};
396
+ for (const key of Object.keys(updates as Record<string, unknown>)) {
397
+ if (UPDATABLE_USER_FIELDS.has(key)) {
398
+ (rollbackUpdates as Record<string, unknown>)[key] = toRollbackValue(existing, key);
399
+ }
400
+ }
401
+
402
+ try {
403
+ if (emailChanged && newEmail) {
404
+ await confirmEmail(db, newEmail, userId);
405
+ }
406
+ if (phoneChanged && typeof newPhone === 'string') {
407
+ await confirmPhone(db, newPhone, userId);
408
+ }
409
+
410
+ await syncPublicUserProjection(db, userId, authService.buildPublicUserData(user), {
411
+ executionCtx: options.executionCtx,
412
+ kv: options.kv,
413
+ awaitCacheWrites: false,
414
+ });
415
+
416
+ if (emailChanged && oldEmail) {
417
+ await deleteEmail(db, oldEmail);
418
+ }
419
+ if (phoneChanged && oldPhone) {
420
+ await deletePhone(db, oldPhone);
421
+ }
422
+
423
+ return user;
424
+ } catch (err) {
425
+ await authService.updateUser(db, userId, rollbackUpdates).catch(() => {});
426
+ await syncPublicUserProjection(db, userId, authService.buildPublicUserData(existing), {
427
+ executionCtx: options.executionCtx,
428
+ kv: options.kv,
429
+ awaitCacheWrites: false,
430
+ }).catch(() => {});
431
+ if (newEmail) await deleteEmail(db, newEmail).catch(() => {});
432
+ if (emailChanged && oldEmail) {
433
+ await ensureConfirmedEmailIndex(db, oldEmail, userId).catch((restoreErr) => {
434
+ console.error(`[EdgeBase] Failed to restore old email index for ${userId}:`, restoreErr);
435
+ });
436
+ }
437
+ if (typeof newPhone === 'string') await deletePhone(db, newPhone).catch(() => {});
438
+ if (phoneChanged && oldPhone) {
439
+ await ensureConfirmedPhoneIndex(db, oldPhone, userId).catch((restoreErr) => {
440
+ console.error(`[EdgeBase] Failed to restore old phone index for ${userId}:`, restoreErr);
441
+ });
442
+ }
443
+ throw toEdgeBaseError(err, 500, 'User update failed.');
444
+ }
445
+ }
446
+
447
+ export async function prepareImportedPasswordHash(user: {
448
+ passwordHash?: string;
449
+ password?: string;
450
+ }): Promise<string> {
451
+ if (user.passwordHash && isPasswordHash(user.passwordHash)) {
452
+ return user.passwordHash;
453
+ }
454
+ if (user.password) {
455
+ if (user.password.length < 8) {
456
+ throw new EdgeBaseError(400, 'Password must be at least 8 characters.');
457
+ }
458
+ if (user.password.length > 256) {
459
+ throw new EdgeBaseError(400, 'Password must not exceed 256 characters.');
460
+ }
461
+ return hashPassword(user.password);
462
+ }
463
+ return '';
464
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * AnalyticsAdapter pattern.
3
+ *
4
+ * Provides environment-aware analytics storage for App Functions:
5
+ * - Cloud: AnalyticsEngineAdapter (separate ANALYTICS_APP dataset)
6
+ * - Self-hosted: ConsoleAnalyticsAdapter (fallback)
7
+ *
8
+ * Separate from LogWriter (#83) — different lifecycle & retention policy.
9
+ * LogWriter = operational metrics, AnalyticsAdapter = business analytics.
10
+ *
11
+ * Usage (inside App Functions):
12
+ * ctx.analytics.write({ event: 'page_view', blobs: ['/home'], doubles: [1] });
13
+ * const result = await ctx.analytics.query({ sql: 'SELECT ...' });
14
+ */
15
+
16
+ // ─── Types ───
17
+
18
+ export interface AnalyticsDataPoint {
19
+ event: string; // e.g. 'page_view', 'button_click'
20
+ blobs?: string[]; // String dimensions (max 20)
21
+ doubles?: number[]; // Numeric metrics (max 20)
22
+ timestamp?: number; // Default: Date.now()
23
+ }
24
+
25
+ export interface AnalyticsQuery {
26
+ sql: string; // Cloud: Analytics Engine SQL API, self-hosted: SQLite SQL
27
+ timeRange?: { start: string; end: string };
28
+ }
29
+
30
+ export interface AnalyticsResult {
31
+ data: Record<string, unknown>[];
32
+ }
33
+
34
+ export interface AnalyticsAdapter {
35
+ write(dataPoint: AnalyticsDataPoint): void;
36
+ query(params: AnalyticsQuery): Promise<AnalyticsResult>;
37
+ }
38
+
39
+ // ─── AnalyticsEngineAdapter (Cloud) ───
40
+
41
+ /**
42
+ * Analytics adapter using a dedicated Cloudflare Analytics Engine dataset.
43
+ * Separate from LogWriter's ANALYTICS binding — uses ANALYTICS_APP binding.
44
+ * Non-blocking fire-and-forget write. SQL API for queries.
45
+ */
46
+ export class AnalyticsEngineAdapter implements AnalyticsAdapter {
47
+ constructor(private engine: AnalyticsEngineDataset) {}
48
+
49
+ write(dp: AnalyticsDataPoint): void {
50
+ this.engine.writeDataPoint({
51
+ indexes: [dp.event],
52
+ blobs: [dp.event, ...(dp.blobs || [])],
53
+ doubles: [...(dp.doubles || []), dp.timestamp || Date.now()],
54
+ });
55
+ }
56
+
57
+ async query(_params: AnalyticsQuery): Promise<AnalyticsResult> {
58
+ // Analytics Engine SQL API requires account-level credentials
59
+ // and is called via the Cloudflare REST API, not from Workers.
60
+ // App Functions should use client.functions.call() → server-side query endpoint.
61
+ return { data: [] };
62
+ }
63
+ }
64
+
65
+ // ─── ConsoleAnalyticsAdapter (Self-hosted / Dev fallback) ───
66
+
67
+ /**
68
+ * Fallback analytics adapter using console.log.
69
+ * Used when neither Analytics Engine nor SQLite analytics DB is available.
70
+ *
71
+ * Note: In production self-hosted, this would use a dedicated SQLite file (analytics.db).
72
+ * For M19, we implement the interface and fall back to console logging
73
+ * when ANALYTICS_APP binding is not present.
74
+ */
75
+ export class ConsoleAnalyticsAdapter implements AnalyticsAdapter {
76
+ write(dp: AnalyticsDataPoint): void {
77
+ const ts = new Date(dp.timestamp || Date.now()).toISOString();
78
+ console.log(
79
+ `[EdgeBase:Analytics] ${ts} event=${dp.event}` +
80
+ (dp.blobs?.length ? ` blobs=[${dp.blobs.join(',')}]` : '') +
81
+ (dp.doubles?.length ? ` doubles=[${dp.doubles.join(',')}]` : ''),
82
+ );
83
+ }
84
+
85
+ async query(_params: AnalyticsQuery): Promise<AnalyticsResult> {
86
+ return { data: [] };
87
+ }
88
+ }
89
+
90
+ // ─── Factory ───
91
+
92
+ /**
93
+ * Create the appropriate AnalyticsAdapter based on environment bindings.
94
+ * - `env.ANALYTICS_APP` exists → AnalyticsEngineAdapter (separate from LogWriter's ANALYTICS)
95
+ * - Otherwise → ConsoleAnalyticsAdapter (fallback)
96
+ */
97
+ export function createAnalyticsAdapter(env: Record<string, unknown>): AnalyticsAdapter {
98
+ const analyticsApp = env.ANALYTICS_APP as AnalyticsEngineDataset | undefined;
99
+ if (analyticsApp) {
100
+ return new AnalyticsEngineAdapter(analyticsApp);
101
+ }
102
+ return new ConsoleAnalyticsAdapter();
103
+ }