@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,418 @@
1
+ /**
2
+ * 서버 단위 테스트 — lib/push-token.ts
3
+ *
4
+ * 실행: cd packages/server && npx vitest run src/__tests__/push-token.test.ts
5
+ *
6
+ * 테스트 대상:
7
+ * registerToken — 새 디바이스 등록, 기존 업데이트, MAX_DEVICES 초과 시 oldest 제거
8
+ * unregisterToken — 특정 디바이스 삭제
9
+ * getDevicesForUser — 디바이스 목록, 빈 목록, 잘못된 JSON 처리
10
+ * removeDeviceFromUser — 디바이스 제거, 빈 배열 시 키 삭제, 없는 디바이스 무시
11
+ * unregisterAllTokens — 전체 삭제
12
+ * storePushLog — 로그 저장 (TTL)
13
+ * getPushLogs — 로그 조회, 잘못된 JSON 스킵
14
+ */
15
+
16
+ import { describe, it, expect } from 'vitest';
17
+ import {
18
+ registerToken,
19
+ unregisterToken,
20
+ getDevicesForUser,
21
+ removeDeviceFromUser,
22
+ unregisterAllTokens,
23
+ storePushLog,
24
+ getPushLogs,
25
+ } from '../lib/push-token.js';
26
+ import type { PushLogEntry } from '../lib/push-token.js';
27
+ import type { AuthDb } from '../lib/auth-db-adapter.js';
28
+
29
+ // ─── Mock KVNamespace ────────────────────────────────────────────────────────
30
+
31
+ interface MockKVStore {
32
+ data: Record<string, { value: string; options?: { expirationTtl?: number } }>;
33
+ }
34
+
35
+ function createMockKV(): KVNamespace & { _store: MockKVStore } {
36
+ const store: MockKVStore = { data: {} };
37
+
38
+ const kv = {
39
+ get: async (key: string) => store.data[key]?.value ?? null,
40
+ put: async (key: string, value: string, options?: { expirationTtl?: number }) => {
41
+ store.data[key] = { value, options };
42
+ },
43
+ delete: async (key: string) => {
44
+ delete store.data[key];
45
+ },
46
+ list: async (opts: { prefix: string; limit?: number }) => {
47
+ const matchingKeys = Object.keys(store.data)
48
+ .filter((k) => k.startsWith(opts.prefix))
49
+ .slice(0, opts.limit ?? 100)
50
+ .map((name) => ({ name, expiration: undefined, metadata: undefined }));
51
+ return { keys: matchingKeys, list_complete: true, cacheStatus: null };
52
+ },
53
+ getWithMetadata: async () => ({ value: null, metadata: null, cacheStatus: null }),
54
+ _store: store,
55
+ };
56
+
57
+ return kv as any;
58
+ }
59
+
60
+ function createMockAuthDb(): AuthDb & { _pushDevices: Map<string, Array<Record<string, unknown>>> } {
61
+ const pushDevices = new Map<string, Array<Record<string, unknown>>>();
62
+
63
+ return {
64
+ dialect: 'sqlite',
65
+ async query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]> {
66
+ if (sql.includes('FROM _push_devices')) {
67
+ const userId = String(params?.[0] ?? '');
68
+ const rows = [...(pushDevices.get(userId) ?? [])]
69
+ .sort((left, right) => {
70
+ const leftUpdated = String(left.updatedAt ?? '');
71
+ const rightUpdated = String(right.updatedAt ?? '');
72
+ return leftUpdated.localeCompare(rightUpdated) || String(left.deviceId ?? '').localeCompare(String(right.deviceId ?? ''));
73
+ });
74
+ return rows as T[];
75
+ }
76
+ throw new Error(`Unsupported query in mock auth db: ${sql}`);
77
+ },
78
+ async first<T = Record<string, unknown>>(): Promise<T | null> {
79
+ return null;
80
+ },
81
+ async run(): Promise<void> {},
82
+ async batch(statements: { sql: string; params?: unknown[] }[]): Promise<void> {
83
+ for (const statement of statements) {
84
+ if (statement.sql.startsWith('DELETE FROM _push_devices WHERE userId = ?')) {
85
+ const userId = String(statement.params?.[0] ?? '');
86
+ pushDevices.delete(userId);
87
+ continue;
88
+ }
89
+ if (statement.sql.includes('INSERT INTO _push_devices')) {
90
+ const [userId, deviceId, token, platform, updatedAt, deviceInfo, metadata] = statement.params ?? [];
91
+ const rows = pushDevices.get(String(userId)) ?? [];
92
+ rows.push({
93
+ userId,
94
+ deviceId,
95
+ token,
96
+ platform,
97
+ updatedAt,
98
+ deviceInfo,
99
+ metadata,
100
+ });
101
+ pushDevices.set(String(userId), rows);
102
+ continue;
103
+ }
104
+ throw new Error(`Unsupported batch statement in mock auth db: ${statement.sql}`);
105
+ }
106
+ },
107
+ _pushDevices: pushDevices,
108
+ };
109
+ }
110
+
111
+ // ─── registerToken ───────────────────────────────────────────────────────────
112
+
113
+ describe('registerToken', () => {
114
+ it('registers a new device', async () => {
115
+ const kv = createMockKV();
116
+ await registerToken(kv, 'user-1', 'device-a', 'token-a', 'ios');
117
+
118
+ const devices = await getDevicesForUser(kv, 'user-1');
119
+ expect(devices).toHaveLength(1);
120
+ expect(devices[0].deviceId).toBe('device-a');
121
+ expect(devices[0].token).toBe('token-a');
122
+ expect(devices[0].platform).toBe('ios');
123
+ });
124
+
125
+ it('updates existing device token', async () => {
126
+ const kv = createMockKV();
127
+ await registerToken(kv, 'user-1', 'device-a', 'old-token', 'ios');
128
+ await registerToken(kv, 'user-1', 'device-a', 'new-token', 'ios');
129
+
130
+ const devices = await getDevicesForUser(kv, 'user-1');
131
+ expect(devices).toHaveLength(1);
132
+ expect(devices[0].token).toBe('new-token');
133
+ });
134
+
135
+ it('adds multiple devices for same user', async () => {
136
+ const kv = createMockKV();
137
+ await registerToken(kv, 'user-1', 'device-a', 'token-a', 'ios');
138
+ await registerToken(kv, 'user-1', 'device-b', 'token-b', 'android');
139
+
140
+ const devices = await getDevicesForUser(kv, 'user-1');
141
+ expect(devices).toHaveLength(2);
142
+ });
143
+
144
+ it('enforces MAX_DEVICES (10) — removes oldest', async () => {
145
+ const kv = createMockKV();
146
+ // Register 11 devices
147
+ for (let i = 0; i < 11; i++) {
148
+ await registerToken(kv, 'user-1', `device-${i}`, `token-${i}`, 'ios');
149
+ }
150
+
151
+ const devices = await getDevicesForUser(kv, 'user-1');
152
+ expect(devices).toHaveLength(10);
153
+ // device-0 should be removed (oldest)
154
+ expect(devices.find((d) => d.deviceId === 'device-0')).toBeUndefined();
155
+ // device-10 should be present (newest)
156
+ expect(devices.find((d) => d.deviceId === 'device-10')).toBeDefined();
157
+ });
158
+
159
+ it('stores deviceInfo and metadata', async () => {
160
+ const kv = createMockKV();
161
+ await registerToken(kv, 'user-1', 'device-a', 'token-a', 'ios', {
162
+ name: 'iPhone 15',
163
+ osVersion: '17.0',
164
+ }, { key: 'value' });
165
+
166
+ const devices = await getDevicesForUser(kv, 'user-1');
167
+ expect(devices[0].deviceInfo?.name).toBe('iPhone 15');
168
+ expect(devices[0].metadata).toEqual({ key: 'value' });
169
+ });
170
+
171
+ it('writes AUTH_DB-backed inventory and mirrors KV', async () => {
172
+ const kv = createMockKV();
173
+ const authDb = createMockAuthDb();
174
+
175
+ await registerToken({ kv, authDb }, 'user-1', 'device-a', 'token-a', 'ios', {
176
+ name: 'iPhone 15',
177
+ }, { build: '1' });
178
+
179
+ const devices = await getDevicesForUser({ kv, authDb }, 'user-1');
180
+ expect(devices).toHaveLength(1);
181
+ expect(devices[0].deviceId).toBe('device-a');
182
+ expect(devices[0].deviceInfo?.name).toBe('iPhone 15');
183
+ expect(devices[0].metadata).toEqual({ build: '1' });
184
+ expect(JSON.parse(kv._store.data['push:user:user-1'].value)).toHaveLength(1);
185
+ });
186
+ });
187
+
188
+ // ─── unregisterToken ─────────────────────────────────────────────────────────
189
+
190
+ describe('unregisterToken', () => {
191
+ it('removes specific device', async () => {
192
+ const kv = createMockKV();
193
+ await registerToken(kv, 'user-1', 'device-a', 'token-a', 'ios');
194
+ await registerToken(kv, 'user-1', 'device-b', 'token-b', 'android');
195
+
196
+ await unregisterToken(kv, 'user-1', 'device-a');
197
+
198
+ const devices = await getDevicesForUser(kv, 'user-1');
199
+ expect(devices).toHaveLength(1);
200
+ expect(devices[0].deviceId).toBe('device-b');
201
+ });
202
+
203
+ it('removes KV key when last device unregistered', async () => {
204
+ const kv = createMockKV();
205
+ await registerToken(kv, 'user-1', 'device-a', 'token-a', 'ios');
206
+ await unregisterToken(kv, 'user-1', 'device-a');
207
+
208
+ expect(kv._store.data['push:user:user-1']).toBeUndefined();
209
+ });
210
+ });
211
+
212
+ // ─── getDevicesForUser ───────────────────────────────────────────────────────
213
+
214
+ describe('getDevicesForUser', () => {
215
+ it('returns empty array for unknown user', async () => {
216
+ const kv = createMockKV();
217
+ const devices = await getDevicesForUser(kv, 'unknown');
218
+ expect(devices).toEqual([]);
219
+ });
220
+
221
+ it('returns empty array for invalid JSON', async () => {
222
+ const kv = createMockKV();
223
+ kv._store.data['push:user:user-1'] = { value: 'not-json' };
224
+ const devices = await getDevicesForUser(kv, 'user-1');
225
+ expect(devices).toEqual([]);
226
+ });
227
+
228
+ it('backfills AUTH_DB from KV when migrated rows are missing', async () => {
229
+ const kv = createMockKV();
230
+ const authDb = createMockAuthDb();
231
+ kv._store.data['push:user:user-1'] = {
232
+ value: JSON.stringify([
233
+ {
234
+ deviceId: 'device-a',
235
+ token: 'token-a',
236
+ platform: 'ios',
237
+ updatedAt: '2026-03-09T00:00:00.000Z',
238
+ metadata: { role: 'primary' },
239
+ },
240
+ ]),
241
+ };
242
+
243
+ const devices = await getDevicesForUser({ kv, authDb }, 'user-1');
244
+ expect(devices).toHaveLength(1);
245
+ expect(authDb._pushDevices.get('user-1')).toHaveLength(1);
246
+ });
247
+ });
248
+
249
+ // ─── removeDeviceFromUser ────────────────────────────────────────────────────
250
+
251
+ describe('removeDeviceFromUser', () => {
252
+ it('removes specific device and keeps others', async () => {
253
+ const kv = createMockKV();
254
+ await registerToken(kv, 'user-1', 'dev-a', 'tok-a', 'ios');
255
+ await registerToken(kv, 'user-1', 'dev-b', 'tok-b', 'android');
256
+
257
+ await removeDeviceFromUser(kv, 'user-1', 'dev-a');
258
+
259
+ const devices = await getDevicesForUser(kv, 'user-1');
260
+ expect(devices).toHaveLength(1);
261
+ expect(devices[0].deviceId).toBe('dev-b');
262
+ });
263
+
264
+ it('deletes KV key when removing last device', async () => {
265
+ const kv = createMockKV();
266
+ await registerToken(kv, 'user-1', 'dev-a', 'tok-a', 'ios');
267
+ await removeDeviceFromUser(kv, 'user-1', 'dev-a');
268
+
269
+ expect(kv._store.data['push:user:user-1']).toBeUndefined();
270
+ });
271
+
272
+ it('no-op when device not found (no write)', async () => {
273
+ const kv = createMockKV();
274
+ await registerToken(kv, 'user-1', 'dev-a', 'tok-a', 'ios');
275
+
276
+ const before = kv._store.data['push:user:user-1']?.value;
277
+ await removeDeviceFromUser(kv, 'user-1', 'dev-nonexistent');
278
+ const after = kv._store.data['push:user:user-1']?.value;
279
+
280
+ // Value should be unchanged
281
+ expect(after).toBe(before);
282
+ });
283
+ });
284
+
285
+ // ─── unregisterAllTokens ─────────────────────────────────────────────────────
286
+
287
+ describe('unregisterAllTokens', () => {
288
+ it('removes all devices for a user', async () => {
289
+ const kv = createMockKV();
290
+ await registerToken(kv, 'user-1', 'dev-a', 'tok-a', 'ios');
291
+ await registerToken(kv, 'user-1', 'dev-b', 'tok-b', 'android');
292
+
293
+ await unregisterAllTokens(kv, 'user-1');
294
+
295
+ expect(kv._store.data['push:user:user-1']).toBeUndefined();
296
+ const devices = await getDevicesForUser(kv, 'user-1');
297
+ expect(devices).toEqual([]);
298
+ });
299
+
300
+ it('removes AUTH_DB-backed devices as well', async () => {
301
+ const kv = createMockKV();
302
+ const authDb = createMockAuthDb();
303
+ await registerToken({ kv, authDb }, 'user-1', 'dev-a', 'tok-a', 'ios');
304
+ await registerToken({ kv, authDb }, 'user-1', 'dev-b', 'tok-b', 'android');
305
+
306
+ await unregisterAllTokens({ kv, authDb }, 'user-1');
307
+
308
+ expect(kv._store.data['push:user:user-1']).toBeUndefined();
309
+ expect(authDb._pushDevices.get('user-1')).toBeUndefined();
310
+ });
311
+ });
312
+
313
+ // ─── storePushLog ────────────────────────────────────────────────────────────
314
+
315
+ describe('storePushLog', () => {
316
+ it('stores log entry with TTL', async () => {
317
+ const kv = createMockKV();
318
+ const entry: PushLogEntry = {
319
+ sentAt: '2024-01-01T00:00:00Z',
320
+ userId: 'user-1',
321
+ platform: 'ios',
322
+ status: 'sent',
323
+ };
324
+
325
+ await storePushLog(kv, 'user-1', entry);
326
+
327
+ const keys = Object.keys(kv._store.data).filter((k) => k.startsWith('push:log:user-1:'));
328
+ expect(keys).toHaveLength(1);
329
+
330
+ // Verify TTL was set (24 hours = 86400 seconds)
331
+ const stored = kv._store.data[keys[0]];
332
+ expect(stored.options?.expirationTtl).toBe(86400);
333
+
334
+ // Verify content
335
+ const parsed = JSON.parse(stored.value);
336
+ expect(parsed.status).toBe('sent');
337
+ expect(parsed.platform).toBe('ios');
338
+ });
339
+
340
+ it('stores log with error info', async () => {
341
+ const kv = createMockKV();
342
+ const entry: PushLogEntry = {
343
+ sentAt: '2024-01-01T00:00:00Z',
344
+ userId: 'user-1',
345
+ platform: 'android',
346
+ status: 'failed',
347
+ error: 'FCM_INVALID_TOKEN',
348
+ };
349
+
350
+ await storePushLog(kv, 'user-1', entry);
351
+
352
+ const keys = Object.keys(kv._store.data).filter((k) => k.startsWith('push:log:user-1:'));
353
+ const parsed = JSON.parse(kv._store.data[keys[0]].value);
354
+ expect(parsed.status).toBe('failed');
355
+ expect(parsed.error).toBe('FCM_INVALID_TOKEN');
356
+ });
357
+ });
358
+
359
+ // ─── getPushLogs ─────────────────────────────────────────────────────────────
360
+
361
+ describe('getPushLogs', () => {
362
+ it('returns empty array when no logs', async () => {
363
+ const kv = createMockKV();
364
+ const logs = await getPushLogs(kv, 'user-1');
365
+ expect(logs).toEqual([]);
366
+ });
367
+
368
+ it('returns stored log entries', async () => {
369
+ const kv = createMockKV();
370
+ const entry: PushLogEntry = {
371
+ sentAt: '2024-01-01T00:00:00Z',
372
+ userId: 'user-1',
373
+ platform: 'ios',
374
+ status: 'sent',
375
+ };
376
+ await storePushLog(kv, 'user-1', entry);
377
+
378
+ const logs = await getPushLogs(kv, 'user-1');
379
+ expect(logs).toHaveLength(1);
380
+ expect(logs[0].status).toBe('sent');
381
+ });
382
+
383
+ it('skips corrupted log entries', async () => {
384
+ const kv = createMockKV();
385
+
386
+ // Store a valid entry
387
+ const entry: PushLogEntry = {
388
+ sentAt: '2024-01-01T00:00:00Z',
389
+ userId: 'user-1',
390
+ platform: 'ios',
391
+ status: 'sent',
392
+ };
393
+ await storePushLog(kv, 'user-1', entry);
394
+
395
+ // Manually corrupt another entry
396
+ kv._store.data['push:log:user-1:999:xxxx'] = { value: 'not-json' };
397
+
398
+ const logs = await getPushLogs(kv, 'user-1');
399
+ // Should have 1 valid + 1 skipped = 1 returned
400
+ expect(logs).toHaveLength(1);
401
+ expect(logs[0].status).toBe('sent');
402
+ });
403
+
404
+ it('respects limit parameter', async () => {
405
+ const kv = createMockKV();
406
+ for (let i = 0; i < 5; i++) {
407
+ await storePushLog(kv, 'user-1', {
408
+ sentAt: `2024-01-0${i + 1}T00:00:00Z`,
409
+ userId: 'user-1',
410
+ platform: 'ios',
411
+ status: 'sent',
412
+ });
413
+ }
414
+
415
+ const logs = await getPushLogs(kv, 'user-1', 3);
416
+ expect(logs.length).toBeLessThanOrEqual(3);
417
+ });
418
+ });