@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,169 @@
1
+ /**
2
+ * Auth Middleware — JWT Access Token verification + auth context injection
3
+ *
4
+ * Parses `Authorization: Bearer {token}` from request headers.
5
+ * If valid → sets `auth` context with user info.
6
+ * If missing → sets `auth` to null (allows public endpoints).
7
+ * If invalid/expired → returns 401.
8
+ * If token present but JWT_USER_SECRET not configured → returns 401 (fail-closed,).
9
+ */
10
+ import type { Context, Next } from 'hono';
11
+ import { getAuthEnrichHandler, type AuthContext as SharedAuthContext } from '@edge-base/shared';
12
+ import type { Env } from '../types.js';
13
+ import {
14
+ verifyAccessToken,
15
+ TokenExpiredError,
16
+ TokenInvalidError,
17
+ } from '../lib/jwt.js';
18
+ import {
19
+ buildKeymap,
20
+ extractBearerToken,
21
+ extractServiceKeyHeader,
22
+ matchesConfiguredSecret,
23
+ } from '../lib/service-key.js';
24
+ import { parseConfig } from '../lib/do-router.js';
25
+
26
+ // Extend Hono context variables
27
+ declare module 'hono' {
28
+ interface ContextVariableMap {
29
+ auth: AuthContext | null;
30
+ serviceKeyToken: string | null;
31
+ }
32
+ }
33
+
34
+ export interface AuthContext extends SharedAuthContext {
35
+ role: string;
36
+ isAnonymous: boolean;
37
+ /** auth enrich hook output — request-scoped extension data (#133 §38). Default: {} */
38
+ meta: Record<string, unknown>;
39
+ }
40
+
41
+ interface AuthResolutionEnv {
42
+ JWT_USER_SECRET?: string;
43
+ }
44
+
45
+ export function buildAuthContextFromPayload(payload: Record<string, unknown>): AuthContext {
46
+ return {
47
+ id: payload.sub as string,
48
+ email: typeof payload.email === 'string' ? payload.email : undefined,
49
+ role: (payload.role as string) ?? 'user',
50
+ isAnonymous: (payload.isAnonymous as boolean) ?? false,
51
+ custom: (payload.custom as Record<string, unknown> | undefined) ?? undefined,
52
+ meta: {},
53
+ };
54
+ }
55
+
56
+ export async function enrichAuthContext(
57
+ env: AuthResolutionEnv,
58
+ auth: AuthContext,
59
+ request: Request,
60
+ ): Promise<AuthContext> {
61
+ const config = parseConfig(env);
62
+ const enrich = getAuthEnrichHandler(config);
63
+ if (!enrich) return auth;
64
+
65
+ try {
66
+ const meta = await Promise.race([
67
+ Promise.resolve(enrich(auth, request)),
68
+ new Promise<Record<string, unknown>>((_, reject) =>
69
+ setTimeout(() => reject(new Error('auth.handlers.hooks.enrich timeout')), 50),
70
+ ),
71
+ ]);
72
+ auth.meta = meta ?? {};
73
+ } catch {
74
+ auth.meta = {};
75
+ }
76
+
77
+ return auth;
78
+ }
79
+
80
+ export async function resolveAuthContextFromToken(
81
+ env: AuthResolutionEnv,
82
+ token: string,
83
+ request: Request,
84
+ ): Promise<AuthContext> {
85
+ const secret = env.JWT_USER_SECRET;
86
+ if (!secret) {
87
+ throw new TokenInvalidError('Authentication service not configured.');
88
+ }
89
+
90
+ const payload = await verifyAccessToken(token, secret);
91
+ const auth = buildAuthContextFromPayload(payload as Record<string, unknown>);
92
+ return enrichAuthContext(env, auth, request);
93
+ }
94
+
95
+ function matchesConfiguredServiceKeyCandidate(token: string, c: Context<{ Bindings: Env }>): boolean {
96
+ const config = parseConfig(c.env);
97
+ const keymap = buildKeymap(config, c.env);
98
+ return matchesConfiguredSecret(token, keymap);
99
+ }
100
+
101
+ /**
102
+ * Auth middleware — extracts and verifies JWT Access Token.
103
+ * Non-blocking: if no token, sets auth to null (for public endpoints).
104
+ * If token present but invalid/expired, returns 401.
105
+ */
106
+ export async function authMiddleware(c: Context<{ Bindings: Env }>, next: Next): Promise<Response | void> {
107
+ c.set('serviceKeyToken', null);
108
+
109
+ // An explicit service key header wins over Bearer auth. Route-specific scope
110
+ // validation happens downstream, but we must avoid parsing the paired
111
+ // Authorization header as a user JWT first.
112
+ const serviceKeyHeader =
113
+ c.req.header('X-EdgeBase-Service-Key') ??
114
+ c.req.header('x-edgebase-service-key') ??
115
+ c.req.raw.headers.get('X-EdgeBase-Service-Key') ??
116
+ c.req.raw.headers.get('x-edgebase-service-key') ??
117
+ extractServiceKeyHeader(c.req);
118
+ if (serviceKeyHeader !== undefined && serviceKeyHeader !== null) {
119
+ c.set('serviceKeyToken', serviceKeyHeader);
120
+ c.set('auth', null);
121
+ return next();
122
+ }
123
+
124
+ // No token → public request
125
+ const token = extractBearerToken(c.req);
126
+ if (token === null) {
127
+ c.set('auth', null);
128
+ return next();
129
+ }
130
+
131
+ // Service Key shortcut — exact Service Key secret matches bypass JWT parsing
132
+ // and are validated by downstream route/rules middleware.
133
+ if (matchesConfiguredServiceKeyCandidate(token, c)) {
134
+ c.set('serviceKeyToken', token);
135
+ c.set('auth', null);
136
+ return next();
137
+ }
138
+
139
+ try {
140
+ if (!c.env.JWT_USER_SECRET) {
141
+ // Fail-closed: token provided but cannot verify — reject
142
+ return c.json(
143
+ { code: 401, message: 'Authentication service not configured.', error: 'AUTH_NOT_CONFIGURED' },
144
+ 401,
145
+ );
146
+ }
147
+
148
+ const auth = await resolveAuthContextFromToken(c.env, token, c.req.raw);
149
+ c.set('auth', auth);
150
+ return next();
151
+ } catch (err) {
152
+ if (err instanceof TokenExpiredError) {
153
+ return c.json(
154
+ { code: 401, message: 'Token expired.', error: 'TOKEN_EXPIRED' },
155
+ 401,
156
+ );
157
+ }
158
+ if (err instanceof TokenInvalidError) {
159
+ return c.json(
160
+ { code: 401, message: 'Invalid token.', error: 'TOKEN_INVALID' },
161
+ 401,
162
+ );
163
+ }
164
+ return c.json(
165
+ { code: 401, message: 'Authentication failed.', error: 'AUTH_FAILED' },
166
+ 401,
167
+ );
168
+ }
169
+ }
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Captcha (Turnstile) verification middleware.
3
+ *
4
+ * NOT a global middleware — applied internally within auth and function routes.
5
+ * Validates Turnstile tokens via siteverify API.
6
+ *
7
+ * Token extraction order: body.captchaToken → query.captcha_token → X-EdgeBase-Captcha-Token header.
8
+ */
9
+ import type { Context, Next } from 'hono';
10
+ import type { Env } from '../types.js';
11
+ import { parseConfig } from '../lib/do-router.js';
12
+
13
+ interface CaptchaConfig {
14
+ siteKey: string;
15
+ secretKey: string;
16
+ failMode?: 'open' | 'closed';
17
+ siteverifyTimeout?: number;
18
+ }
19
+
20
+ type HonoContext = Context<{ Bindings: Env }>;
21
+
22
+ interface SiteverifyResponse {
23
+ success: boolean;
24
+ action?: string;
25
+ 'error-codes'?: string[];
26
+ }
27
+
28
+ /**
29
+ * Resolve captcha config. (#133 §31: uses parseConfig() singleton)
30
+ */
31
+ function resolveCaptchaConfig(env: Env): CaptchaConfig | null {
32
+ try {
33
+ const config = parseConfig(env);
34
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
+ const captcha = (config as any)?.captcha;
36
+
37
+ if (!captcha) return null;
38
+ if (captcha === false) return null;
39
+
40
+ // captcha: true — check for auto-provisioned keys
41
+ if (captcha === true) {
42
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
43
+ const siteKey = (config as any)?.captchaSiteKey as string | undefined;
44
+ const secretKey = env.TURNSTILE_SECRET;
45
+ if (!siteKey || !secretKey) return null;
46
+ return { siteKey, secretKey };
47
+ }
48
+
49
+ // captcha: { siteKey, secretKey, ... }
50
+ if (typeof captcha === 'object' && captcha.siteKey && captcha.secretKey) {
51
+ return captcha as CaptchaConfig;
52
+ }
53
+
54
+ return null;
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Extract captcha token from request.
62
+ * Order: body.captchaToken → query.captcha_token → X-EdgeBase-Captcha-Token header
63
+ */
64
+ async function extractCaptchaToken(c: HonoContext): Promise<string | null> {
65
+ // Try body (POST requests)
66
+ if (c.req.method === 'POST' || c.req.method === 'PUT' || c.req.method === 'PATCH') {
67
+ try {
68
+ const body = await c.req.json();
69
+ if (body?.captchaToken) return body.captchaToken;
70
+ } catch {
71
+ // Body parsing failed — try other sources
72
+ }
73
+ }
74
+
75
+ // Try query parameter (GET requests, e.g. OAuth)
76
+ const queryToken = c.req.query('captcha_token');
77
+ if (queryToken) return queryToken;
78
+
79
+ // Try header (fallback)
80
+ const headerToken = c.req.header('X-EdgeBase-Captcha-Token');
81
+ if (headerToken) return headerToken;
82
+
83
+ return null;
84
+ }
85
+
86
+ /**
87
+ * Call Cloudflare Turnstile siteverify API.
88
+ */
89
+ async function siteverify(
90
+ secretKey: string,
91
+ token: string,
92
+ remoteip: string | undefined,
93
+ timeout: number,
94
+ ): Promise<SiteverifyResponse> {
95
+ const controller = new AbortController();
96
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
97
+
98
+ try {
99
+ const response = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
100
+ method: 'POST',
101
+ headers: { 'Content-Type': 'application/json' },
102
+ body: JSON.stringify({
103
+ secret: secretKey,
104
+ response: token,
105
+ ...(remoteip ? { remoteip } : {}),
106
+ }),
107
+ signal: controller.signal,
108
+ });
109
+ return (await response.json()) as SiteverifyResponse;
110
+ } catch {
111
+ // Timeout or network error
112
+ return { success: false, 'error-codes': ['timeout-or-network-error'] };
113
+ } finally {
114
+ clearTimeout(timeoutId);
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Check if request has a Service Key (bypass captcha per).
120
+ */
121
+ function hasServiceKey(c: HonoContext): boolean {
122
+ return !!(
123
+ c.req.header('X-EdgeBase-Service-Key') ||
124
+ c.req.header('Authorization')?.startsWith('ServiceKey ')
125
+ );
126
+ }
127
+
128
+ /**
129
+ * Create a captcha middleware for Auth routes.
130
+ * @param expectedAction - Expected Turnstile action value (e.g. 'signup', 'signin')
131
+ */
132
+ export function captchaMiddleware(expectedAction: string) {
133
+ return async (c: HonoContext, next: Next) => {
134
+ const captchaConfig = resolveCaptchaConfig(c.env);
135
+
136
+ // Step 1-2: No config or keys not provisioned → pass through
137
+ if (!captchaConfig) {
138
+ // Log warning if captcha is enabled but keys missing
139
+ try {
140
+ const config = parseConfig(c.env);
141
+ if (config?.captcha === true) {
142
+ console.warn('⚠️ Captcha skipped: no Turnstile keys provisioned.');
143
+ }
144
+ } catch { /* ignore */ }
145
+ await next();
146
+ return;
147
+ }
148
+
149
+ // Step 3: Service Key → bypass
150
+ if (hasServiceKey(c)) {
151
+ await next();
152
+ return;
153
+ }
154
+
155
+ // Step 4: Extract token
156
+ const token = await extractCaptchaToken(c);
157
+
158
+ // Step 5: No token → 403
159
+ if (!token) {
160
+ return c.json({ code: 403, message: 'Captcha verification required.', data: { captcha_required: true } }, 403);
161
+ }
162
+
163
+ // Step 6: siteverify
164
+ const timeout = captchaConfig.siteverifyTimeout ?? 3000;
165
+ const failMode = captchaConfig.failMode ?? 'open';
166
+ const remoteip = c.req.header('cf-connecting-ip') || undefined;
167
+
168
+ const result = await siteverify(captchaConfig.secretKey, token, remoteip, timeout);
169
+
170
+ // Handle siteverify API failure (timeout, network error)
171
+ if (result['error-codes']?.includes('timeout-or-network-error')) {
172
+ if (failMode === 'open') {
173
+ console.warn('⚠️ Turnstile siteverify failed (timeout/network). Allowing request (failMode=open).');
174
+ await next();
175
+ return;
176
+ }
177
+ return c.json({ code: 503, message: 'Captcha service unavailable.' }, 503);
178
+ }
179
+
180
+ // Step 7: Verify success + action
181
+ if (!result.success) {
182
+ return c.json({ code: 403, message: 'Captcha verification failed.', data: { captcha_required: true } }, 403);
183
+ }
184
+
185
+ // Action verification
186
+ if (result.action && result.action !== expectedAction) {
187
+ return c.json({
188
+ code: 403,
189
+ message: `Captcha action mismatch: expected '${expectedAction}'.`,
190
+ data: { captcha_required: true },
191
+ }, 403);
192
+ }
193
+
194
+ // Step 8: Passed
195
+ await next();
196
+ };
197
+ }
198
+
199
+ /**
200
+ * Captcha middleware for Functions routes.
201
+ * Checks the function definition's captcha flag before verifying.
202
+ */
203
+ export function functionCaptchaMiddleware(functionName: string, captchaEnabled: boolean) {
204
+ if (!captchaEnabled) {
205
+ return async (_c: HonoContext, next: Next) => { await next(); };
206
+ }
207
+ return captchaMiddleware(`function:${functionName}`);
208
+ }
209
+
210
+ // ─── Test exports (for unit testing only) ───
211
+ export const _test = {
212
+ resolveCaptchaConfig,
213
+ extractCaptchaToken,
214
+ hasServiceKey,
215
+ siteverify,
216
+ };
217
+
@@ -0,0 +1,159 @@
1
+ import type { MiddlewareHandler } from 'hono';
2
+ import type { Env } from '../types.js';
3
+ import { parseConfig } from '../lib/do-router.js';
4
+
5
+ type HonoEnv = { Bindings: Env };
6
+
7
+ interface CorsConfig {
8
+ origin?: string | string[];
9
+ methods?: string[];
10
+ credentials?: boolean;
11
+ maxAge?: number;
12
+ }
13
+
14
+ interface ResolvedCorsHeaders {
15
+ allowOrigin: string;
16
+ allowMethods: string;
17
+ allowHeaders: string;
18
+ allowCredentials: boolean;
19
+ maxAge: string;
20
+ }
21
+
22
+ /**
23
+ * Convert wildcard origin pattern to regex.
24
+ * e.g. '*.example.com' → /^https?:\/\/.*\.example\.com$/
25
+ */
26
+ export function wildcardToRegex(pattern: string): RegExp {
27
+ const escaped = pattern
28
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
29
+ .replace(/\*/g, '.*');
30
+ return new RegExp(`^https?:\\/\\/${escaped}$`);
31
+ }
32
+
33
+ /**
34
+ * Check if origin matches allowed origins list.
35
+ */
36
+ export function isOriginAllowed(origin: string, allowedOrigins: string | string[]): boolean {
37
+ if (allowedOrigins === '*') return true;
38
+
39
+ const origins = Array.isArray(allowedOrigins) ? allowedOrigins : [allowedOrigins];
40
+
41
+ for (const pattern of origins) {
42
+ // Exact match
43
+ if (pattern === origin) return true;
44
+ // Wildcard match
45
+ if (pattern.includes('*')) {
46
+ if (wildcardToRegex(pattern).test(origin)) return true;
47
+ }
48
+ }
49
+ return false;
50
+ }
51
+
52
+ function resolveCorsHeaders(
53
+ origin: string,
54
+ configuredOrigins: CorsConfig['origin'],
55
+ methods: string[],
56
+ credentials: boolean,
57
+ maxAge: number,
58
+ ): ResolvedCorsHeaders | null {
59
+ if (!origin) return null;
60
+
61
+ const isWildcardOrigin = configuredOrigins === '*';
62
+ const effectiveCredentials = isWildcardOrigin ? false : credentials;
63
+
64
+ let isAllowed = false;
65
+ if (configuredOrigins) {
66
+ isAllowed = isOriginAllowed(origin, configuredOrigins);
67
+ } else {
68
+ isAllowed =
69
+ /^http:\/\/localhost(:[0-9]+)?(\/|$)/.test(origin) ||
70
+ /^http:\/\/127\.0\.0\.1(:[0-9]+)?(\/|$)/.test(origin);
71
+ }
72
+
73
+ if (!isAllowed) return null;
74
+
75
+ return {
76
+ allowOrigin: effectiveCredentials ? origin : (isWildcardOrigin ? '*' : origin),
77
+ allowMethods: methods.join(', '),
78
+ allowHeaders: 'Content-Type, Authorization, X-EdgeBase-Service-Key',
79
+ allowCredentials: effectiveCredentials,
80
+ maxAge: String(maxAge),
81
+ };
82
+ }
83
+
84
+ function applyCorsHeaders(
85
+ target: { set(name: string, value: string): void; get?(name: string): string | null | undefined },
86
+ headers: ResolvedCorsHeaders | null,
87
+ ): void {
88
+ if (!headers) return;
89
+
90
+ target.set('Access-Control-Allow-Origin', headers.allowOrigin);
91
+ target.set('Access-Control-Allow-Methods', headers.allowMethods);
92
+ target.set('Access-Control-Allow-Headers', headers.allowHeaders);
93
+ target.set('Access-Control-Max-Age', headers.maxAge);
94
+ if (headers.allowCredentials) {
95
+ target.set('Access-Control-Allow-Credentials', 'true');
96
+ }
97
+ target.set('Vary', 'Origin');
98
+ }
99
+
100
+ export function decorateResponseHeaders(
101
+ response: Response,
102
+ headers: ResolvedCorsHeaders | null,
103
+ ): Response {
104
+ // WebSocket upgrade responses are not normal fetch responses:
105
+ // Response() cannot be re-constructed with status 101, and browsers do not
106
+ // use CORS response headers for successful WS upgrades.
107
+ if (response.status === 101) {
108
+ return response;
109
+ }
110
+
111
+ try {
112
+ applyCorsHeaders(response.headers, headers);
113
+ response.headers.set('X-Content-Type-Options', 'nosniff');
114
+ return response;
115
+ } catch {
116
+ const cloned = new Response(response.body, {
117
+ status: response.status,
118
+ statusText: response.statusText,
119
+ headers: new Headers(response.headers),
120
+ });
121
+ applyCorsHeaders(cloned.headers, headers);
122
+ cloned.headers.set('X-Content-Type-Options', 'nosniff');
123
+ return cloned;
124
+ }
125
+ }
126
+
127
+ /**
128
+ * CORS middleware — config-aware.
129
+ *
130
+ * Reads cors config from bundled edgebase.config.ts.
131
+ * Default: allow localhost origins for development.
132
+ *
133
+ * Validates:
134
+ * - origin: '*' + credentials: true conflict (browser policy violation)
135
+ * - Wildcard patterns converted to regex matching
136
+ */
137
+ export const corsMiddleware: MiddlewareHandler<HonoEnv> = async (c, next) => {
138
+ const origin = c.req.header('Origin') || '';
139
+
140
+ // ── Parse config ──
141
+ const config = parseConfig(c.env);
142
+ const corsConfig = (config as Record<string, unknown>).cors as CorsConfig | undefined;
143
+
144
+ // ── Determine allowed origins ──
145
+ const configuredOrigins = corsConfig?.origin;
146
+ const methods = corsConfig?.methods ?? ['GET', 'POST', 'PATCH', 'DELETE', 'OPTIONS'];
147
+ const credentials = corsConfig?.credentials ?? true;
148
+ const maxAge = corsConfig?.maxAge ?? 86400;
149
+ const corsHeaders = resolveCorsHeaders(origin, configuredOrigins, methods, credentials, maxAge);
150
+
151
+ // Handle preflight
152
+ if (c.req.method === 'OPTIONS') {
153
+ applyCorsHeaders({ set: c.header.bind(c) }, corsHeaders);
154
+ return c.body(null, 204);
155
+ }
156
+
157
+ await next();
158
+ c.res = decorateResponseHeaders(c.res, corsHeaders);
159
+ };
@@ -0,0 +1,54 @@
1
+ import type { MiddlewareHandler } from 'hono';
2
+ import { HTTPException } from 'hono/http-exception';
3
+ import type { Env } from '../types.js';
4
+ import { EdgeBaseError } from '@edge-base/shared';
5
+ import { normalizeDatabaseError } from '../lib/errors.js';
6
+
7
+ type HonoEnv = { Bindings: Env };
8
+
9
+ /**
10
+ * Global error handler middleware.
11
+ * Catches all errors and returns a standardized response.
12
+ */
13
+ export const errorHandlerMiddleware: MiddlewareHandler<HonoEnv> = async (c, next) => {
14
+ try {
15
+ await next();
16
+ } catch (err) {
17
+ if (err instanceof EdgeBaseError) {
18
+ return c.json(err.toJSON(), err.code as 400);
19
+ }
20
+
21
+ // Hono HTTPException (thrown by @hono/zod-openapi validators on malformed JSON, etc.)
22
+ if (err instanceof HTTPException) {
23
+ return c.json({ code: err.status, message: err.message, slug: 'validation-failed' }, err.status as 400);
24
+ }
25
+
26
+ // Duck-type fallback for cross-module instanceof failures (Cloudflare Workers)
27
+ if (err && typeof err === 'object') {
28
+ const e = err as Record<string, unknown>;
29
+ if (typeof e.code === 'number' && e.code >= 400 && e.code < 600 && typeof e.message === 'string') {
30
+ const body: { code: number; message: string; slug?: string; data?: unknown } = { code: e.code, message: e.message };
31
+ if (typeof e.slug === 'string') body.slug = e.slug;
32
+ if (e.data) body.data = e.data;
33
+ return c.json(body, e.code as 400);
34
+ }
35
+ }
36
+
37
+ const normalizedDbError = normalizeDatabaseError(err);
38
+ if (normalizedDbError) {
39
+ return c.json(normalizedDbError.toJSON(), normalizedDbError.code as 400);
40
+ }
41
+
42
+ // Unexpected error
43
+ console.error('Unhandled error:', err);
44
+
45
+ return c.json(
46
+ {
47
+ code: 500,
48
+ message: 'Internal server error.',
49
+ slug: 'internal-error',
50
+ },
51
+ 500,
52
+ );
53
+ }
54
+ };
@@ -0,0 +1,26 @@
1
+ import type { MiddlewareHandler } from 'hono';
2
+ import type { Env } from '../types.js';
3
+
4
+ type HonoEnv = { Bindings: Env };
5
+
6
+ /**
7
+ * Internal guard middleware.
8
+ * Blocks ALL external access to /internal/* endpoints unconditionally.
9
+ *
10
+ * SECURITY: The X-EdgeBase-Internal header MUST NOT be trusted from external
11
+ * requests — any client can set arbitrary headers. Workers cannot strip
12
+ * incoming headers (Request.headers is immutable), so we simply block all
13
+ * /internal/* requests at this middleware regardless of headers.
14
+ *
15
+ * DO-to-DO calls use the Worker's own internal routing (same-process), not the
16
+ * public /internal/* path, so legitimate internal calls never reach this guard.
17
+ */
18
+ export const internalGuardMiddleware: MiddlewareHandler<HonoEnv> = async (c) => {
19
+ return c.json(
20
+ {
21
+ code: 403,
22
+ message: 'Access denied. Internal endpoints are not publicly accessible.',
23
+ },
24
+ 403,
25
+ );
26
+ };