@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,53 @@
1
+ /**
2
+ * Public Config Route
3
+ *
4
+ * GET /api/config — Returns publicly-safe configuration.
5
+ * No authentication required.
6
+ * Currently exposes captcha siteKey for client-side Turnstile rendering.
7
+ *
8
+ * siteKey is served from CAPTCHA_SITE_KEY env var (set by deploy.ts after provisioning).
9
+ * Falls back to bundled runtime config for advanced captcha object configs.
10
+ */
11
+ import { OpenAPIHono, createRoute, type HonoEnv } from '../lib/hono.js';
12
+ import { parseConfig } from '../lib/do-router.js';
13
+ import { zodDefaultHook, jsonResponseSchema } from '../lib/schemas.js';
14
+
15
+
16
+ export const configRoute = new OpenAPIHono<HonoEnv>({ defaultHook: zodDefaultHook });
17
+
18
+ const getConfig = createRoute({
19
+ operationId: 'getConfig',
20
+ method: 'get',
21
+ path: '/',
22
+ tags: ['client'],
23
+ summary: 'Get public configuration',
24
+ responses: {
25
+ 200: { description: 'Public config', content: { 'application/json': { schema: jsonResponseSchema } } },
26
+ },
27
+ });
28
+
29
+ configRoute.openapi(getConfig, (c) => {
30
+ let captcha: { siteKey: string } | null = null;
31
+
32
+ try {
33
+ // §34: CAPTCHA_SITE_KEY env var takes priority (set by deploy.ts)
34
+ if (c.env.CAPTCHA_SITE_KEY) {
35
+ captcha = { siteKey: c.env.CAPTCHA_SITE_KEY };
36
+ } else {
37
+ // Fallback: parseConfig() singleton for advanced captcha object configs
38
+ const config = parseConfig(c.env);
39
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
+ const captchaCfg = (config as any)?.captcha;
41
+ if (captchaCfg && typeof captchaCfg === 'object' && captchaCfg.siteKey) {
42
+ captcha = { siteKey: captchaCfg.siteKey };
43
+ }
44
+ }
45
+ } catch {
46
+ // Return null captcha on error
47
+ }
48
+
49
+ return c.json({ captcha }, 200, {
50
+ 'Cache-Control': 'public, max-age=60, s-maxage=60',
51
+ 'CDN-Cache-Control': 'public, max-age=60',
52
+ });
53
+ });
@@ -0,0 +1,109 @@
1
+ /**
2
+ * D1 route — POST /api/d1/:database
3
+ *
4
+ * Allows server SDK (with Service Key) to execute raw SQL on user-defined D1 databases.
5
+ * NOT available to client SDK (server-only,).
6
+ *
7
+ * Security:
8
+ * - Config Allowlist: database must be declared in config.d1
9
+ * - Service Key required with scoped validation
10
+ * - Internal D1 binding (AUTH_DB) is never exposed
11
+ * - DDL allowed — Service Key holders are admin-level trusted (#75)
12
+ * - ? bind variables enforced (SQL injection prevention)
13
+ *
14
+ * Flow: Server SDK → POST /api/d1/:database → Worker → D1 binding → JSON
15
+ */
16
+ import { OpenAPIHono, createRoute, z, type HonoEnv } from '../lib/hono.js';
17
+ import { EdgeBaseError } from '@edge-base/shared';
18
+ import { parseConfig } from '../lib/do-router.js';
19
+ import { validateKey, buildConstraintCtx } from '../lib/service-key.js';
20
+ import { zodDefaultHook, d1BodySchema, jsonResponseSchema, errorResponseSchema } from '../lib/schemas.js';
21
+
22
+
23
+ export const d1Route = new OpenAPIHono<HonoEnv>({ defaultHook: zodDefaultHook });
24
+
25
+ /**
26
+ * POST /api/d1/:database
27
+ * Body: { query: string, params?: unknown[] }
28
+ */
29
+ const executeD1Query = createRoute({
30
+ operationId: 'executeD1Query',
31
+ method: 'post',
32
+ path: '/{database}',
33
+ tags: ['admin'],
34
+ summary: 'Execute raw SQL on D1 database',
35
+ request: {
36
+ params: z.object({ database: z.string() }),
37
+ body: { content: { 'application/json': { schema: d1BodySchema } }, required: true },
38
+ },
39
+ responses: {
40
+ 200: { description: 'Query results', content: { 'application/json': { schema: jsonResponseSchema } } },
41
+ 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
42
+ 401: { description: 'Unauthorized', content: { 'application/json': { schema: errorResponseSchema } } },
43
+ 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
44
+ },
45
+ });
46
+
47
+ d1Route.openapi(executeD1Query, async (c) => {
48
+ const nameParam = c.req.param('database')!;
49
+
50
+ let body: { query?: string; params?: unknown[] };
51
+ try {
52
+ body = await c.req.json();
53
+ } catch {
54
+ return c.json({ code: 400, message: 'Invalid JSON body' }, 400);
55
+ }
56
+
57
+ const { query, params } = body;
58
+ if (!query || typeof query !== 'string') {
59
+ return c.json({ code: 400, message: 'query is required' }, 400);
60
+ }
61
+
62
+ // §2 Allowlist: validate database is declared in config
63
+ const config = parseConfig(c.env);
64
+ const d1Config = config.d1?.[nameParam];
65
+ if (!d1Config) {
66
+ return c.json({ code: 404, message: `D1 database '${nameParam}' not found in config.` }, 404);
67
+ }
68
+
69
+ // §4 Scope: d1:database:{name}:exec
70
+ const { result: skResult } = validateKey(
71
+ c.req.header('X-EdgeBase-Service-Key'),
72
+ `d1:database:${nameParam}:exec`,
73
+ config,
74
+ c.env,
75
+ undefined,
76
+ buildConstraintCtx(c.env, c.req),
77
+ );
78
+ if (skResult === 'missing') {
79
+ return c.json({ code: 403, message: 'Service Key required to access D1' }, 403);
80
+ }
81
+ if (skResult === 'invalid') {
82
+ return c.json({ code: 401, message: 'Unauthorized. Invalid Service Key.' }, 401);
83
+ }
84
+
85
+ // §1 Env type — dynamic binding access via type assertion
86
+ const binding = (c.env as unknown as Record<string, unknown>)[d1Config.binding] as D1Database | undefined;
87
+ if (!binding) {
88
+ return c.json({ code: 500, message: `D1 binding '${d1Config.binding}' not available.` }, 500);
89
+ }
90
+
91
+ // Execute D1 query — all SQL allowed (DDL included), ? bind variables enforced
92
+ try {
93
+ const stmt = binding.prepare(query);
94
+ const boundStmt = params && params.length > 0 ? stmt.bind(...params) : stmt;
95
+ const result = await boundStmt.all();
96
+ return c.json({
97
+ results: result.results,
98
+ meta: {
99
+ changes: result.meta?.changes,
100
+ duration: result.meta?.duration,
101
+ rows_read: result.meta?.rows_read,
102
+ rows_written: result.meta?.rows_written,
103
+ },
104
+ });
105
+ } catch (err) {
106
+ const message = err instanceof Error ? err.message : 'D1 query execution failed';
107
+ throw new EdgeBaseError(400, message);
108
+ }
109
+ });
@@ -0,0 +1,281 @@
1
+ import { OpenAPIHono, createRoute, z, type HonoEnv } from '../lib/hono.js';
2
+ import {
3
+ acquirePendingWebSocketSlot,
4
+ getPendingWebSocketCount,
5
+ releasePendingWebSocketSlot,
6
+ } from '../lib/websocket-pending.js';
7
+ import {
8
+ buildDbLiveChannel,
9
+ DATABASE_LIVE_HUB_DO_NAME,
10
+ isDbLiveChannel,
11
+ } from '../lib/database-live-emitter.js';
12
+ import { parseConfig } from '../lib/do-router.js';
13
+ import { validateKey, buildConstraintCtx } from '../lib/service-key.js';
14
+ import { getTrustedClientIp } from '../lib/client-ip.js';
15
+ import {
16
+ zodDefaultHook,
17
+ broadcastBodySchema,
18
+ jsonResponseSchema,
19
+ errorResponseSchema,
20
+ } from '../lib/schemas.js';
21
+
22
+ export const databaseLiveRoute = new OpenAPIHono<HonoEnv>({ defaultHook: zodDefaultHook });
23
+
24
+ const MAX_PENDING_PER_IP = 5;
25
+ const PENDING_TTL_SECONDS = 10;
26
+ const dbLiveQuerySchema = z.object({
27
+ channel: z.string().optional().openapi({ description: 'Legacy DB subscription channel name' }),
28
+ namespace: z.string().optional().openapi({ description: 'Database namespace', example: 'shared' }),
29
+ instanceId: z.string().optional().openapi({ description: 'Database instance ID for dynamic DB blocks', example: 'ws-456' }),
30
+ table: z.string().optional().openapi({ description: 'Table name', example: 'posts' }),
31
+ docId: z.string().optional().openapi({ description: 'Optional document ID for single-document subscriptions', example: 'post-1' }),
32
+ }).refine(
33
+ (value) => {
34
+ if (value.channel) return true;
35
+ return !!value.namespace && !!value.table;
36
+ },
37
+ {
38
+ message: 'Provide channel or namespace + table',
39
+ path: ['channel'],
40
+ },
41
+ );
42
+
43
+ const dbConnectDiagnosticSchema = z.object({
44
+ ok: z.boolean(),
45
+ type: z.string(),
46
+ category: z.string(),
47
+ message: z.string(),
48
+ channel: z.string().optional(),
49
+ pendingCount: z.number().optional(),
50
+ maxPending: z.number().optional(),
51
+ });
52
+
53
+ function resolveDatabaseLiveChannel(query: {
54
+ channel?: string;
55
+ namespace?: string;
56
+ instanceId?: string;
57
+ table?: string;
58
+ docId?: string;
59
+ }): string | null {
60
+ if (query.channel) {
61
+ return isDbLiveChannel(query.channel) ? query.channel : null;
62
+ }
63
+ if (!query.namespace || !query.table) return null;
64
+ return buildDbLiveChannel(query.namespace, query.table, query.instanceId, query.docId);
65
+ }
66
+
67
+ function getPendingKey(ip: string): string {
68
+ return `ws:pending:${ip}`;
69
+ }
70
+
71
+ const checkDatabaseConnection = createRoute({
72
+ operationId: 'checkDatabaseSubscriptionConnection',
73
+ method: 'get',
74
+ path: '/connect-check',
75
+ tags: ['client'],
76
+ summary: 'Check database live subscription WebSocket prerequisites',
77
+ request: {
78
+ query: dbLiveQuerySchema,
79
+ },
80
+ responses: {
81
+ 200: { description: 'Database live connection looks ready', content: { 'application/json': { schema: dbConnectDiagnosticSchema } } },
82
+ 400: { description: 'Bad request', content: { 'application/json': { schema: dbConnectDiagnosticSchema } } },
83
+ 429: { description: 'Rate limited', content: { 'application/json': { schema: dbConnectDiagnosticSchema } } },
84
+ },
85
+ });
86
+
87
+ databaseLiveRoute.openapi(checkDatabaseConnection, async (c) => {
88
+ const channel = resolveDatabaseLiveChannel({
89
+ channel: c.req.query('channel') ?? undefined,
90
+ namespace: c.req.query('namespace') ?? undefined,
91
+ instanceId: c.req.query('instanceId') ?? undefined,
92
+ table: c.req.query('table') ?? undefined,
93
+ docId: c.req.query('docId') ?? undefined,
94
+ });
95
+
96
+ if (!channel) {
97
+ return c.json({
98
+ ok: false,
99
+ type: 'db_connect_invalid_request',
100
+ category: 'request',
101
+ message: 'Database subscription target required',
102
+ }, 400);
103
+ }
104
+
105
+ const ip = getTrustedClientIp(c.env, c.req) ?? 'unknown';
106
+ const kvKey = getPendingKey(ip);
107
+ const pendingCount = await getPendingWebSocketCount(c.env.KV, kvKey).catch(() => 0);
108
+ if (pendingCount >= MAX_PENDING_PER_IP) {
109
+ return c.json({
110
+ ok: false,
111
+ type: 'db_connect_rate_limited',
112
+ category: 'rate_limit',
113
+ message: 'Too many pending WebSocket connections',
114
+ channel,
115
+ pendingCount,
116
+ maxPending: MAX_PENDING_PER_IP,
117
+ }, 429);
118
+ }
119
+
120
+ return c.json({
121
+ ok: true,
122
+ type: 'db_connect_ready',
123
+ category: 'ready',
124
+ message: 'Database live subscription preflight passed',
125
+ channel,
126
+ pendingCount,
127
+ maxPending: MAX_PENDING_PER_IP,
128
+ }, 200);
129
+ });
130
+
131
+ const connectDatabaseSubscription = createRoute({
132
+ operationId: 'connectDatabaseSubscription',
133
+ method: 'get',
134
+ path: '/subscribe',
135
+ tags: ['client'],
136
+ summary: 'Connect to database live subscriptions WebSocket',
137
+ description: 'Database-owned WebSocket entrypoint for onSnapshot subscriptions. Requires Upgrade: websocket header.',
138
+ request: {
139
+ query: dbLiveQuerySchema,
140
+ },
141
+ responses: {
142
+ 101: { description: 'WebSocket upgrade successful' },
143
+ 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
144
+ 429: { description: 'Rate limited', content: { 'application/json': { schema: errorResponseSchema } } },
145
+ },
146
+ });
147
+
148
+ /**
149
+ * POST /broadcast
150
+ * Server-side broadcast to a database-live channel.
151
+ * Service Key required AND validated.
152
+ * Body: { channel: string, event: string, payload?: Record<string, unknown> }
153
+ */
154
+ const databaseLiveBroadcast = createRoute({
155
+ operationId: 'databaseLiveBroadcast',
156
+ method: 'post',
157
+ path: '/broadcast',
158
+ tags: ['admin'],
159
+ summary: 'Broadcast to database live channel',
160
+ request: {
161
+ body: { content: { 'application/json': { schema: broadcastBodySchema } }, required: true },
162
+ },
163
+ responses: {
164
+ 200: { description: 'Broadcast sent', content: { 'application/json': { schema: jsonResponseSchema } } },
165
+ 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
166
+ 401: { description: 'Unauthorized', content: { 'application/json': { schema: errorResponseSchema } } },
167
+ 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
168
+ },
169
+ });
170
+
171
+ databaseLiveRoute.openapi(databaseLiveBroadcast, async (c) => {
172
+ let body: { channel?: string; event?: string; payload?: Record<string, unknown> };
173
+ try {
174
+ body = await c.req.json();
175
+ } catch {
176
+ return c.json({ code: 400, message: 'Invalid JSON body' }, 400);
177
+ }
178
+
179
+ const { channel, event, payload } = body;
180
+ if (!channel || typeof channel !== 'string') {
181
+ return c.json({ code: 400, message: 'channel is required' }, 400);
182
+ }
183
+ if (!event || typeof event !== 'string') {
184
+ return c.json({ code: 400, message: 'event is required' }, 400);
185
+ }
186
+
187
+ // Service Key required AND validated
188
+ const config = parseConfig(c.env);
189
+ const { result: skResult } = validateKey(
190
+ c.req.header('X-EdgeBase-Service-Key'),
191
+ `db:channel:${channel}:broadcast`,
192
+ config,
193
+ c.env,
194
+ undefined,
195
+ buildConstraintCtx(c.env, c.req),
196
+ );
197
+ if (skResult === 'missing') {
198
+ return c.json({ code: 403, message: 'Service Key required for server broadcast' }, 403);
199
+ }
200
+ if (skResult === 'invalid') {
201
+ return c.json({ code: 401, message: 'Unauthorized. Invalid Service Key.' }, 401);
202
+ }
203
+
204
+ // Route broadcast through the DatabaseLiveDO hub
205
+ const doId = c.env.DATABASE_LIVE.idFromName(DATABASE_LIVE_HUB_DO_NAME);
206
+ const doStub = c.env.DATABASE_LIVE.get(doId);
207
+
208
+ const doResponse = await doStub.fetch(new Request('http://do/internal/broadcast', {
209
+ method: 'POST',
210
+ headers: { 'Content-Type': 'application/json' },
211
+ body: JSON.stringify({ channel, event, payload: payload ?? {} }),
212
+ }));
213
+
214
+ if (!doResponse.ok) {
215
+ return c.json({ code: doResponse.status, message: 'Broadcast failed' }, doResponse.status as 400 | 500);
216
+ }
217
+
218
+ return c.json({ ok: true });
219
+ });
220
+
221
+ databaseLiveRoute.openapi(connectDatabaseSubscription, async (c) => {
222
+ const upgradeHeader = c.req.header('Upgrade');
223
+ if (upgradeHeader !== 'websocket') {
224
+ return c.json({ code: 400, message: 'Expected WebSocket upgrade' }, 400);
225
+ }
226
+
227
+ const channel = resolveDatabaseLiveChannel({
228
+ channel: c.req.query('channel') ?? undefined,
229
+ namespace: c.req.query('namespace') ?? undefined,
230
+ instanceId: c.req.query('instanceId') ?? undefined,
231
+ table: c.req.query('table') ?? undefined,
232
+ docId: c.req.query('docId') ?? undefined,
233
+ });
234
+ if (!channel) {
235
+ return c.json({ code: 400, message: 'Database subscription target required' }, 400);
236
+ }
237
+
238
+ const ip = getTrustedClientIp(c.env, c.req) ?? 'unknown';
239
+ const kvKey = getPendingKey(ip);
240
+ let pendingTracked = false;
241
+
242
+ try {
243
+ pendingTracked = await acquirePendingWebSocketSlot(
244
+ c.env.KV,
245
+ kvKey,
246
+ MAX_PENDING_PER_IP,
247
+ PENDING_TTL_SECONDS,
248
+ );
249
+ if (!pendingTracked) {
250
+ return c.json({
251
+ code: 429,
252
+ message: 'Too many pending WebSocket connections',
253
+ }, 429);
254
+ }
255
+ } catch {
256
+ // KV failure shouldn't block legitimate connections
257
+ }
258
+
259
+ const doId = c.env.DATABASE_LIVE.idFromName(DATABASE_LIVE_HUB_DO_NAME);
260
+ const doStub = c.env.DATABASE_LIVE.get(doId);
261
+
262
+ const url = new URL(c.req.url);
263
+ url.pathname = '/websocket';
264
+ url.search = '';
265
+ url.searchParams.set('channel', channel);
266
+ const doRequest = new Request(url.toString(), {
267
+ headers: c.req.raw.headers,
268
+ });
269
+
270
+ try {
271
+ return await doStub.fetch(doRequest);
272
+ } finally {
273
+ if (pendingTracked) {
274
+ try {
275
+ await releasePendingWebSocketSlot(c.env.KV, kvKey, PENDING_TTL_SECONDS);
276
+ } catch {
277
+ // KV failure shouldn't break an already accepted connection.
278
+ }
279
+ }
280
+ }
281
+ });
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Functions Route - HTTP trigger handler with file-system routing.
3
+ *
4
+ * Handles requests to /api/functions/:functionName
5
+ * Supports:
6
+ * - Static routes: /api/functions/hello
7
+ * - Dynamic params: /api/functions/users/abc123/profile
8
+ * - Catch-all: /api/functions/docs/a/b/c
9
+ * - Directory middleware (_middleware.ts)
10
+ * - FunctionError structured error responses
11
+ */
12
+ import { OpenAPIHono, type HonoEnv } from '../lib/hono.js';
13
+ import {
14
+ matchRoute,
15
+ routeExistsForPath,
16
+ buildFunctionContext,
17
+ getMiddlewareChain,
18
+ getWorkerUrl,
19
+ } from '../lib/functions.js';
20
+ import { parseConfig } from '../lib/do-router.js';
21
+ import { resolveRootServiceKey } from '../lib/service-key.js';
22
+ import type { AuthContext } from '../lib/functions.js';
23
+ import { captchaMiddleware } from '../middleware/captcha-verify.js';
24
+ import { FunctionError } from '@edge-base/shared';
25
+
26
+ function isFunctionErrorLike(
27
+ value: unknown,
28
+ ): value is {
29
+ code: string;
30
+ message: string;
31
+ httpStatus?: number;
32
+ status?: number;
33
+ details?: Record<string, unknown>;
34
+ toJSON?: () => unknown;
35
+ } {
36
+ if (!value || typeof value !== 'object') return false;
37
+ const candidate = value as Record<string, unknown>;
38
+ return (
39
+ typeof candidate.code === 'string' &&
40
+ typeof candidate.message === 'string' &&
41
+ (typeof candidate.httpStatus === 'number' || typeof candidate.status === 'number')
42
+ );
43
+ }
44
+
45
+ export const functionsRoute = new OpenAPIHono<HonoEnv>();
46
+
47
+ /**
48
+ * Dynamic HTTP trigger handler with pattern matching.
49
+ */
50
+ functionsRoute.all('/:functionName{.+}', async (c) => {
51
+ const functionName = c.req.param('functionName');
52
+ const method = c.req.method.toUpperCase();
53
+
54
+ // Match route with pattern matching (handles [param], [...slug], static routes)
55
+ const matched = matchRoute(functionName, method);
56
+
57
+ if (!matched) {
58
+ // Check if route exists but method is wrong → 405
59
+ if (routeExistsForPath(functionName)) {
60
+ return c.json(
61
+ { code: 405, message: `Method ${method} not allowed for '${functionName}'.` },
62
+ 405,
63
+ );
64
+ }
65
+ return c.json(
66
+ { code: 404, message: `Function '${functionName}' not found.` },
67
+ 404,
68
+ );
69
+ }
70
+
71
+ const config = parseConfig(c.env);
72
+ const serviceKey = resolveRootServiceKey(config, c.env);
73
+
74
+ // Auth context from middleware (set by authMiddleware earlier in chain)
75
+ const auth = (c.get('auth' as never) || null) as AuthContext | null;
76
+
77
+ const workerUrl = getWorkerUrl(c.req.url, c.env) ?? 'http://localhost';
78
+
79
+ const ctx = buildFunctionContext({
80
+ request: c.req.raw,
81
+ auth,
82
+ databaseNamespace: c.env.DATABASE,
83
+ authNamespace: c.env.AUTH,
84
+ d1Database: c.env.AUTH_DB,
85
+ kvNamespace: c.env.KV,
86
+ env: c.env as never,
87
+ executionCtx: c.executionCtx as never,
88
+ config,
89
+ serviceKey,
90
+ workerUrl,
91
+ params: matched.params,
92
+ });
93
+
94
+ // Captcha check for functions with captcha: true
95
+ if (matched.route.definition.captcha) {
96
+ const captchaMw = captchaMiddleware(`function:${matched.route.name}`);
97
+ await captchaMw(c, async () => {});
98
+ if (c.res && c.res.status === 403) {
99
+ return c.res;
100
+ }
101
+ }
102
+
103
+ try {
104
+ // Execute middleware chain (root → nested → handler)
105
+ const middlewares = getMiddlewareChain(matched.route.name);
106
+ for (const mw of middlewares) {
107
+ await mw(ctx);
108
+ }
109
+
110
+ // Execute handler
111
+ const result = await matched.route.definition.handler(ctx);
112
+
113
+ // If handler returns a Response, use it directly
114
+ if (result instanceof Response) {
115
+ return result;
116
+ }
117
+
118
+ // If handler returns an object, JSON-serialize it
119
+ if (result && typeof result === 'object') {
120
+ return c.json(result);
121
+ }
122
+
123
+ // If handler returns a string, return as text
124
+ if (typeof result === 'string') {
125
+ return c.text(result);
126
+ }
127
+
128
+ // Default: 204 No Content
129
+ return c.body(null, 204);
130
+ } catch (err: unknown) {
131
+ // Handle FunctionError specially — return structured JSON
132
+ if (err instanceof FunctionError) {
133
+ return c.json(err.toJSON(), err.httpStatus as 400);
134
+ }
135
+
136
+ // Cloudflare/local dev can bundle user functions with a second copy of
137
+ // @edge-base/shared, so instanceof is not reliable across that boundary.
138
+ if (isFunctionErrorLike(err)) {
139
+ const status = err.httpStatus ?? err.status ?? 500;
140
+ const body =
141
+ typeof err.toJSON === 'function'
142
+ ? err.toJSON()
143
+ : {
144
+ code: err.code,
145
+ message: err.message,
146
+ status,
147
+ ...(err.details ? { details: err.details } : {}),
148
+ };
149
+ return c.json(body, status as 400);
150
+ }
151
+
152
+ console.error(`[EdgeBase] HTTP function '${matched.route.name}' error:`, err);
153
+ return c.json({ code: 500, message: 'Function execution failed.' }, 500);
154
+ }
155
+ });
@@ -0,0 +1,32 @@
1
+ import { OpenAPIHono, createRoute, type HonoEnv } from '../lib/hono.js';
2
+ import { healthResponseSchema, zodDefaultHook } from '../lib/schemas.js';
3
+ import { SERVER_VERSION } from '../lib/version.js';
4
+
5
+
6
+ export const healthRoute = new OpenAPIHono<HonoEnv>({ defaultHook: zodDefaultHook });
7
+
8
+ /**
9
+ * GET /api/health — Health check endpoint.
10
+ * Returns server status and version information.
11
+ */
12
+ const getHealth = createRoute({
13
+ operationId: 'getHealth',
14
+ method: 'get',
15
+ path: '/health',
16
+ tags: ['client'],
17
+ summary: 'Health check',
18
+ responses: {
19
+ 200: {
20
+ description: 'Server is healthy',
21
+ content: { 'application/json': { schema: healthResponseSchema } },
22
+ },
23
+ },
24
+ });
25
+
26
+ healthRoute.openapi(getHealth, (c) => {
27
+ return c.json({
28
+ status: 'ok',
29
+ version: SERVER_VERSION,
30
+ timestamp: new Date().toISOString(),
31
+ }, 200);
32
+ });