@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,126 @@
1
+ /**
2
+ * Request logging middleware.
3
+ *
4
+ * Records method, path, status, duration, userId, and enriched analytics
5
+ * fields (category, subcategory, target, operation, region, sizes) for
6
+ * every request. Uses LogWriter adapter for environment-aware storage.
7
+ */
8
+ import type { MiddlewareHandler } from 'hono';
9
+ import type { Env } from '../types.js';
10
+ import { createLogWriter } from '../lib/log-writer.js';
11
+ import { parseRoute } from '../lib/route-parser.js';
12
+
13
+ type HonoEnv = { Bindings: Env };
14
+ type LogFieldOverrides = Partial<{
15
+ category: string;
16
+ subcategory: string;
17
+ target1: string;
18
+ target2: string;
19
+ operation: string;
20
+ region: string;
21
+ requestSize: number;
22
+ responseSize: number;
23
+ resultCount: number;
24
+ userId: string;
25
+ }>;
26
+
27
+ function resolveRequestPath(c: {
28
+ req: {
29
+ path?: string;
30
+ url: string;
31
+ };
32
+ }): string {
33
+ if (typeof c.req.path === 'string' && c.req.path.length > 0) {
34
+ return c.req.path;
35
+ }
36
+
37
+ try {
38
+ return new URL(c.req.url, 'http://edgebase.local').pathname;
39
+ } catch {
40
+ return '/';
41
+ }
42
+ }
43
+
44
+ export const loggerMiddleware: MiddlewareHandler<HonoEnv> = async (c, next) => {
45
+ const start = Date.now();
46
+
47
+ // Pre-compute route classification before response
48
+ const path = resolveRequestPath(c);
49
+ const method = c.req.method;
50
+ const route = parseRoute(method, path);
51
+
52
+ // Capture request size from Content-Length header
53
+ const requestSize = parseInt(c.req.header('content-length') || '0', 10) || 0;
54
+
55
+ await next();
56
+
57
+ const duration = Date.now() - start;
58
+ const auth = c.get('auth' as never) as { id: string } | null | undefined;
59
+
60
+ // Extract region from Cloudflare cf object or cf-ray header
61
+ let region = '';
62
+ try {
63
+ const cf = (c.req.raw as unknown as { cf?: { colo?: string } }).cf;
64
+ if (cf?.colo) {
65
+ region = cf.colo;
66
+ }
67
+ } catch {
68
+ // cf object not available (non-CF environment)
69
+ }
70
+ if (!region) {
71
+ // Fallback: extract datacenter code from CF-Ray header (e.g., "abc123-ICN" → "ICN")
72
+ const cfRay = c.req.header('cf-ray');
73
+ if (cfRay) {
74
+ const parts = cfRay.split('-');
75
+ if (parts.length >= 2) {
76
+ region = parts[parts.length - 1];
77
+ }
78
+ }
79
+ }
80
+
81
+ // Capture response size from Content-Length header
82
+ const responseSize = parseInt(c.res.headers.get('content-length') || '0', 10) || 0;
83
+ const overrides = (c.get('logFields' as never) ?? {}) as LogFieldOverrides;
84
+
85
+ // Get execution context for non-blocking writes
86
+ let executionCtx: { waitUntil: (promise: Promise<unknown>) => void } | undefined;
87
+ try {
88
+ executionCtx = c.executionCtx;
89
+ } catch {
90
+ // No execution context (unit tests)
91
+ }
92
+
93
+ const logger = createLogWriter(
94
+ (c.env || {}) as unknown as Record<string, unknown>,
95
+ executionCtx,
96
+ );
97
+
98
+ const entry = {
99
+ method,
100
+ path,
101
+ status: c.res.status,
102
+ duration,
103
+ userId: overrides.userId ?? auth?.id,
104
+ timestamp: Date.now(),
105
+ // Enriched analytics fields
106
+ category: overrides.category ?? route.category,
107
+ subcategory: overrides.subcategory ?? route.subcategory,
108
+ target1: overrides.target1 ?? route.target1,
109
+ target2: overrides.target2 ?? route.target2,
110
+ operation: overrides.operation ?? route.operation,
111
+ region: overrides.region ?? region,
112
+ requestSize: overrides.requestSize ?? requestSize,
113
+ responseSize: overrides.responseSize ?? responseSize,
114
+ resultCount: overrides.resultCount ?? 0,
115
+ };
116
+
117
+ // Fire-and-forget — don't block response
118
+ if (executionCtx) {
119
+ executionCtx.waitUntil(
120
+ Promise.resolve().then(() => logger.write(entry)),
121
+ );
122
+ } else {
123
+ // Unit test environment or no execution context — write synchronously
124
+ logger.write(entry);
125
+ }
126
+ };
@@ -0,0 +1,283 @@
1
+ import type { MiddlewareHandler } from 'hono';
2
+ import type { Env } from '../types.js';
3
+ import type { EdgeBaseConfig } from '@edge-base/shared';
4
+ import {
5
+ buildKeymap,
6
+ extractBearerToken,
7
+ extractServiceKeyHeader,
8
+ validateConfiguredKey,
9
+ type ConstraintContext,
10
+ } from '../lib/service-key.js';
11
+ import { parseConfig } from '../lib/do-router.js';
12
+ import { getTrustedClientIp } from '../lib/client-ip.js';
13
+
14
+ type HonoEnv = { Bindings: Env };
15
+
16
+ /**
17
+ * Rate Limiting middleware — 2-layer architecture.
18
+ *
19
+ * Layer 1: Software counter (per-isolate FixedWindowCounter)
20
+ * - Reads limits from the bundled runtime config (user-configurable)
21
+ * - Falls back to sensible defaults if config is not set
22
+ *
23
+ * Layer 2: Cloudflare Rate Limiting Binding (ceiling safety net)
24
+ * - All Bindings set to 10,000,000/60s in wrangler.toml
25
+ * - Catches cases where isolate restarts reset software counters
26
+ * - Miniflare emulates in all environments (Edge, dev, self-hosting)
27
+ *
28
+ * Groups handled here:
29
+ * - `global` — all routes (last-resort safety net)
30
+ * - `db` — /api/db/* table CRUD
31
+ * - `storage` — /api/storage/*
32
+ * - `functions` — /api/functions/*
33
+ *
34
+ * Auth-specific groups (auth, authSignin, authSignup) are applied
35
+ * directly in auth routes using the exported counter and helpers.
36
+ *
37
+ * Valid Service Key requests bypass app-level rate limits entirely.
38
+ */
39
+
40
+ // ─── Defaults (used when config.rateLimiting is not set) ───
41
+
42
+ export const RATE_LIMIT_DEFAULTS: Record<string, { requests: number; windowSec: number }> = {
43
+ global: { requests: 10_000_000, windowSec: 60 },
44
+ db: { requests: 100, windowSec: 60 },
45
+ storage: { requests: 50, windowSec: 60 },
46
+ functions: { requests: 50, windowSec: 60 },
47
+ auth: { requests: 30, windowSec: 60 },
48
+ authSignin: { requests: 10, windowSec: 60 },
49
+ authSignup: { requests: 10, windowSec: 60 },
50
+ events: { requests: 100, windowSec: 60 },
51
+ };
52
+
53
+ // ─── Window parser ───
54
+
55
+ /** Parse window string ('60s', '5m', '1h') or number (seconds) to seconds */
56
+ export function parseWindow(window: string | number): number {
57
+ if (typeof window === 'number') return window > 0 ? window : 60;
58
+ const match = window.match(/^(\d+)(s|m|h)$/);
59
+ if (!match) return 60; // fallback
60
+ const value = parseInt(match[1], 10);
61
+ switch (match[2]) {
62
+ case 's': return value;
63
+ case 'm': return value * 60;
64
+ case 'h': return value * 3600;
65
+ default: return 60;
66
+ }
67
+ }
68
+
69
+ // ─── Fixed Window Counter (per-isolate memory) ───
70
+
71
+ interface Bucket {
72
+ count: number;
73
+ resetAt: number;
74
+ }
75
+
76
+ /**
77
+ * Per-isolate in-memory Fixed Window Counter.
78
+ * Provides config-driven rate limiting with automatic expiry cleanup.
79
+ *
80
+ * Accuracy:
81
+ * - Self-hosting (single process): exact
82
+ * - Cloudflare Edge (multiple isolates): approximate (each isolate has own counter)
83
+ * - Binding ceiling provides absolute safety regardless of counter accuracy
84
+ */
85
+ export class FixedWindowCounter {
86
+ private buckets = new Map<string, Bucket>();
87
+ private lastCleanup = Date.now();
88
+ private static readonly CLEANUP_INTERVAL = 120_000; // 2 minutes
89
+
90
+ /**
91
+ * Check and increment counter. Returns true if within limit.
92
+ * @param key Unique key (e.g., 'db:1.2.3.4')
93
+ * @param limit Max requests per window
94
+ * @param windowSec Window size in seconds
95
+ */
96
+ check(key: string, limit: number, windowSec: number): boolean {
97
+ const now = Date.now();
98
+ this.maybeCleanup(now);
99
+
100
+ // limit=0 means "always blocked" (ban-mode) — never allow any request
101
+ if (limit <= 0) return false;
102
+
103
+ const windowMs = windowSec * 1000;
104
+ const bucket = this.buckets.get(key);
105
+
106
+ if (!bucket || now >= bucket.resetAt) {
107
+ this.buckets.set(key, { count: 1, resetAt: now + windowMs });
108
+ return true;
109
+ }
110
+
111
+ if (bucket.count >= limit) {
112
+ return false;
113
+ }
114
+
115
+ bucket.count++;
116
+ return true;
117
+ }
118
+
119
+ /** Get remaining seconds until reset for a key (for Retry-After header).
120
+ * Returns 0 if key has never been seen — no active rate-limit window exists. */
121
+ getRetryAfter(key: string): number {
122
+ const bucket = this.buckets.get(key);
123
+ if (!bucket) return 0;
124
+ return Math.max(1, Math.ceil((bucket.resetAt - Date.now()) / 1000));
125
+ }
126
+
127
+ private maybeCleanup(now: number): void {
128
+ if (now - this.lastCleanup < FixedWindowCounter.CLEANUP_INTERVAL) return;
129
+ this.lastCleanup = now;
130
+ for (const [key, bucket] of this.buckets) {
131
+ if (now >= bucket.resetAt) this.buckets.delete(key);
132
+ }
133
+ }
134
+ }
135
+
136
+ // ─── Singleton counter (shared within isolate) ───
137
+
138
+ export const counter = new FixedWindowCounter();
139
+
140
+ // ─── Helpers ───
141
+
142
+ /** Get config-based limit for a group, with fallback to defaults */
143
+ export function getLimit(
144
+ config: EdgeBaseConfig | undefined,
145
+ group: string,
146
+ ): { requests: number; windowSec: number } {
147
+ const rl = config?.rateLimiting;
148
+ if (rl) {
149
+ const configGroup = rl[group as keyof typeof rl];
150
+ if (configGroup?.requests != null && configGroup?.window) {
151
+ return {
152
+ requests: configGroup.requests,
153
+ windowSec: parseWindow(configGroup.window),
154
+ };
155
+ }
156
+ }
157
+ return RATE_LIMIT_DEFAULTS[group] ?? { requests: 10_000_000, windowSec: 60 };
158
+ }
159
+
160
+ /** Map group name to the corresponding env binding */
161
+ function getBinding(env: Env, group: string): RateLimit | undefined {
162
+ if (!env) return undefined;
163
+ switch (group) {
164
+ case 'global': return env.GLOBAL_RATE_LIMITER;
165
+ case 'db': return env.DB_RATE_LIMITER;
166
+ case 'storage': return env.STORAGE_RATE_LIMITER;
167
+ case 'functions': return env.FUNCTIONS_RATE_LIMITER;
168
+ case 'events': return env.EVENTS_RATE_LIMITER;
169
+ default: return undefined;
170
+ }
171
+ }
172
+
173
+ /** Determine the rate limit group for a request path */
174
+ export function getGroup(path: string): string {
175
+ if (path.startsWith('/api/db/')) {
176
+ // Database-live endpoints live under /api/db/ but are not database CRUD operations
177
+ if (path === '/api/db/subscribe' || path === '/api/db/connect-check' || path === '/api/db/broadcast') {
178
+ return 'global';
179
+ }
180
+ return 'db';
181
+ }
182
+ if (path.startsWith('/api/storage/')) return 'storage';
183
+ if (path.startsWith('/api/functions/')) return 'functions';
184
+ if (path.startsWith('/api/analytics/track')) return 'events';
185
+ return 'global';
186
+ }
187
+
188
+ /**
189
+ * Rate Limiting middleware — 2-layer architecture.
190
+ *
191
+ * 1. Software counter: config-driven (runtime config)
192
+ * 2. Binding: ceiling safety net (wrangler.toml, 10M/60s)
193
+ *
194
+ * Auth routes are included in global group.
195
+ * Valid Service Key requests bypass app-level rate limits.
196
+ * Identifier: always IP-based (auth middleware runs after rate limit).
197
+ */
198
+ export const rateLimitMiddleware: MiddlewareHandler<HonoEnv> = async (c, next) => {
199
+ const path = new URL(c.req.url).pathname;
200
+ const group = getGroup(path);
201
+
202
+ // ── Determine identifier — always IP ──
203
+ // Security: CF-Connecting-IP is set by Cloudflare and cannot be spoofed by clients.
204
+ // X-Forwarded-For is only used as a fallback for self-hosted environments and
205
+ // MUST be set by a trusted reverse proxy (Nginx/Caddy). If EdgeBase is exposed
206
+ // directly without a proxy, clients can forge this header to bypass rate limits.
207
+ const ip = getTrustedClientIp(c.env, c.req) ?? 'unknown';
208
+
209
+ // ── Service Key check ──
210
+ const serviceKeyHeader = extractServiceKeyHeader(c.req) ?? extractBearerToken(c.req) ?? undefined;
211
+ let isServiceKey = false;
212
+ if (serviceKeyHeader) {
213
+ const config = c.env ? parseConfig(c.env) : {};
214
+ const constraintCtx: ConstraintContext = {
215
+ env: c.env?.ENVIRONMENT,
216
+ ip: ip !== 'unknown' ? ip : undefined,
217
+ };
218
+ const keymap = c.env ? buildKeymap(config, c.env as never) : null;
219
+ isServiceKey = validateConfiguredKey(serviceKeyHeader, keymap, constraintCtx) === 'valid';
220
+ }
221
+
222
+ if (isServiceKey) {
223
+ await next();
224
+ return;
225
+ }
226
+
227
+ // ── Parse config ──
228
+ const config = c.env ? parseConfig(c.env) : undefined;
229
+
230
+ // ── Layer 1: Software counter (config-driven) ──
231
+ const { requests, windowSec } = getLimit(config, group);
232
+ const counterKey = `${group}:${ip}`;
233
+
234
+ if (!counter.check(counterKey, requests, windowSec)) {
235
+ c.header('Retry-After', String(counter.getRetryAfter(counterKey)));
236
+ return c.json(
237
+ { code: 429, message: 'Too many requests. Please try again later.' },
238
+ 429,
239
+ );
240
+ }
241
+
242
+ // ── Layer 2: Binding ceiling ──
243
+ const limiter = getBinding(c.env, group);
244
+ if (limiter) {
245
+ const { success } = await limiter.limit({ key: ip });
246
+ if (!success) {
247
+ c.header('Retry-After', '60');
248
+ return c.json(
249
+ { code: 429, message: 'Too many requests. Please try again later.' },
250
+ 429,
251
+ );
252
+ }
253
+ }
254
+
255
+ // ── Also check global for non-global groups ──
256
+ if (group !== 'global') {
257
+ // Software counter for global
258
+ const globalLimit = getLimit(config, 'global');
259
+ const globalKey = `global:${ip}`;
260
+ if (!counter.check(globalKey, globalLimit.requests, globalLimit.windowSec)) {
261
+ c.header('Retry-After', String(counter.getRetryAfter(globalKey)));
262
+ return c.json(
263
+ { code: 429, message: 'Too many requests. Please try again later.' },
264
+ 429,
265
+ );
266
+ }
267
+
268
+ // Binding ceiling for global
269
+ const globalLimiter = getBinding(c.env, 'global');
270
+ if (globalLimiter) {
271
+ const { success } = await globalLimiter.limit({ key: ip });
272
+ if (!success) {
273
+ c.header('Retry-After', '60');
274
+ return c.json(
275
+ { code: 429, message: 'Too many requests. Please try again later.' },
276
+ 429,
277
+ );
278
+ }
279
+ }
280
+ }
281
+
282
+ await next();
283
+ };