@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,260 @@
1
+ /**
2
+ * Regression tests for rate-limit helpers.
3
+ *
4
+ * Key regression: requests=0 must be honoured (ban-mode),
5
+ * not silently swallowed by a truthy check.
6
+ */
7
+ import { describe, it, expect } from 'vitest';
8
+ import { Hono } from 'hono';
9
+ import {
10
+ counter,
11
+ getLimit,
12
+ parseWindow,
13
+ FixedWindowCounter,
14
+ RATE_LIMIT_DEFAULTS,
15
+ rateLimitMiddleware,
16
+ } from '../middleware/rate-limit.js';
17
+ import type { EdgeBaseConfig } from '@edge-base/shared';
18
+ import { setConfig } from '../lib/do-router.js';
19
+
20
+ // ─── parseWindow ───
21
+
22
+ describe('parseWindow', () => {
23
+ it('parses seconds', () => {
24
+ expect(parseWindow('30s')).toBe(30);
25
+ });
26
+
27
+ it('parses minutes', () => {
28
+ expect(parseWindow('5m')).toBe(300);
29
+ });
30
+
31
+ it('parses hours', () => {
32
+ expect(parseWindow('1h')).toBe(3600);
33
+ });
34
+
35
+ it('falls back to 60 for invalid format', () => {
36
+ expect(parseWindow('abc')).toBe(60);
37
+ expect(parseWindow('')).toBe(60);
38
+ expect(parseWindow('10')).toBe(60);
39
+ });
40
+ });
41
+
42
+ // ─── getLimit ───
43
+
44
+ describe('getLimit', () => {
45
+ it('returns defaults when config is undefined', () => {
46
+ const result = getLimit(undefined, 'db');
47
+ expect(result).toEqual(RATE_LIMIT_DEFAULTS['db']);
48
+ });
49
+
50
+ it('returns defaults when rateLimiting is not set', () => {
51
+ const config = {} as EdgeBaseConfig;
52
+ const result = getLimit(config, 'db');
53
+ expect(result).toEqual(RATE_LIMIT_DEFAULTS['db']);
54
+ });
55
+
56
+ it('returns defaults for unknown group', () => {
57
+ const result = getLimit(undefined, 'nonexistent');
58
+ expect(result).toEqual({ requests: 10_000_000, windowSec: 60 });
59
+ });
60
+
61
+ it('uses config values when set', () => {
62
+ const config = {
63
+ rateLimiting: {
64
+ db: { requests: 500, window: '2m' },
65
+ },
66
+ } as EdgeBaseConfig;
67
+ expect(getLimit(config, 'db')).toEqual({ requests: 500, windowSec: 120 });
68
+ });
69
+
70
+ // ── REGRESSION: requests=0 must NOT fall through to defaults ──
71
+ it('honours requests=0 (ban-mode) — does not treat 0 as falsy', () => {
72
+ const config = {
73
+ rateLimiting: {
74
+ db: { requests: 0, window: '60s' },
75
+ },
76
+ } as EdgeBaseConfig;
77
+ const result = getLimit(config, 'db');
78
+ expect(result).toEqual({ requests: 0, windowSec: 60 });
79
+ });
80
+
81
+ it('falls back when group exists but has no requests/window', () => {
82
+ const config = {
83
+ rateLimiting: {
84
+ db: undefined,
85
+ },
86
+ } as EdgeBaseConfig;
87
+ expect(getLimit(config, 'db')).toEqual(RATE_LIMIT_DEFAULTS['db']);
88
+ });
89
+ });
90
+
91
+ // ─── FixedWindowCounter ───
92
+
93
+ describe('FixedWindowCounter', () => {
94
+ it('allows requests within limit', () => {
95
+ const counter = new FixedWindowCounter();
96
+ expect(counter.check('test:key', 3, 60)).toBe(true);
97
+ expect(counter.check('test:key', 3, 60)).toBe(true);
98
+ expect(counter.check('test:key', 3, 60)).toBe(true);
99
+ });
100
+
101
+ it('blocks requests over limit', () => {
102
+ const counter = new FixedWindowCounter();
103
+ counter.check('over:key', 2, 60);
104
+ counter.check('over:key', 2, 60);
105
+ expect(counter.check('over:key', 2, 60)).toBe(false);
106
+ });
107
+
108
+ // ── REGRESSION: limit=0 must block all requests (ban-mode) ──
109
+ it('blocks all requests when limit=0 (ban-mode)', () => {
110
+ const counter = new FixedWindowCounter();
111
+ expect(counter.check('ban:key', 0, 60)).toBe(false);
112
+ });
113
+ });
114
+
115
+ // ─── rateLimitMiddleware ───
116
+
117
+ describe('rateLimitMiddleware — valid Service Key bypass', () => {
118
+ function createApp(config: EdgeBaseConfig) {
119
+ setConfig(config);
120
+ const app = new Hono();
121
+ app.use('*', rateLimitMiddleware as never);
122
+ app.get('/api/health', (c) => c.json({ ok: true }));
123
+ app.get('/api/db/shared/tables/posts', (c) => c.json({ ok: true }));
124
+ return app;
125
+ }
126
+
127
+ const env = {
128
+ SERVICE_KEY: 'sk-root-test',
129
+ ENVIRONMENT: 'test',
130
+ };
131
+
132
+ const config = {
133
+ rateLimiting: {
134
+ global: { requests: 1, window: '60s' },
135
+ db: { requests: 1, window: '60s' },
136
+ },
137
+ serviceKeys: {
138
+ keys: [
139
+ {
140
+ kid: 'root-test',
141
+ tier: 'root',
142
+ scopes: ['*'],
143
+ secretSource: 'dashboard',
144
+ secretRef: 'SERVICE_KEY',
145
+ },
146
+ {
147
+ kid: 'db-scoped',
148
+ tier: 'scoped',
149
+ scopes: ['db:table:posts:read'],
150
+ secretSource: 'inline',
151
+ inlineSecret: 'jb_db-scoped_payload',
152
+ },
153
+ {
154
+ kid: 'constrained-db',
155
+ tier: 'scoped',
156
+ scopes: ['db:table:posts:read'],
157
+ secretSource: 'inline',
158
+ inlineSecret: 'jb_constrained-db_payload',
159
+ constraints: {
160
+ env: ['prod'],
161
+ ipCidr: ['10.0.0.0/8'],
162
+ },
163
+ },
164
+ ],
165
+ },
166
+ } as EdgeBaseConfig;
167
+
168
+ it('bypasses the global limiter for valid service key requests', async () => {
169
+ const app = createApp(config);
170
+ const ip = `rl-sk-global-${Date.now()}`;
171
+ counter.check(`global:${ip}`, 1, 60);
172
+
173
+ const response = await app.fetch(
174
+ new Request('http://localhost/api/health', {
175
+ headers: {
176
+ 'cf-connecting-ip': ip,
177
+ 'X-EdgeBase-Service-Key': 'sk-root-test',
178
+ },
179
+ }),
180
+ env,
181
+ {} as ExecutionContext,
182
+ );
183
+
184
+ expect(response.status).toBe(200);
185
+ });
186
+
187
+ it('bypasses group-specific limiters for valid service key requests', async () => {
188
+ const app = createApp(config);
189
+ const ip = `rl-sk-db-${Date.now()}`;
190
+ counter.check(`db:${ip}`, 1, 60);
191
+ counter.check(`global:${ip}`, 1, 60);
192
+
193
+ const response = await app.fetch(
194
+ new Request('http://localhost/api/db/shared/tables/posts', {
195
+ headers: {
196
+ 'cf-connecting-ip': ip,
197
+ 'X-EdgeBase-Service-Key': 'sk-root-test',
198
+ },
199
+ }),
200
+ env,
201
+ {} as ExecutionContext,
202
+ );
203
+
204
+ expect(response.status).toBe(200);
205
+ });
206
+
207
+ it('bypasses for scoped service key requests too', async () => {
208
+ const app = createApp(config);
209
+ const ip = `rl-sk-scoped-${Date.now()}`;
210
+ counter.check(`db:${ip}`, 1, 60);
211
+ counter.check(`global:${ip}`, 1, 60);
212
+
213
+ const response = await app.fetch(
214
+ new Request('http://localhost/api/db/shared/tables/posts', {
215
+ headers: {
216
+ 'cf-connecting-ip': ip,
217
+ 'X-EdgeBase-Service-Key': 'jb_db-scoped_payload',
218
+ },
219
+ }),
220
+ env,
221
+ {} as ExecutionContext,
222
+ );
223
+
224
+ expect(response.status).toBe(200);
225
+ });
226
+
227
+ it('still requires constraints to pass before bypassing', async () => {
228
+ const app = createApp(config);
229
+ const allowedIp = '10.1.2.3';
230
+ counter.check(`db:${allowedIp}`, 1, 60);
231
+ counter.check(`global:${allowedIp}`, 1, 60);
232
+ const prodEnv = { ...env, ENVIRONMENT: 'prod' };
233
+ const devEnv = { ...env, ENVIRONMENT: 'dev' };
234
+
235
+ const allowed = await app.fetch(
236
+ new Request('http://localhost/api/db/shared/tables/posts', {
237
+ headers: {
238
+ 'cf-connecting-ip': allowedIp,
239
+ 'X-EdgeBase-Service-Key': 'jb_constrained-db_payload',
240
+ },
241
+ }),
242
+ prodEnv as never,
243
+ {} as ExecutionContext,
244
+ );
245
+
246
+ const denied = await app.fetch(
247
+ new Request('http://localhost/api/db/shared/tables/posts', {
248
+ headers: {
249
+ 'cf-connecting-ip': allowedIp,
250
+ 'X-EdgeBase-Service-Key': 'jb_constrained-db_payload',
251
+ },
252
+ }),
253
+ devEnv as never,
254
+ {} as ExecutionContext,
255
+ );
256
+
257
+ expect(allowed.status).toBe(200);
258
+ expect(denied.status).toBe(429);
259
+ });
260
+ });
@@ -0,0 +1,101 @@
1
+ import { afterEach, describe, expect, it } from 'vitest';
2
+ import { defineConfig, type AuthContext } from '@edge-base/shared';
3
+ import { setConfig } from '../lib/do-router.js';
4
+ import { OpenAPIHono, type HonoEnv } from '../lib/hono.js';
5
+ import { roomRoute } from '../routes/room.js';
6
+ import type { Env } from '../types.js';
7
+
8
+ function createRoomEnv(): Env {
9
+ return {
10
+ ROOMS: {
11
+ idFromName: (name: string) => name as unknown as DurableObjectId,
12
+ get: () => ({
13
+ fetch: async () => new Response(JSON.stringify({ visibility: 'public' }), {
14
+ headers: { 'Content-Type': 'application/json' },
15
+ }),
16
+ }),
17
+ } as unknown as DurableObjectNamespace,
18
+ } as Env;
19
+ }
20
+
21
+ function createApp(auth?: AuthContext | null) {
22
+ const app = new OpenAPIHono<HonoEnv>();
23
+ app.use('/api/room/*', async (c, next) => {
24
+ c.set('auth', (auth ?? null) as never);
25
+ await next();
26
+ });
27
+ app.route('/api/room', roomRoute);
28
+ return app;
29
+ }
30
+
31
+ describe('Room route access policy', () => {
32
+ afterEach(() => {
33
+ setConfig({});
34
+ });
35
+
36
+ it('denies metadata in release mode without access.metadata or public.metadata', async () => {
37
+ setConfig(defineConfig({
38
+ release: true,
39
+ rooms: {
40
+ game: {},
41
+ },
42
+ }));
43
+
44
+ const app = createApp();
45
+ const response = await app.request('/api/room/metadata?namespace=game&id=room-1', {
46
+ method: 'GET',
47
+ }, createRoomEnv());
48
+
49
+ expect(response.status).toBe(403);
50
+ await expect(response.json()).resolves.toMatchObject({
51
+ message: 'Room metadata requires access.metadata or public.metadata in release mode',
52
+ });
53
+ });
54
+
55
+ it('allows metadata when public.metadata is explicitly enabled', async () => {
56
+ setConfig(defineConfig({
57
+ release: true,
58
+ rooms: {
59
+ game: {
60
+ public: {
61
+ metadata: true,
62
+ },
63
+ },
64
+ },
65
+ }));
66
+
67
+ const app = createApp();
68
+ const response = await app.request('/api/room/metadata?namespace=game&id=room-1', {
69
+ method: 'GET',
70
+ }, createRoomEnv());
71
+
72
+ expect(response.status).toBe(200);
73
+ await expect(response.json()).resolves.toEqual({ visibility: 'public' });
74
+ });
75
+
76
+ it('passes enriched auth context into metadata access rules', async () => {
77
+ setConfig(defineConfig({
78
+ release: true,
79
+ rooms: {
80
+ game: {
81
+ access: {
82
+ metadata: (auth, roomId) => auth?.meta?.tenant === 'team-1' && roomId === 'room-1',
83
+ },
84
+ },
85
+ },
86
+ }));
87
+
88
+ const app = createApp({
89
+ id: 'user-1',
90
+ role: 'user',
91
+ isAnonymous: false,
92
+ meta: { tenant: 'team-1' },
93
+ });
94
+ const response = await app.request('/api/room/metadata?namespace=game&id=room-1', {
95
+ method: 'GET',
96
+ }, createRoomEnv());
97
+
98
+ expect(response.status).toBe(200);
99
+ await expect(response.json()).resolves.toEqual({ visibility: 'public' });
100
+ });
101
+ });
@@ -0,0 +1,130 @@
1
+ import { describe, expect, it, vi, afterEach } from 'vitest';
2
+
3
+ vi.mock('cloudflare:workers', () => ({
4
+ DurableObject: class DurableObject {},
5
+ }));
6
+
7
+ describe('RoomsDO handler context', () => {
8
+ afterEach(() => {
9
+ vi.unstubAllGlobals();
10
+ });
11
+
12
+ it('routes admin.db() through the database durable object instead of worker fetch', async () => {
13
+ const { RoomsDO } = await import('../durable-objects/rooms-do.js');
14
+ const workerFetch = vi.fn().mockRejectedValue(new Error('worker fetch should not be used'));
15
+ vi.stubGlobal('fetch', workerFetch);
16
+
17
+ const databaseFetch = vi.fn().mockResolvedValue(
18
+ new Response(JSON.stringify({ id: 'sig-1', title: 'Room inserted' }), {
19
+ status: 201,
20
+ headers: { 'Content-Type': 'application/json' },
21
+ }),
22
+ );
23
+
24
+ const room: any = Object.create(RoomsDO.prototype);
25
+
26
+ room.env = {
27
+ DATABASE: {
28
+ idFromName: vi.fn(() => 'shared-id'),
29
+ get: vi.fn(() => ({ fetch: databaseFetch })),
30
+ },
31
+ AUTH: {
32
+ idFromName: vi.fn(() => 'auth-id'),
33
+ get: vi.fn(() => ({ fetch: vi.fn() })),
34
+ },
35
+ AUTH_DB: {},
36
+ };
37
+ room.config = {
38
+ databases: {
39
+ shared: {
40
+ tables: {
41
+ signals: {
42
+ schema: {
43
+ title: { type: 'string' },
44
+ },
45
+ },
46
+ },
47
+ },
48
+ },
49
+ };
50
+
51
+ const ctx = room.buildHandlerContext();
52
+ const inserted = await ctx.admin.db('shared').table('signals').insert({ title: 'Room inserted' });
53
+
54
+ expect(inserted).toEqual({ id: 'sig-1', title: 'Room inserted' });
55
+ expect(workerFetch).not.toHaveBeenCalled();
56
+ expect(databaseFetch).toHaveBeenCalledWith(
57
+ 'http://do/tables/signals',
58
+ expect.objectContaining({
59
+ method: 'POST',
60
+ headers: expect.objectContaining({
61
+ 'X-DO-Name': 'shared',
62
+ 'X-EdgeBase-Internal': 'true',
63
+ }),
64
+ }),
65
+ );
66
+ });
67
+
68
+ it('routes admin.db().upsert() through the database durable object', async () => {
69
+ const { RoomsDO } = await import('../durable-objects/rooms-do.js');
70
+ const workerFetch = vi.fn().mockRejectedValue(new Error('worker fetch should not be used'));
71
+ vi.stubGlobal('fetch', workerFetch);
72
+
73
+ const databaseFetch = vi.fn().mockResolvedValue(
74
+ new Response(JSON.stringify({ id: 'sig-1', title: 'Room upserted', action: 'updated' }), {
75
+ status: 200,
76
+ headers: { 'Content-Type': 'application/json' },
77
+ }),
78
+ );
79
+
80
+ const room: any = Object.create(RoomsDO.prototype);
81
+
82
+ room.env = {
83
+ DATABASE: {
84
+ idFromName: vi.fn(() => 'shared-id'),
85
+ get: vi.fn(() => ({ fetch: databaseFetch })),
86
+ },
87
+ AUTH: {
88
+ idFromName: vi.fn(() => 'auth-id'),
89
+ get: vi.fn(() => ({ fetch: vi.fn() })),
90
+ },
91
+ AUTH_DB: {},
92
+ };
93
+ room.config = {
94
+ databases: {
95
+ shared: {
96
+ tables: {
97
+ signals: {
98
+ schema: {
99
+ title: { type: 'string' },
100
+ },
101
+ },
102
+ },
103
+ },
104
+ },
105
+ };
106
+
107
+ const ctx = room.buildHandlerContext();
108
+ const upserted = await ctx.admin.db('shared').table('signals').upsert({
109
+ id: 'sig-1',
110
+ title: 'Room upserted',
111
+ });
112
+
113
+ expect(upserted).toEqual({ id: 'sig-1', title: 'Room upserted', action: 'updated' });
114
+ expect(workerFetch).not.toHaveBeenCalled();
115
+ expect(databaseFetch).toHaveBeenCalledWith(
116
+ 'http://do/tables/signals?upsert=true',
117
+ expect.objectContaining({
118
+ method: 'POST',
119
+ headers: expect.objectContaining({
120
+ 'X-DO-Name': 'shared',
121
+ 'X-EdgeBase-Internal': 'true',
122
+ }),
123
+ body: JSON.stringify({
124
+ id: 'sig-1',
125
+ title: 'Room upserted',
126
+ }),
127
+ }),
128
+ );
129
+ });
130
+ });
@@ -0,0 +1,138 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ fetchRoomMonitoringStatsFromKv,
4
+ persistRoomMonitoringSnapshot,
5
+ } from '../lib/room-monitoring.js';
6
+
7
+ interface MockKvEntry {
8
+ value: string;
9
+ options?: { expirationTtl?: number };
10
+ }
11
+
12
+ function createMockKv(pageSize = 50): KVNamespace & { _store: Record<string, MockKvEntry> } {
13
+ const store: Record<string, MockKvEntry> = {};
14
+
15
+ return {
16
+ get: async (key: string, type?: 'text' | 'json') => {
17
+ const entry = store[key];
18
+ if (!entry) return null;
19
+ if (type === 'json') {
20
+ try {
21
+ return JSON.parse(entry.value);
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+ return entry.value;
27
+ },
28
+ put: async (key: string, value: string, options?: { expirationTtl?: number }) => {
29
+ store[key] = { value, options };
30
+ },
31
+ delete: async (key: string) => {
32
+ delete store[key];
33
+ },
34
+ list: async ({ prefix = '', cursor }: { prefix?: string; cursor?: string } = {}) => {
35
+ const names = Object.keys(store)
36
+ .filter((name) => name.startsWith(prefix))
37
+ .sort();
38
+ const start = cursor ? Number(cursor) : 0;
39
+ const page = names.slice(start, start + pageSize);
40
+ const nextCursor = start + pageSize;
41
+
42
+ return {
43
+ keys: page.map((name) => ({ name })),
44
+ list_complete: nextCursor >= names.length,
45
+ cursor: nextCursor >= names.length ? undefined : String(nextCursor),
46
+ cacheStatus: null,
47
+ };
48
+ },
49
+ _store: store,
50
+ } as unknown as KVNamespace & { _store: Record<string, MockKvEntry> };
51
+ }
52
+
53
+ describe('room monitoring helpers', () => {
54
+ it('persists active room snapshots with a monitoring TTL', async () => {
55
+ const kv = createMockKv();
56
+
57
+ await persistRoomMonitoringSnapshot(kv, {
58
+ room: 'incident-1',
59
+ activeConnections: 3,
60
+ authenticatedConnections: 2,
61
+ updatedAt: '2026-03-12T00:00:00.000Z',
62
+ });
63
+
64
+ const stored = kv._store['monitoring:room:incident-1'];
65
+ expect(stored).toBeDefined();
66
+ expect(stored.options?.expirationTtl).toBe(600);
67
+ expect(JSON.parse(stored!.value)).toMatchObject({
68
+ room: 'incident-1',
69
+ activeConnections: 3,
70
+ authenticatedConnections: 2,
71
+ });
72
+ });
73
+
74
+ it('deletes room snapshots when the room is no longer active', async () => {
75
+ const kv = createMockKv();
76
+
77
+ await persistRoomMonitoringSnapshot(kv, {
78
+ room: 'incident-2',
79
+ activeConnections: 1,
80
+ authenticatedConnections: 1,
81
+ updatedAt: '2026-03-12T00:00:00.000Z',
82
+ });
83
+
84
+ await persistRoomMonitoringSnapshot(kv, {
85
+ room: 'incident-2',
86
+ activeConnections: 0,
87
+ authenticatedConnections: 0,
88
+ updatedAt: '2026-03-12T00:01:00.000Z',
89
+ });
90
+
91
+ expect(kv._store['monitoring:room:incident-2']).toBeUndefined();
92
+ });
93
+
94
+ it('aggregates paginated room snapshots and ignores invalid entries', async () => {
95
+ const kv = createMockKv(1);
96
+
97
+ await kv.put('monitoring:room:alpha', JSON.stringify({
98
+ room: 'alpha',
99
+ activeConnections: 2,
100
+ authenticatedConnections: 1,
101
+ updatedAt: '2026-03-12T00:00:00.000Z',
102
+ }));
103
+ await kv.put('monitoring:room:beta', JSON.stringify({
104
+ room: 'beta',
105
+ activeConnections: 4,
106
+ authenticatedConnections: 3,
107
+ updatedAt: '2026-03-12T00:00:00.000Z',
108
+ }));
109
+ await kv.put('monitoring:room:gamma', JSON.stringify({
110
+ room: 'gamma',
111
+ activeConnections: 0,
112
+ authenticatedConnections: 0,
113
+ updatedAt: '2026-03-12T00:00:00.000Z',
114
+ }));
115
+ await kv.put('monitoring:room:broken', JSON.stringify({
116
+ room: 'broken',
117
+ activeConnections: 'many',
118
+ authenticatedConnections: 1,
119
+ updatedAt: '2026-03-12T00:00:00.000Z',
120
+ }));
121
+ await kv.put('unrelated:key', JSON.stringify({
122
+ room: 'ignored',
123
+ activeConnections: 99,
124
+ authenticatedConnections: 99,
125
+ updatedAt: '2026-03-12T00:00:00.000Z',
126
+ }));
127
+
128
+ await expect(fetchRoomMonitoringStatsFromKv(kv)).resolves.toEqual({
129
+ activeConnections: 6,
130
+ authenticatedConnections: 4,
131
+ channels: 2,
132
+ channelDetails: [
133
+ { channel: 'beta', subscribers: 4 },
134
+ { channel: 'alpha', subscribers: 2 },
135
+ ],
136
+ });
137
+ });
138
+ });