@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,475 @@
1
+ /**
2
+ * Rules Middleware — Function-based access rules evaluation
3
+ *
4
+ * Pipeline: Auth → Rules → Handler(DO proxy)
5
+ *
6
+ * Worker level (before DO):
7
+ * - DB-level access: canCreate(auth, id) / access(auth, id) — §4
8
+ * - Table-level create: create(auth) — no row needed
9
+ * - Service Key bypass — §1
10
+ *
11
+ * DO level (inside database-do.ts):
12
+ * - Table-level read/update/delete: evaluated per-row with actual row data
13
+ * - All-or-Nothing: any row failing read → full 403 — §7
14
+ *
15
+ * Rules are TypeScript functions — no string DSL, no Pratt parser — §3.
16
+ * Timeout: Worker rules 50ms, DO rules 10ms — fail-closed — §12①.
17
+ */
18
+ import type { Context, Next } from 'hono';
19
+ import type { Env } from '../types.js';
20
+ import {
21
+ validateKey,
22
+ buildKeymap,
23
+ resolveServiceKeyCandidate,
24
+ type ConstraintContext,
25
+ } from '../lib/service-key.js';
26
+ import {
27
+ callDO,
28
+ findTableNamespace,
29
+ getDbDoName,
30
+ parseConfig,
31
+ shouldRouteToD1,
32
+ } from '../lib/do-router.js';
33
+ import type { AuthContext, TableRules, DbLevelRules } from '@edge-base/shared';
34
+ import { EdgeBaseError, getDbAccess, getTableAccess } from '@edge-base/shared';
35
+ import { handleD1Request } from '../lib/d1-handler.js';
36
+ import { handlePgRequest } from '../lib/postgres-handler.js';
37
+ import { buildInternalHandlerContext } from '../lib/internal-request.js';
38
+ import { getTrustedClientIp } from '../lib/client-ip.js';
39
+
40
+ type HonoContext = Context<{ Bindings: Env }>;
41
+ const WORKER_RULE_TIMEOUT_MS = 50;
42
+ const DB_ACCESS_RULE_TIMEOUT_MS = 2000;
43
+
44
+ /**
45
+ * Normalize a raw rule value (function | boolean | string) into a callable.
46
+ *
47
+ * JSON-deserialized test config cannot encode
48
+ * functions — rules arrive as boolean literals or expression strings like
49
+ * "auth != null" or "auth.id == resource.authorId".
50
+ * Live edgebase.config.js rules are real JS functions.
51
+ */
52
+ function normalizeRule<T extends unknown[]>(
53
+ rule: ((...args: T) => boolean | Promise<boolean>) | boolean | string | undefined,
54
+ ): ((...args: T) => boolean) | null {
55
+ if (rule === undefined || rule === null) return null;
56
+ if (typeof rule === 'boolean') return () => rule;
57
+ if (typeof rule === 'function') return rule as (...args: T) => boolean;
58
+ // String expression — simple interpreter for common patterns
59
+ if (typeof rule === 'string') {
60
+ return (...args: T) => evalStringRule(rule, args[0] as AuthContext | null, args[1] as Record<string, unknown> | undefined);
61
+ }
62
+ return null;
63
+ }
64
+
65
+ /**
66
+ * Evaluate a simple string rule expression against auth and resource.
67
+ * Supports: 'true', 'false', 'auth != null', 'auth !== null',
68
+ * 'auth.id == resource.X', 'auth.id === resource.X'.
69
+ */
70
+ function evalStringRule(
71
+ expr: string,
72
+ auth: AuthContext | null,
73
+ resource?: Record<string, unknown>,
74
+ ): boolean {
75
+ const e = expr.trim().replace(/\s+/g, ' ');
76
+ if (e === 'true') return true;
77
+ if (e === 'false') return false;
78
+ if (e === 'auth != null' || e === 'auth !== null') return auth !== null;
79
+ if (e === 'auth == null' || e === 'auth === null') return auth === null;
80
+ // auth.id == resource.X
81
+ const authIdEqResource = /^auth\.id ===? resource\.(\w+)$/.exec(e);
82
+ if (authIdEqResource) {
83
+ const field = authIdEqResource[1];
84
+ return auth !== null && resource !== undefined && auth.id === resource[field];
85
+ }
86
+ // Default: deny (fail-closed for unknown/unsupported expressions)
87
+ console.warn(`[Rules] Unrecognized string rule expression: "${expr}" — denied (fail-closed).`);
88
+ return false;
89
+ }
90
+
91
+ // ─── Rule Evaluation with Timeout (§12①) ───
92
+
93
+ /**
94
+ * Evaluate a rule function with a timeout.
95
+ * Accepts function, boolean or string rule values.
96
+ * Fail-closed: timeout or error → false (deny).
97
+ */
98
+ async function evalWithTimeout(
99
+ fn: (() => boolean | Promise<boolean>),
100
+ timeoutMs: number,
101
+ ): Promise<boolean> {
102
+ try {
103
+ const result = fn();
104
+ if (typeof result === 'boolean') return result;
105
+ // Promise — race with timeout
106
+ return await Promise.race([
107
+ result,
108
+ new Promise<boolean>((resolve) => setTimeout(() => resolve(false), timeoutMs)),
109
+ ]);
110
+ } catch {
111
+ // Any error → deny (fail-closed)
112
+ return false;
113
+ }
114
+ }
115
+
116
+ // ─── Main Rules Middleware ───
117
+
118
+ /**
119
+ * Rules middleware for database table endpoints.
120
+ * Must be mounted on /api/db/* in the main app.
121
+ */
122
+ export async function rulesMiddleware(c: HonoContext, next: Next): Promise<Response | void> {
123
+ const path = new URL(c.req.raw.url).pathname;
124
+
125
+ // Extract namespace, instanceId, tableName from /api/db/:namespace/tables/:table[/*]
126
+ // or /api/db/:namespace/:instanceId/tables/:table[/*]
127
+ // pathParts: ['api','db','shared','tables','posts'] or ['api','db','workspace','ws-456','tables','docs']
128
+ const pathParts = path.split('/').filter(Boolean);
129
+ const tablesIdx = pathParts.indexOf('tables');
130
+ if (pathParts[1] !== 'db' || tablesIdx === -1) {
131
+ return next();
132
+ }
133
+ const rawTableName = pathParts[tablesIdx + 1];
134
+ const tableName = rawTableName ? decodeURIComponent(rawTableName) : rawTableName;
135
+ const namespace = pathParts[2];
136
+ // instanceId present when path is /api/db/:ns/:id/tables/:name
137
+ const instanceId = tablesIdx === 4 ? pathParts[3] : undefined;
138
+ if (!tableName) return next();
139
+
140
+ // ── Step 0: Internal call bypass ──
141
+ const isInternal = c.get('isInternalRequest' as never) === true;
142
+ if (isInternal) {
143
+ return next();
144
+ }
145
+
146
+ const config = parseConfig(c.env);
147
+ const auth = c.get('auth') as AuthContext | null;
148
+ const dbId = instanceId ||
149
+ c.req.header('X-EdgeBase-DB-Id') ||
150
+ extractAuthIdForUserNamespace(namespace, auth);
151
+
152
+ // ── Step 1: Service Key check ──
153
+ const provided = resolveServiceKeyCandidate(
154
+ c.req,
155
+ c.get('serviceKeyToken') as string | null | undefined,
156
+ );
157
+ const httpMethod = c.req.raw.method.toUpperCase();
158
+ const isReadMethod = httpMethod === 'GET' || httpMethod === 'HEAD';
159
+ const scopeAction = isReadMethod ? 'read' : 'write';
160
+ const requiredScope = `db:table:${tableName}:${scopeAction}`;
161
+
162
+ const constraintCtx: ConstraintContext = {
163
+ env: c.env.ENVIRONMENT,
164
+ ip: getTrustedClientIp(c.env, c.req),
165
+ };
166
+ if (dbId) constraintCtx.tenantId = dbId;
167
+
168
+ // BUG-007 fix: build keymap once per request (not internally on every validateKey call)
169
+ const keymap = buildKeymap(config, c.env);
170
+ const { result: keyResult } = validateKey(provided, requiredScope, config, c.env, keymap, constraintCtx);
171
+
172
+ if (keyResult === 'valid') {
173
+ c.set('isServiceKey' as never, true);
174
+ return next();
175
+ }
176
+ if (keyResult === 'invalid') {
177
+ throw new EdgeBaseError(401, 'Unauthorized. Invalid Service Key.');
178
+ }
179
+ // keyResult === 'missing' → continue to normal rules evaluation
180
+
181
+ // ── Step 2: Find DB block by namespace (from URL — §2) ──
182
+ if (!config.databases) {
183
+ // No databases config → release mode check
184
+ if (!config.release) return next();
185
+ throw new EdgeBaseError(403, `Access denied. No databases config defined.`);
186
+ }
187
+
188
+ // namespace comes directly from the URL (/api/db/:namespace/...)
189
+ const tableNamespace = namespace;
190
+ const dbBlock = config.databases[tableNamespace];
191
+
192
+ if (!dbBlock) {
193
+ // Namespace not found in config → deny
194
+ if (!config.release) return next();
195
+ throw new EdgeBaseError(403, `Access denied. Namespace '${tableNamespace}' is not configured.`);
196
+ }
197
+
198
+ if (dbBlock.tables && !dbBlock.tables[tableName]) {
199
+ // Table not defined in this DB block
200
+ if (!config.release) return next();
201
+ throw new EdgeBaseError(403, `Access denied. Table '${tableName}' has no access rules defined.`);
202
+ }
203
+
204
+ // ── Step 3: DB-level access check (§4) ──
205
+ // instanceId from URL (/api/db/:ns/:id/tables/:name) or fallback for 'user' namespace
206
+ const dbRules = getDbAccess(dbBlock) as DbLevelRules | undefined;
207
+ if (dbRules && dbId !== undefined) {
208
+ // access: check if user can access this DO instance
209
+ if (dbRules.access) {
210
+ const dbRuleCtx = buildDbRuleCtx(c, config, namespace, dbId);
211
+ const canAccess = await evalWithTimeout(
212
+ () => dbRules.access!(auth, dbId, dbRuleCtx),
213
+ // Dynamic DB access rules often perform an internal DB/DO lookup.
214
+ // Cold starts on deployed workers regularly exceed 50ms, so a short
215
+ // ceiling turns valid tenant memberships into false 403s.
216
+ DB_ACCESS_RULE_TIMEOUT_MS,
217
+ );
218
+ if (!canAccess) {
219
+ throw new EdgeBaseError(403, `Access denied. You do not have access to ${tableNamespace}:${dbId}.`);
220
+ }
221
+ }
222
+ // NOTE: DbLevelRules.delete exists in the type definition (config.ts) but is not
223
+ // enforced here because no public/admin endpoint for deleting a DB block instance (DO)
224
+ // exists yet. canCreate is evaluated in tables.ts (§36 2-RTT flow), access is evaluated
225
+ // above. When a DB instance deletion endpoint is added, enforce dbRules.delete(auth, dbId)
226
+ // at the corresponding route handler before allowing the operation.
227
+ }
228
+
229
+ // ── Step 4: Table-level rules (create at Worker level, others at DO level) ──
230
+ const tableConfig = dbBlock.tables?.[tableName];
231
+ const tableRules = getTableAccess(tableConfig) as TableRules | undefined;
232
+
233
+ if (!tableRules) {
234
+ if (!config.release) return next();
235
+ throw new EdgeBaseError(403, `Access denied. No access rules defined for '${tableName}'.`);
236
+ }
237
+
238
+ // Determine action
239
+ const hasUpsert = c.req.query('upsert') === 'true';
240
+ const action = getAction(httpMethod, path, hasUpsert);
241
+
242
+ // Handle upsert (§16)
243
+ if (hasUpsert && httpMethod === 'POST') {
244
+ const insertRuleFn = normalizeRule(tableRules.insert);
245
+ const updateRuleFn = normalizeRule(tableRules.update);
246
+
247
+ if (!insertRuleFn && !updateRuleFn) {
248
+ if (!config.release) return next();
249
+ throw new EdgeBaseError(403, `Access denied. No insert or update rules for '${tableName}'.`);
250
+ }
251
+
252
+ // Worker pre-check: insert(auth) OR update(auth, null) — §16
253
+ // update called with null row since we don't have it yet
254
+ const insertPass = insertRuleFn
255
+ ? await evalWithTimeout(() => insertRuleFn(auth), WORKER_RULE_TIMEOUT_MS)
256
+ : !config.release;
257
+ const updatePass = updateRuleFn
258
+ ? await evalWithTimeout(() => updateRuleFn(auth, {}), WORKER_RULE_TIMEOUT_MS) // null-safe: {} simulates missing row
259
+ : !config.release;
260
+
261
+ if (!insertPass && !updatePass) {
262
+ throw new EdgeBaseError(403, 'Access denied by access rules.');
263
+ }
264
+ return next();
265
+ }
266
+
267
+ // Handle batch (§16)
268
+ if (path.includes('/batch')) {
269
+ if (httpMethod === 'POST') {
270
+ const insertRuleFn = normalizeRule(tableRules.insert);
271
+ if (path.includes('/batch-by-filter')) {
272
+ // batch-by-filter: row-level evaluation happens in DO
273
+ return next();
274
+ }
275
+ if (insertRuleFn) {
276
+ const canInsert = await evalWithTimeout(() => insertRuleFn(auth), WORKER_RULE_TIMEOUT_MS);
277
+ if (!canInsert) throw new EdgeBaseError(403, 'Access denied by access rules.');
278
+ } else if (config.release) {
279
+ throw new EdgeBaseError(403, 'Access denied by access rules.');
280
+ }
281
+ }
282
+ return next();
283
+ }
284
+
285
+ if (!action) return next();
286
+
287
+ // ── Step 5: insert — evaluated at Worker level (no row needed) ──
288
+ if (action === 'insert') {
289
+ const insertRuleFn = normalizeRule(tableRules.insert);
290
+ if (!insertRuleFn) {
291
+ if (!config.release) return next();
292
+ throw new EdgeBaseError(403, `Access denied. No 'insert' rule defined for '${tableName}'.`);
293
+ }
294
+ const canInsert = await evalWithTimeout(() => insertRuleFn(auth), WORKER_RULE_TIMEOUT_MS);
295
+ if (!canInsert) throw new EdgeBaseError(403, 'Access denied by access rules.');
296
+ return next();
297
+ }
298
+
299
+ // ── Step 6: read/update/delete — pass to DO for row-level evaluation ──
300
+ // DO (database-do.ts) handles: read(auth, row), update(auth, row), delete(auth, row).
301
+ // All-or-Nothing policy for reads: any row failing → full 403 — §7.
302
+ // NOTE: Do NOT call c.req.raw.headers.set() here — Request.headers is immutable in
303
+ // Cloudflare Workers runtime (TypeError). tables.ts copies headers via new Headers(...)
304
+ // and sets X-DO-Name. auth is already forwarded via X-Auth-Context header in tables.ts.
305
+ // namespace/dbId are parsed from the URL by the DO directly.
306
+
307
+ return next();
308
+ }
309
+
310
+ // ─── Helpers ───
311
+
312
+ function getAction(method: string, path: string, hasUpsert: boolean): string | null {
313
+ if (path.includes('/search')) return 'search';
314
+ if (path.includes('/count')) return 'list';
315
+ if (path.includes('/batch')) return null;
316
+
317
+ switch (method) {
318
+ case 'GET':
319
+ return isIdPath(path) ? 'get' : 'list';
320
+ case 'POST':
321
+ if (hasUpsert) return null;
322
+ return 'insert';
323
+ case 'PATCH':
324
+ case 'PUT':
325
+ return 'update';
326
+ case 'DELETE':
327
+ return 'delete';
328
+ default:
329
+ return null;
330
+ }
331
+ }
332
+
333
+ function isIdPath(path: string): boolean {
334
+ // /api/db/:ns/tables/:name/:id or /api/db/:ns/:iid/tables/:name/:id
335
+ const parts = path.split('/').filter(Boolean);
336
+ const tabIdx = parts.indexOf('tables');
337
+ if (parts[1] !== 'db' || tabIdx === -1) return false;
338
+ // id segment exists after tableName (parts[tabIdx+2])
339
+ return parts.length > tabIdx + 2;
340
+ }
341
+
342
+ /**
343
+ * For 'user' namespace, auto-extract id from auth.id.
344
+ * For other namespaces, returns undefined (id must come from X-EdgeBase-DB-Id header).
345
+ */
346
+ function extractAuthIdForUserNamespace(namespace: string, auth: AuthContext | null): string | undefined {
347
+ if (namespace === 'user' && auth?.id) {
348
+ return auth.id;
349
+ }
350
+ return undefined;
351
+ }
352
+
353
+ /**
354
+ * Build a read-only DbRuleCtx for DB-level access() evaluation at Worker level.
355
+ * Resolves table names through the configured provider and performs local internal reads.
356
+ */
357
+ function buildDbRuleCtx(
358
+ c: HonoContext,
359
+ config: ReturnType<typeof parseConfig>,
360
+ currentNamespace: string,
361
+ currentDbId: string | undefined,
362
+ ): import('@edge-base/shared').DbRuleCtx {
363
+ return {
364
+ db: {
365
+ async get(table, id) {
366
+ const namespace = findTableNamespace(table, config) ?? currentNamespace;
367
+ if (!namespace || !id) return null;
368
+
369
+ const path = `/tables/${table}/${id}`;
370
+ const response = await executeInternalDbRead(
371
+ c,
372
+ config,
373
+ namespace,
374
+ table,
375
+ path,
376
+ currentNamespace,
377
+ currentDbId,
378
+ );
379
+ if (response.status === 404) return null;
380
+ if (!response.ok) throw new Error(`DB rule ctx get failed: ${response.status}`);
381
+ return await response.json() as Record<string, unknown>;
382
+ },
383
+ async exists(table, filter) {
384
+ const namespace = findTableNamespace(table, config) ?? currentNamespace;
385
+ if (!namespace) return false;
386
+
387
+ const query = new URLSearchParams();
388
+ query.set('limit', '1');
389
+ query.set(
390
+ 'filter',
391
+ JSON.stringify(
392
+ Object.entries(filter).map(([field, value]) => [field, '==', value]),
393
+ ),
394
+ );
395
+ const response = await executeInternalDbRead(
396
+ c,
397
+ config,
398
+ namespace,
399
+ table,
400
+ `/tables/${table}?${query.toString()}`,
401
+ currentNamespace,
402
+ currentDbId,
403
+ );
404
+ if (!response.ok) throw new Error(`DB rule ctx exists failed: ${response.status}`);
405
+ const result = await response.json() as { items?: Array<Record<string, unknown>> };
406
+ return (result.items?.length ?? 0) > 0;
407
+ },
408
+ },
409
+ };
410
+ }
411
+
412
+ async function executeInternalDbRead(
413
+ c: HonoContext,
414
+ config: ReturnType<typeof parseConfig>,
415
+ namespace: string,
416
+ tableName: string,
417
+ path: string,
418
+ currentNamespace: string,
419
+ currentDbId: string | undefined,
420
+ ): Promise<Response> {
421
+ const dbBlock = config.databases?.[namespace];
422
+ const isDynamic = !!(dbBlock?.instance || dbBlock?.access?.canCreate || dbBlock?.access?.access);
423
+
424
+ if (isDynamic && (!currentDbId || namespace !== currentNamespace)) {
425
+ throw new Error(
426
+ `DbRuleCtx cannot resolve dynamic namespace table '${tableName}' outside the current db instance.`,
427
+ );
428
+ }
429
+
430
+ const request = new Request(`http://internal/api/db/${namespace}${path}`, {
431
+ method: 'GET',
432
+ headers: {
433
+ 'Content-Type': 'application/json',
434
+ 'X-EdgeBase-Internal': 'true',
435
+ 'X-Is-Service-Key': 'true',
436
+ },
437
+ });
438
+
439
+ if (shouldRouteToD1(namespace, config)) {
440
+ return handleD1Request(
441
+ buildInternalHandlerContext({
442
+ env: c.env,
443
+ request,
444
+ executionCtx: c.executionCtx,
445
+ }),
446
+ namespace,
447
+ tableName,
448
+ path,
449
+ );
450
+ }
451
+
452
+ const provider = config.databases?.[namespace]?.provider;
453
+ if (provider === 'neon' || provider === 'postgres') {
454
+ return handlePgRequest(
455
+ buildInternalHandlerContext({
456
+ env: c.env,
457
+ request,
458
+ executionCtx: c.executionCtx,
459
+ }),
460
+ namespace,
461
+ tableName,
462
+ path,
463
+ );
464
+ }
465
+
466
+ const doName = getDbDoName(namespace, isDynamic ? currentDbId : undefined);
467
+ return callDO(c.env.DATABASE, doName, path, {
468
+ method: 'GET',
469
+ headers: {
470
+ 'X-DO-Name': doName,
471
+ 'X-EdgeBase-Internal': 'true',
472
+ 'X-Is-Service-Key': 'true',
473
+ },
474
+ });
475
+ }