@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,757 @@
1
+ /**
2
+ * Unit tests for lib/service-key.ts
3
+ *
4
+ * Focuses on buildKeymap and validateKey (KNOWN_UNCOVERED_EXPORTS),
5
+ * plus matchesScope and validateScopedKey coverage.
6
+ */
7
+ import { describe, it, expect } from 'vitest';
8
+ import {
9
+ buildKeymap,
10
+ extractBearerToken,
11
+ validateKey,
12
+ validateConfiguredKey,
13
+ matchesConfiguredSecret,
14
+ matchesScope,
15
+ validateScopedKey,
16
+ buildConstraintCtx,
17
+ extractServiceKeyHeader,
18
+ resolveRootServiceKey,
19
+ resolveServiceKeyCandidate,
20
+ } from '../lib/service-key.js';
21
+ import { getTrustedClientIp } from '../lib/client-ip.js';
22
+ import type { EdgeBaseConfig, ServiceKeyEntry } from '@edge-base/shared';
23
+ import type { Env } from '../types.js';
24
+
25
+ // ─── Helpers ───
26
+
27
+ function makeEntry(overrides: Partial<ServiceKeyEntry> & { kid: string }): ServiceKeyEntry {
28
+ return {
29
+ tier: 'root',
30
+ scopes: ['*'],
31
+ secretSource: 'inline',
32
+ inlineSecret: 'test-secret',
33
+ ...overrides,
34
+ };
35
+ }
36
+
37
+ function makeEnv(overrides: Partial<Env> = {}): Env {
38
+ return { ...overrides } as Env;
39
+ }
40
+
41
+ function makeConfig(keys: ServiceKeyEntry[]): EdgeBaseConfig {
42
+ return { serviceKeys: { keys } } as EdgeBaseConfig;
43
+ }
44
+
45
+ // ─── buildKeymap ───
46
+
47
+ describe('buildKeymap', () => {
48
+ it('returns null when config has no serviceKeys', () => {
49
+ const result = buildKeymap({} as EdgeBaseConfig, makeEnv());
50
+ expect(result).toBeNull();
51
+ });
52
+
53
+ it('returns null when serviceKeys.keys is empty', () => {
54
+ const result = buildKeymap(makeConfig([]), makeEnv());
55
+ expect(result).toBeNull();
56
+ });
57
+
58
+ it('builds keymap from inline secrets', () => {
59
+ const entry = makeEntry({ kid: 'k1', inlineSecret: 'secret-1' });
60
+ const result = buildKeymap(makeConfig([entry]), makeEnv());
61
+
62
+ expect(result).not.toBeNull();
63
+ expect(result!.size).toBe(1);
64
+ expect(result!.get('k1')!.secret).toBe('secret-1');
65
+ expect(result!.get('k1')!.entry).toBe(entry);
66
+ });
67
+
68
+ it('resolves dashboard secrets from env', () => {
69
+ const entry = makeEntry({
70
+ kid: 'k2',
71
+ secretSource: 'dashboard',
72
+ secretRef: 'MY_SECRET_VAR',
73
+ inlineSecret: undefined,
74
+ });
75
+ const env = makeEnv({ MY_SECRET_VAR: 'env-resolved-secret' } as unknown as Partial<Env>);
76
+ const result = buildKeymap(makeConfig([entry]), env);
77
+
78
+ expect(result).not.toBeNull();
79
+ expect(result!.get('k2')!.secret).toBe('env-resolved-secret');
80
+ });
81
+
82
+ it('skips disabled entries', () => {
83
+ const enabled = makeEntry({ kid: 'e1', inlineSecret: 'yes' });
84
+ const disabled = makeEntry({ kid: 'e2', inlineSecret: 'no', enabled: false });
85
+ const result = buildKeymap(makeConfig([enabled, disabled]), makeEnv());
86
+
87
+ expect(result!.size).toBe(1);
88
+ expect(result!.has('e1')).toBe(true);
89
+ expect(result!.has('e2')).toBe(false);
90
+ });
91
+
92
+ it('skips entries with no resolvable secret', () => {
93
+ const noSecret = makeEntry({
94
+ kid: 'k3',
95
+ secretSource: 'dashboard',
96
+ secretRef: 'MISSING_VAR',
97
+ inlineSecret: undefined,
98
+ });
99
+ const result = buildKeymap(makeConfig([noSecret]), makeEnv());
100
+ expect(result).toBeNull(); // single entry has no secret → empty map → null
101
+ });
102
+
103
+ it('returns null when all entries resolve to no secret', () => {
104
+ const entries = [
105
+ makeEntry({ kid: 'a', secretSource: 'dashboard', secretRef: undefined, inlineSecret: undefined }),
106
+ makeEntry({ kid: 'b', secretSource: 'inline', inlineSecret: '' }),
107
+ ];
108
+ const result = buildKeymap(makeConfig(entries), makeEnv());
109
+ expect(result).toBeNull();
110
+ });
111
+
112
+ it('skips dashboard entries without secretRef', () => {
113
+ const entry = makeEntry({
114
+ kid: 'noref',
115
+ secretSource: 'dashboard',
116
+ secretRef: undefined, // no ref
117
+ inlineSecret: undefined,
118
+ });
119
+ const result = buildKeymap(makeConfig([entry]), makeEnv());
120
+ expect(result).toBeNull();
121
+ });
122
+
123
+ it('handles multiple entries with mixed secret sources', () => {
124
+ const entries = [
125
+ makeEntry({ kid: 'inline1', secretSource: 'inline', inlineSecret: 'sec-a' }),
126
+ makeEntry({ kid: 'dash1', secretSource: 'dashboard', secretRef: 'REF_1', inlineSecret: undefined }),
127
+ makeEntry({ kid: 'inline2', secretSource: 'inline', inlineSecret: 'sec-b' }),
128
+ ];
129
+ const env = makeEnv({ REF_1: 'sec-from-env' } as unknown as Partial<Env>);
130
+ const result = buildKeymap(makeConfig(entries), env);
131
+
132
+ expect(result!.size).toBe(3);
133
+ expect(result!.get('inline1')!.secret).toBe('sec-a');
134
+ expect(result!.get('dash1')!.secret).toBe('sec-from-env');
135
+ expect(result!.get('inline2')!.secret).toBe('sec-b');
136
+ });
137
+ });
138
+
139
+ // ─── resolveRootServiceKey ───
140
+
141
+ describe('resolveRootServiceKey', () => {
142
+ it('prefers the canonical SERVICE_KEY root when multiple usable root keys exist', () => {
143
+ const config = makeConfig([
144
+ makeEntry({ kid: 'fallback', inlineSecret: 'fallback-secret' }),
145
+ makeEntry({
146
+ kid: 'default',
147
+ secretSource: 'dashboard',
148
+ secretRef: 'SERVICE_KEY',
149
+ inlineSecret: undefined,
150
+ }),
151
+ ]);
152
+
153
+ const env = makeEnv({ SERVICE_KEY: 'canonical-secret' });
154
+ expect(resolveRootServiceKey(config, env)).toBe('canonical-secret');
155
+ });
156
+
157
+ it('skips root keys that require request-scoped constraints', () => {
158
+ const config = makeConfig([
159
+ makeEntry({
160
+ kid: 'tenant-root',
161
+ inlineSecret: 'tenant-secret',
162
+ constraints: { tenant: 'workspace-123' },
163
+ }),
164
+ makeEntry({
165
+ kid: 'default',
166
+ secretSource: 'dashboard',
167
+ secretRef: 'SERVICE_KEY',
168
+ inlineSecret: undefined,
169
+ }),
170
+ ]);
171
+
172
+ const env = makeEnv({ SERVICE_KEY: 'canonical-secret', ENVIRONMENT: 'prod' });
173
+ expect(resolveRootServiceKey(config, env)).toBe('canonical-secret');
174
+ });
175
+
176
+ it('skips env-constrained roots that do not match the current worker environment', () => {
177
+ const config = makeConfig([
178
+ makeEntry({
179
+ kid: 'prod-only',
180
+ inlineSecret: 'prod-secret',
181
+ constraints: { env: ['prod'] },
182
+ }),
183
+ makeEntry({
184
+ kid: 'default',
185
+ secretSource: 'dashboard',
186
+ secretRef: 'SERVICE_KEY',
187
+ inlineSecret: undefined,
188
+ }),
189
+ ]);
190
+
191
+ const env = makeEnv({ SERVICE_KEY: 'canonical-secret', ENVIRONMENT: 'staging' });
192
+ expect(resolveRootServiceKey(config, env)).toBe('canonical-secret');
193
+ });
194
+ });
195
+
196
+ // ─── validateKey ───
197
+
198
+ describe('validateKey', () => {
199
+ it('uses scoped path when config has serviceKeys', () => {
200
+ const entry = makeEntry({ kid: 'k1', inlineSecret: 'jb_k1_payload', tier: 'root' });
201
+ const config = makeConfig([entry]);
202
+ const env = makeEnv();
203
+
204
+ const { result, keymap } = validateKey('jb_k1_payload', 'db:table:users:read', config, env);
205
+ expect(result).toBe('valid');
206
+ expect(keymap).not.toBeNull();
207
+ });
208
+
209
+ it('returns missing when no serviceKeys are configured', () => {
210
+ const config = {} as EdgeBaseConfig;
211
+ const env = makeEnv({ SERVICE_KEY: 'legacy-key' });
212
+
213
+ const { result, keymap } = validateKey('legacy-key', 'any:scope', config, env);
214
+ expect(result).toBe('missing');
215
+ expect(keymap).toBeNull();
216
+ });
217
+
218
+ it('returns missing when no service key config exists', () => {
219
+ const config = {} as EdgeBaseConfig;
220
+ const env = makeEnv();
221
+
222
+ const { result } = validateKey('some-key', 'any:scope', config, env);
223
+ expect(result).toBe('missing');
224
+ });
225
+
226
+ it('does not validate env-only keys without serviceKeys config', () => {
227
+ const config = {} as EdgeBaseConfig;
228
+ const env = makeEnv({ SERVICE_KEY: 'correct-key' });
229
+
230
+ const { result } = validateKey('wrong-key', 'any:scope', config, env);
231
+ expect(result).toBe('missing');
232
+ });
233
+
234
+ it('uses provided keymapCache instead of rebuilding', () => {
235
+ const entry = makeEntry({ kid: 'cached', inlineSecret: 'jb_cached_secret', tier: 'root' });
236
+ const cachedKeymap = new Map([['cached', { entry, secret: 'jb_cached_secret' }]]);
237
+ const config = {} as EdgeBaseConfig; // no serviceKeys — but cache is provided
238
+ const env = makeEnv();
239
+
240
+ const { result, keymap } = validateKey('jb_cached_secret', 'any:scope', config, env, cachedKeymap);
241
+ expect(result).toBe('valid');
242
+ expect(keymap).toBe(cachedKeymap);
243
+ });
244
+
245
+ it('keymapCache = null preserves missing result without serviceKeys config', () => {
246
+ const config = {} as EdgeBaseConfig;
247
+ const env = makeEnv({ SERVICE_KEY: 'leg' });
248
+
249
+ const { result, keymap } = validateKey('leg', 'scope', config, env, null);
250
+ expect(result).toBe('missing');
251
+ expect(keymap).toBeNull();
252
+ });
253
+
254
+ it('scoped path: invalid key returns invalid', () => {
255
+ const entry = makeEntry({ kid: 'k1', inlineSecret: 'jb_k1_real', tier: 'root' });
256
+ const config = makeConfig([entry]);
257
+ const env = makeEnv();
258
+
259
+ const { result } = validateKey('jb_k1_wrong', 'any:scope', config, env);
260
+ expect(result).toBe('invalid');
261
+ });
262
+
263
+ it('scoped path: null provided returns missing', () => {
264
+ const entry = makeEntry({ kid: 'k1', inlineSecret: 'jb_k1_real', tier: 'root' });
265
+ const config = makeConfig([entry]);
266
+ const env = makeEnv();
267
+
268
+ const { result } = validateKey(null, 'any:scope', config, env);
269
+ expect(result).toBe('missing');
270
+ });
271
+
272
+ it('passes constraint context through to scoped validation', () => {
273
+ const entry = makeEntry({
274
+ kid: 'env1',
275
+ inlineSecret: 'jb_env1_payload',
276
+ tier: 'root',
277
+ constraints: { env: ['production'] },
278
+ });
279
+ const config = makeConfig([entry]);
280
+ const env = makeEnv();
281
+
282
+ // Correct env constraint
283
+ const { result: r1 } = validateKey(
284
+ 'jb_env1_payload', 'any:scope', config, env, undefined, { env: 'production' },
285
+ );
286
+ expect(r1).toBe('valid');
287
+
288
+ // Wrong env constraint
289
+ const { result: r2 } = validateKey(
290
+ 'jb_env1_payload', 'any:scope', config, env, undefined, { env: 'staging' },
291
+ );
292
+ expect(r2).toBe('invalid');
293
+ });
294
+ });
295
+
296
+ // ─── matchesScope (additional edge cases) ───
297
+
298
+ describe('matchesScope', () => {
299
+ it('root tier always returns true', () => {
300
+ const entry = makeEntry({ kid: 'r', tier: 'root', scopes: [] });
301
+ expect(matchesScope('db:table:users:read', entry)).toBe(true);
302
+ });
303
+
304
+ it('global wildcard "*" matches everything', () => {
305
+ const entry = makeEntry({ kid: 's', tier: 'scoped', scopes: ['*'] });
306
+ expect(matchesScope('storage:bucket:avatars:write', entry)).toBe(true);
307
+ });
308
+
309
+ it('exact scope match', () => {
310
+ const entry = makeEntry({ kid: 's', tier: 'scoped', scopes: ['db:table:users:read'] });
311
+ expect(matchesScope('db:table:users:read', entry)).toBe(true);
312
+ });
313
+
314
+ it('partial wildcard in scope', () => {
315
+ const entry = makeEntry({ kid: 's', tier: 'scoped', scopes: ['db:table:*:read'] });
316
+ expect(matchesScope('db:table:users:read', entry)).toBe(true);
317
+ expect(matchesScope('db:table:posts:read', entry)).toBe(true);
318
+ expect(matchesScope('db:table:users:write', entry)).toBe(false);
319
+ });
320
+
321
+ it('segment count mismatch → no match', () => {
322
+ const entry = makeEntry({ kid: 's', tier: 'scoped', scopes: ['db:table:users'] });
323
+ expect(matchesScope('db:table:users:read', entry)).toBe(false);
324
+ });
325
+
326
+ it('multiple scopes — first mismatch, second matches', () => {
327
+ const entry = makeEntry({
328
+ kid: 's',
329
+ tier: 'scoped',
330
+ scopes: ['storage:bucket:photos:write', 'db:table:*:read'],
331
+ });
332
+ expect(matchesScope('db:table:users:read', entry)).toBe(true);
333
+ expect(matchesScope('storage:bucket:photos:write', entry)).toBe(true);
334
+ expect(matchesScope('kv:namespace:cache:read', entry)).toBe(false);
335
+ });
336
+
337
+ it('empty scopes on scoped tier → no match', () => {
338
+ const entry = makeEntry({ kid: 's', tier: 'scoped', scopes: [] });
339
+ expect(matchesScope('db:table:users:read', entry)).toBe(false);
340
+ });
341
+
342
+ it('domain-level wildcard matches all under domain', () => {
343
+ const entry = makeEntry({ kid: 's', tier: 'scoped', scopes: ['storage:*:*:*'] });
344
+ expect(matchesScope('storage:bucket:avatars:write', entry)).toBe(true);
345
+ expect(matchesScope('db:table:users:read', entry)).toBe(false);
346
+ });
347
+ });
348
+
349
+ // ─── validateScopedKey ───
350
+
351
+ describe('validateScopedKey', () => {
352
+ it('returns missing for null/undefined and invalid for empty string', () => {
353
+ const keymap = new Map();
354
+ expect(validateScopedKey(null, 'scope', keymap)).toBe('missing');
355
+ expect(validateScopedKey(undefined, 'scope', keymap)).toBe('missing');
356
+ expect(validateScopedKey('', 'scope', keymap)).toBe('invalid');
357
+ });
358
+
359
+ it('returns missing for empty keymap', () => {
360
+ const keymap = new Map();
361
+ expect(validateScopedKey('some-key', 'scope', keymap)).toBe('missing');
362
+ });
363
+
364
+ it('jb_ format: valid key with matching kid', () => {
365
+ const entry = makeEntry({ kid: 'abc', inlineSecret: 'jb_abc_secretpayload', tier: 'root' });
366
+ const keymap = new Map([['abc', { entry, secret: 'jb_abc_secretpayload' }]]);
367
+
368
+ expect(validateScopedKey('jb_abc_secretpayload', 'any:scope', keymap)).toBe('valid');
369
+ });
370
+
371
+ it('jb_ format: invalid when kid not found', () => {
372
+ const entry = makeEntry({ kid: 'abc', inlineSecret: 'jb_abc_secret', tier: 'root' });
373
+ const keymap = new Map([['abc', { entry, secret: 'jb_abc_secret' }]]);
374
+
375
+ expect(validateScopedKey('jb_xyz_secret', 'any:scope', keymap)).toBe('invalid');
376
+ });
377
+
378
+ it('jb_ format: invalid when secret mismatch', () => {
379
+ const entry = makeEntry({ kid: 'abc', inlineSecret: 'jb_abc_correct', tier: 'root' });
380
+ const keymap = new Map([['abc', { entry, secret: 'jb_abc_correct' }]]);
381
+
382
+ expect(validateScopedKey('jb_abc_wronggg', 'any:scope', keymap)).toBe('invalid');
383
+ });
384
+
385
+ it('jb_ format: invalid when scope not matched (scoped tier)', () => {
386
+ const entry = makeEntry({
387
+ kid: 'sc',
388
+ inlineSecret: 'jb_sc_payload',
389
+ tier: 'scoped',
390
+ scopes: ['db:table:users:read'],
391
+ });
392
+ const keymap = new Map([['sc', { entry, secret: 'jb_sc_payload' }]]);
393
+
394
+ expect(validateScopedKey('jb_sc_payload', 'storage:bucket:avatars:write', keymap)).toBe('invalid');
395
+ });
396
+
397
+ it('plain key format: matches root-tier entry', () => {
398
+ const entry = makeEntry({ kid: 'root1', inlineSecret: 'plain-secret', tier: 'root' });
399
+ const keymap = new Map([['root1', { entry, secret: 'plain-secret' }]]);
400
+
401
+ expect(validateScopedKey('plain-secret', 'any:scope', keymap)).toBe('valid');
402
+ });
403
+
404
+ it('plain key format: returns invalid when no root entry matches', () => {
405
+ const entry = makeEntry({ kid: 'root1', inlineSecret: 'correct-secret', tier: 'root' });
406
+ const keymap = new Map([['root1', { entry, secret: 'correct-secret' }]]);
407
+
408
+ expect(validateScopedKey('wrong-secret', 'any:scope', keymap)).toBe('invalid');
409
+ });
410
+
411
+ it('plain key format: returns missing when no root-tier entries exist', () => {
412
+ const entry = makeEntry({
413
+ kid: 'sc1',
414
+ inlineSecret: 'jb_sc1_something',
415
+ tier: 'scoped',
416
+ scopes: ['*'],
417
+ });
418
+ const keymap = new Map([['sc1', { entry, secret: 'jb_sc1_something' }]]);
419
+
420
+ // This is a plain key (no jb_ prefix), and only scoped entries exist
421
+ expect(validateScopedKey('some-plain-key', 'any:scope', keymap)).toBe('missing');
422
+ });
423
+
424
+ it('jb_ format: constraint failure returns invalid', () => {
425
+ const entry = makeEntry({
426
+ kid: 'cx',
427
+ inlineSecret: 'jb_cx_payload',
428
+ tier: 'root',
429
+ constraints: { expiresAt: '2020-01-01T00:00:00Z' }, // expired
430
+ });
431
+ const keymap = new Map([['cx', { entry, secret: 'jb_cx_payload' }]]);
432
+
433
+ expect(validateScopedKey('jb_cx_payload', 'any:scope', keymap)).toBe('invalid');
434
+ });
435
+
436
+ // ─── checkConstraints detailed paths ───
437
+
438
+ it('expiresAt: future date passes', () => {
439
+ const future = new Date(Date.now() + 3600_000).toISOString();
440
+ const entry = makeEntry({
441
+ kid: 'exp',
442
+ inlineSecret: 'jb_exp_payload',
443
+ tier: 'root',
444
+ constraints: { expiresAt: future },
445
+ });
446
+ const keymap = new Map([['exp', { entry, secret: 'jb_exp_payload' }]]);
447
+ expect(validateScopedKey('jb_exp_payload', 'any:scope', keymap)).toBe('valid');
448
+ });
449
+
450
+ it('expiresAt: exact now boundary → invalid (>= check)', () => {
451
+ // Use a date in the past by 1ms to ensure Date.now() >= expiresAt
452
+ const justPast = new Date(Date.now() - 1).toISOString();
453
+ const entry = makeEntry({
454
+ kid: 'expb',
455
+ inlineSecret: 'jb_expb_payload',
456
+ tier: 'root',
457
+ constraints: { expiresAt: justPast },
458
+ });
459
+ const keymap = new Map([['expb', { entry, secret: 'jb_expb_payload' }]]);
460
+ expect(validateScopedKey('jb_expb_payload', 'any:scope', keymap)).toBe('invalid');
461
+ });
462
+
463
+ it('expiresAt: invalid date string passes (NaN check)', () => {
464
+ const entry = makeEntry({
465
+ kid: 'expn',
466
+ inlineSecret: 'jb_expn_payload',
467
+ tier: 'root',
468
+ constraints: { expiresAt: 'not-a-date' },
469
+ });
470
+ const keymap = new Map([['expn', { entry, secret: 'jb_expn_payload' }]]);
471
+ expect(validateScopedKey('jb_expn_payload', 'any:scope', keymap)).toBe('valid');
472
+ });
473
+
474
+ it('env constraint: matching env passes', () => {
475
+ const entry = makeEntry({
476
+ kid: 'env1',
477
+ inlineSecret: 'jb_env1_payload',
478
+ tier: 'root',
479
+ constraints: { env: ['production', 'staging'] },
480
+ });
481
+ const keymap = new Map([['env1', { entry, secret: 'jb_env1_payload' }]]);
482
+ expect(validateScopedKey('jb_env1_payload', 'any:scope', keymap, { env: 'staging' })).toBe('valid');
483
+ });
484
+
485
+ it('env constraint: non-matching env fails', () => {
486
+ const entry = makeEntry({
487
+ kid: 'env2',
488
+ inlineSecret: 'jb_env2_payload',
489
+ tier: 'root',
490
+ constraints: { env: ['production'] },
491
+ });
492
+ const keymap = new Map([['env2', { entry, secret: 'jb_env2_payload' }]]);
493
+ expect(validateScopedKey('jb_env2_payload', 'any:scope', keymap, { env: 'development' })).toBe('invalid');
494
+ });
495
+
496
+ it('env constraint: no ctx.env → denied (fail-closed)', () => {
497
+ const entry = makeEntry({
498
+ kid: 'env3',
499
+ inlineSecret: 'jb_env3_payload',
500
+ tier: 'root',
501
+ constraints: { env: ['production'] },
502
+ });
503
+ const keymap = new Map([['env3', { entry, secret: 'jb_env3_payload' }]]);
504
+ // No env in context — constraint fails (fail-closed)
505
+ expect(validateScopedKey('jb_env3_payload', 'any:scope', keymap, {})).toBe('invalid');
506
+ });
507
+
508
+ it('ipCidr constraint: matching IP passes', () => {
509
+ const entry = makeEntry({
510
+ kid: 'ip1',
511
+ inlineSecret: 'jb_ip1_payload',
512
+ tier: 'root',
513
+ constraints: { ipCidr: ['10.0.0.0/8'] },
514
+ });
515
+ const keymap = new Map([['ip1', { entry, secret: 'jb_ip1_payload' }]]);
516
+ expect(validateScopedKey('jb_ip1_payload', 'any:scope', keymap, { ip: '10.1.2.3' })).toBe('valid');
517
+ });
518
+
519
+ it('ipCidr constraint: non-matching IP fails', () => {
520
+ const entry = makeEntry({
521
+ kid: 'ip2',
522
+ inlineSecret: 'jb_ip2_payload',
523
+ tier: 'root',
524
+ constraints: { ipCidr: ['10.0.0.0/8'] },
525
+ });
526
+ const keymap = new Map([['ip2', { entry, secret: 'jb_ip2_payload' }]]);
527
+ expect(validateScopedKey('jb_ip2_payload', 'any:scope', keymap, { ip: '192.168.1.1' })).toBe('invalid');
528
+ });
529
+
530
+ it('ipCidr constraint: no ctx.ip → denied (fail-closed)', () => {
531
+ const entry = makeEntry({
532
+ kid: 'ip3',
533
+ inlineSecret: 'jb_ip3_payload',
534
+ tier: 'root',
535
+ constraints: { ipCidr: ['10.0.0.0/8'] },
536
+ });
537
+ const keymap = new Map([['ip3', { entry, secret: 'jb_ip3_payload' }]]);
538
+ expect(validateScopedKey('jb_ip3_payload', 'any:scope', keymap, {})).toBe('invalid');
539
+ });
540
+
541
+ it('tenant constraint: matching tenantId passes', () => {
542
+ const entry = makeEntry({
543
+ kid: 'tn1',
544
+ inlineSecret: 'jb_tn1_payload',
545
+ tier: 'root',
546
+ constraints: { tenant: 'tenant-abc' },
547
+ });
548
+ const keymap = new Map([['tn1', { entry, secret: 'jb_tn1_payload' }]]);
549
+ expect(validateScopedKey('jb_tn1_payload', 'any:scope', keymap, { tenantId: 'tenant-abc' })).toBe('valid');
550
+ });
551
+
552
+ it('tenant constraint: non-matching tenantId fails', () => {
553
+ const entry = makeEntry({
554
+ kid: 'tn2',
555
+ inlineSecret: 'jb_tn2_payload',
556
+ tier: 'root',
557
+ constraints: { tenant: 'tenant-abc' },
558
+ });
559
+ const keymap = new Map([['tn2', { entry, secret: 'jb_tn2_payload' }]]);
560
+ expect(validateScopedKey('jb_tn2_payload', 'any:scope', keymap, { tenantId: 'tenant-xyz' })).toBe('invalid');
561
+ });
562
+
563
+ it('tenant constraint: no ctx.tenantId → denied (fail-closed)', () => {
564
+ const entry = makeEntry({
565
+ kid: 'tn3',
566
+ inlineSecret: 'jb_tn3_payload',
567
+ tier: 'root',
568
+ constraints: { tenant: 'tenant-abc' },
569
+ });
570
+ const keymap = new Map([['tn3', { entry, secret: 'jb_tn3_payload' }]]);
571
+ expect(validateScopedKey('jb_tn3_payload', 'any:scope', keymap, {})).toBe('invalid');
572
+ });
573
+
574
+ it('no constraints defined → passes', () => {
575
+ const entry = makeEntry({
576
+ kid: 'nc',
577
+ inlineSecret: 'jb_nc_payload',
578
+ tier: 'root',
579
+ // No constraints
580
+ });
581
+ const keymap = new Map([['nc', { entry, secret: 'jb_nc_payload' }]]);
582
+ expect(validateScopedKey('jb_nc_payload', 'any:scope', keymap)).toBe('valid');
583
+ });
584
+
585
+ // ─── validateScopedKey additional edge cases ───
586
+
587
+ it('empty string with non-empty keymap returns invalid', () => {
588
+ const entry = makeEntry({ kid: 'k1', inlineSecret: 'jb_k1_sec', tier: 'root' });
589
+ const keymap = new Map([['k1', { entry, secret: 'jb_k1_sec' }]]);
590
+ expect(validateScopedKey('', 'any:scope', keymap)).toBe('invalid');
591
+ });
592
+
593
+ it('jb_ prefix without second underscore falls through to plain key path', () => {
594
+ // "jb_abc" has no second underscore → treated as plain key
595
+ const entry = makeEntry({ kid: 'root', inlineSecret: 'jb_abc', tier: 'root' });
596
+ const keymap = new Map([['root', { entry, secret: 'jb_abc' }]]);
597
+ expect(validateScopedKey('jb_abc', 'any:scope', keymap)).toBe('valid');
598
+ });
599
+
600
+ it('jb_ with underscore at position 3 exactly falls through', () => {
601
+ // "jb__rest" → secondUnderscore === 3, which is NOT > 3
602
+ const entry = makeEntry({ kid: 'root', inlineSecret: 'jb__rest', tier: 'root' });
603
+ const keymap = new Map([['root', { entry, secret: 'jb__rest' }]]);
604
+ expect(validateScopedKey('jb__rest', 'any:scope', keymap)).toBe('valid');
605
+ });
606
+
607
+ it('plain key: root-tier constraint failure returns invalid', () => {
608
+ const entry = makeEntry({
609
+ kid: 'root1',
610
+ inlineSecret: 'plain-secret',
611
+ tier: 'root',
612
+ constraints: { expiresAt: '2020-01-01T00:00:00Z' }, // expired
613
+ });
614
+ const keymap = new Map([['root1', { entry, secret: 'plain-secret' }]]);
615
+ expect(validateScopedKey('plain-secret', 'any:scope', keymap)).toBe('invalid');
616
+ });
617
+ });
618
+
619
+ // ─── buildConstraintCtx ───
620
+
621
+ describe('buildConstraintCtx', () => {
622
+ it('getTrustedClientIp prefers cf-connecting-ip even when trustSelfHostedProxy is enabled', () => {
623
+ const req = {
624
+ header: (name: string) => {
625
+ if (name === 'cf-connecting-ip') return '1.1.1.1';
626
+ if (name === 'x-forwarded-for') return '10.0.0.1';
627
+ return undefined;
628
+ },
629
+ };
630
+ expect(getTrustedClientIp({ EDGEBASE_CONFIG: JSON.stringify({ trustSelfHostedProxy: true }) }, req)).toBe('1.1.1.1');
631
+ });
632
+
633
+ it('sets env from ENVIRONMENT', () => {
634
+ const ctx = buildConstraintCtx({ ENVIRONMENT: 'production' });
635
+ expect(ctx.env).toBe('production');
636
+ });
637
+
638
+ it('extracts IP from cf-connecting-ip', () => {
639
+ const req = { header: (name: string) => name === 'cf-connecting-ip' ? '1.2.3.4' : undefined };
640
+ const ctx = buildConstraintCtx({ EDGEBASE_CONFIG: '{}' }, req);
641
+ expect(ctx.ip).toBe('1.2.3.4');
642
+ });
643
+
644
+ it('ignores x-forwarded-for unless trustSelfHostedProxy is enabled', () => {
645
+ const req = {
646
+ header: (name: string) => name === 'x-forwarded-for' ? '10.0.0.1, 10.0.0.2' : undefined,
647
+ };
648
+ const ctx = buildConstraintCtx({ EDGEBASE_CONFIG: '{}' }, req);
649
+ expect(ctx.ip).toBeUndefined();
650
+ });
651
+
652
+ it('uses x-forwarded-for first IP when trustSelfHostedProxy is enabled', () => {
653
+ const req = {
654
+ header: (name: string) => name === 'x-forwarded-for' ? '10.0.0.1, 10.0.0.2' : undefined,
655
+ };
656
+ const ctx = buildConstraintCtx({ EDGEBASE_CONFIG: JSON.stringify({ trustSelfHostedProxy: true }) }, req);
657
+ expect(ctx.ip).toBe('10.0.0.1');
658
+ });
659
+
660
+ it('trims whitespace from x-forwarded-for IP', () => {
661
+ const req = {
662
+ header: (name: string) => name === 'x-forwarded-for' ? ' 10.0.0.1 , 10.0.0.2' : undefined,
663
+ };
664
+ const ctx = buildConstraintCtx({ EDGEBASE_CONFIG: JSON.stringify({ trustSelfHostedProxy: true }) }, req);
665
+ expect(ctx.ip).toBe('10.0.0.1');
666
+ });
667
+
668
+ it('handles x-forwarded-for with single IP (no comma)', () => {
669
+ const req = {
670
+ header: (name: string) => name === 'x-forwarded-for' ? '10.0.0.1' : undefined,
671
+ };
672
+ const ctx = buildConstraintCtx({ EDGEBASE_CONFIG: JSON.stringify({ trustSelfHostedProxy: true }) }, req);
673
+ expect(ctx.ip).toBe('10.0.0.1');
674
+ });
675
+
676
+ it('no req → ip is undefined', () => {
677
+ const ctx = buildConstraintCtx({});
678
+ expect(ctx.ip).toBeUndefined();
679
+ });
680
+
681
+ it('no headers at all → ip is undefined', () => {
682
+ const req = { header: () => undefined };
683
+ const ctx = buildConstraintCtx({}, req);
684
+ expect(ctx.ip).toBeUndefined();
685
+ });
686
+ });
687
+
688
+ describe('service key header helpers', () => {
689
+ it('extractServiceKeyHeader preserves explicitly empty header values', () => {
690
+ const req = {
691
+ header: (name: string) => (name === 'X-EdgeBase-Service-Key' ? '' : undefined),
692
+ };
693
+ expect(extractServiceKeyHeader(req)).toBe('');
694
+ });
695
+
696
+ it('resolveServiceKeyCandidate prefers header presence over fallback token', () => {
697
+ const req = {
698
+ header: (name: string) => (name === 'X-EdgeBase-Service-Key' ? '' : undefined),
699
+ };
700
+ expect(resolveServiceKeyCandidate(req, 'fallback-token')).toBe('');
701
+ });
702
+
703
+ it('extractBearerToken preserves empty Bearer payloads', () => {
704
+ const req = {
705
+ header: (name: string) => (name === 'authorization' ? 'Bearer ' : undefined),
706
+ };
707
+ expect(extractBearerToken(req)).toBe('');
708
+ });
709
+
710
+ it('extractServiceKeyHeader falls back to raw request headers when Hono header lookup misses', () => {
711
+ const req = {
712
+ header: () => undefined,
713
+ raw: new Request('http://localhost/api/test', {
714
+ headers: { 'X-EdgeBase-Service-Key': 'raw-secret' },
715
+ }),
716
+ };
717
+ expect(extractServiceKeyHeader(req)).toBe('raw-secret');
718
+ });
719
+
720
+ it('resolveServiceKeyCandidate prefers raw service key header over bearer fallback', () => {
721
+ const req = {
722
+ header: () => undefined,
723
+ raw: new Request('http://localhost/api/test', {
724
+ headers: { 'X-EdgeBase-Service-Key': 'raw-secret' },
725
+ }),
726
+ };
727
+ expect(resolveServiceKeyCandidate(req, 'fallback-token')).toBe('raw-secret');
728
+ });
729
+ });
730
+
731
+ describe('validateConfiguredKey', () => {
732
+ it('validates configured secrets exactly', () => {
733
+ const entry = makeEntry({ kid: 'cfg', inlineSecret: 'my-secret-key' });
734
+ const keymap = new Map([['cfg', { entry, secret: 'my-secret-key' }]]);
735
+
736
+ expect(validateConfiguredKey('my-secret-key', keymap, {})).toBe('valid');
737
+ expect(validateConfiguredKey('wrong-key', keymap, {})).toBe('invalid');
738
+ });
739
+
740
+ it('treats empty configured key as invalid and nullish as missing', () => {
741
+ const entry = makeEntry({ kid: 'cfg', inlineSecret: 'my-secret-key' });
742
+ const keymap = new Map([['cfg', { entry, secret: 'my-secret-key' }]]);
743
+
744
+ expect(validateConfiguredKey('', keymap, {})).toBe('invalid');
745
+ expect(validateConfiguredKey(null, keymap, {})).toBe('missing');
746
+ expect(validateConfiguredKey(undefined, keymap, {})).toBe('missing');
747
+ });
748
+
749
+ it('matchesConfiguredSecret only accepts exact configured values', () => {
750
+ const entry = makeEntry({ kid: 'cfg', inlineSecret: 'my-secret-key' });
751
+ const keymap = new Map([['cfg', { entry, secret: 'my-secret-key' }]]);
752
+
753
+ expect(matchesConfiguredSecret('my-secret-key', keymap)).toBe(true);
754
+ expect(matchesConfiguredSecret('my-secret-key ', keymap)).toBe(false);
755
+ expect(matchesConfiguredSecret('', keymap)).toBe(false);
756
+ });
757
+ });