@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.
- package/admin-build/.gitkeep +0 -0
- package/admin-build/_app/env.js +1 -0
- package/admin-build/_app/immutable/assets/0.Bm6cF078.css +1 -0
- package/admin-build/_app/immutable/assets/1.BfW3pUNa.css +1 -0
- package/admin-build/_app/immutable/assets/11.CVmQOewb.css +1 -0
- package/admin-build/_app/immutable/assets/12.B1EhbRZT.css +1 -0
- package/admin-build/_app/immutable/assets/13.BvwYeuwE.css +1 -0
- package/admin-build/_app/immutable/assets/14.CdVfcO0R.css +1 -0
- package/admin-build/_app/immutable/assets/15.2yeZ66b-.css +1 -0
- package/admin-build/_app/immutable/assets/17.BVg0JEVu.css +1 -0
- package/admin-build/_app/immutable/assets/18.Rwnl3x_i.css +1 -0
- package/admin-build/_app/immutable/assets/20.DsPWA9AV.css +1 -0
- package/admin-build/_app/immutable/assets/21.Dz2RJ56c.css +1 -0
- package/admin-build/_app/immutable/assets/22.DwNLk5Ai.css +1 -0
- package/admin-build/_app/immutable/assets/23.CFpu0gOO.css +1 -0
- package/admin-build/_app/immutable/assets/24.Cy5LBeoJ.css +1 -0
- package/admin-build/_app/immutable/assets/25.pUyLVf-h.css +1 -0
- package/admin-build/_app/immutable/assets/26.DBcGrlXa.css +1 -0
- package/admin-build/_app/immutable/assets/27.BswYyAJD.css +1 -0
- package/admin-build/_app/immutable/assets/28.B4ueB1Kf.css +1 -0
- package/admin-build/_app/immutable/assets/29.B-qU6PdF.css +1 -0
- package/admin-build/_app/immutable/assets/3.Dg81Pgmd.css +1 -0
- package/admin-build/_app/immutable/assets/30.CsdWum94.css +1 -0
- package/admin-build/_app/immutable/assets/31.U6OwIp50.css +1 -0
- package/admin-build/_app/immutable/assets/4.CyawCCux.css +1 -0
- package/admin-build/_app/immutable/assets/5.C0YO2HTk.css +1 -0
- package/admin-build/_app/immutable/assets/8.Br5jd6kD.css +1 -0
- package/admin-build/_app/immutable/assets/Badge.EMYLHBxE.css +1 -0
- package/admin-build/_app/immutable/assets/Button.DpzMRTjK.css +1 -0
- package/admin-build/_app/immutable/assets/ConfirmDialog.DAnaWRRk.css +1 -0
- package/admin-build/_app/immutable/assets/EmptyState.CwKsu57Y.css +1 -0
- package/admin-build/_app/immutable/assets/Input.BDUSenmU.css +1 -0
- package/admin-build/_app/immutable/assets/Modal.Dm5B0Xie.css +1 -0
- package/admin-build/_app/immutable/assets/PageShell.CmU-Xh-b.css +1 -0
- package/admin-build/_app/immutable/assets/SchemaFieldEditor.g4NsCdno.css +1 -0
- package/admin-build/_app/immutable/assets/Select.BW4Keufm.css +1 -0
- package/admin-build/_app/immutable/assets/Skeleton.KWUulTKJ.css +1 -0
- package/admin-build/_app/immutable/assets/Tabs.CniGYb67.css +1 -0
- package/admin-build/_app/immutable/assets/TimeChart.BTCDAvmT.css +1 -0
- package/admin-build/_app/immutable/assets/Toggle.Cy_K12OM.css +1 -0
- package/admin-build/_app/immutable/assets/TopList.ClFzmPlA.css +1 -0
- package/admin-build/_app/immutable/chunks/7B47DvSx.js +1 -0
- package/admin-build/_app/immutable/chunks/7f08Id8e.js +1 -0
- package/admin-build/_app/immutable/chunks/8wJeQ7LN.js +1 -0
- package/admin-build/_app/immutable/chunks/B-h2afW5.js +1 -0
- package/admin-build/_app/immutable/chunks/B8vJP3wz.js +1 -0
- package/admin-build/_app/immutable/chunks/BR_fL5Yv.js +1 -0
- package/admin-build/_app/immutable/chunks/BY92tFS2.js +1 -0
- package/admin-build/_app/immutable/chunks/BcR-Rdj9.js +1 -0
- package/admin-build/_app/immutable/chunks/BdrwyZv8.js +1 -0
- package/admin-build/_app/immutable/chunks/Bh56EfQ_.js +1 -0
- package/admin-build/_app/immutable/chunks/BkrCkgYp.js +1 -0
- package/admin-build/_app/immutable/chunks/BmRjiP5k.js +1 -0
- package/admin-build/_app/immutable/chunks/BsokvhWC.js +1 -0
- package/admin-build/_app/immutable/chunks/C4D51vTW.js +1 -0
- package/admin-build/_app/immutable/chunks/C6puvcoR.js +2 -0
- package/admin-build/_app/immutable/chunks/CCKNu7m7.js +1 -0
- package/admin-build/_app/immutable/chunks/CWj6FrbW.js +1 -0
- package/admin-build/_app/immutable/chunks/Ce-ngf4p.js +5 -0
- package/admin-build/_app/immutable/chunks/Cs0GwzJA.js +1 -0
- package/admin-build/_app/immutable/chunks/CwROoZK0.js +1 -0
- package/admin-build/_app/immutable/chunks/CxCPv_Ut.js +1 -0
- package/admin-build/_app/immutable/chunks/CxbRue-5.js +1 -0
- package/admin-build/_app/immutable/chunks/CyqB6g-D.js +1 -0
- package/admin-build/_app/immutable/chunks/D5h5A1cc.js +2 -0
- package/admin-build/_app/immutable/chunks/DnyL7Zq-.js +1 -0
- package/admin-build/_app/immutable/chunks/DoPXzH7F.js +1 -0
- package/admin-build/_app/immutable/chunks/DrQSgw-f.js +1 -0
- package/admin-build/_app/immutable/chunks/DttM2zNO.js +1 -0
- package/admin-build/_app/immutable/chunks/DuXuUBWN.js +1 -0
- package/admin-build/_app/immutable/chunks/MdeqaOQx.js +10 -0
- package/admin-build/_app/immutable/chunks/NuUjtcO2.js +1 -0
- package/admin-build/_app/immutable/chunks/Q2nPFxS6.js +1 -0
- package/admin-build/_app/immutable/chunks/R6arueIl.js +1 -0
- package/admin-build/_app/immutable/chunks/UUazaC_N.js +1 -0
- package/admin-build/_app/immutable/chunks/cOYbrQxx.js +1 -0
- package/admin-build/_app/immutable/chunks/eFQHTGwA.js +1 -0
- package/admin-build/_app/immutable/chunks/ehbppgYb.js +1 -0
- package/admin-build/_app/immutable/chunks/glwixJlP.js +1 -0
- package/admin-build/_app/immutable/chunks/vApWTCBs.js +1 -0
- package/admin-build/_app/immutable/chunks/w89G9Xpi.js +1 -0
- package/admin-build/_app/immutable/chunks/wJsUhbfZ.js +1 -0
- package/admin-build/_app/immutable/chunks/zfauFM8P.js +1 -0
- package/admin-build/_app/immutable/entry/app.CcO-Uos3.js +2 -0
- package/admin-build/_app/immutable/entry/start.COebYq3I.js +1 -0
- package/admin-build/_app/immutable/nodes/0.CjtHKU-6.js +1 -0
- package/admin-build/_app/immutable/nodes/1.DEisjlM0.js +1 -0
- package/admin-build/_app/immutable/nodes/10.CvhdyWVB.js +1 -0
- package/admin-build/_app/immutable/nodes/11.DjHqcOvy.js +1 -0
- package/admin-build/_app/immutable/nodes/12.mQLz4Mj_.js +1 -0
- package/admin-build/_app/immutable/nodes/13.CBonZZyP.js +110 -0
- package/admin-build/_app/immutable/nodes/14.d-oiZL0j.js +3 -0
- package/admin-build/_app/immutable/nodes/15.CKPQsUYF.js +1 -0
- package/admin-build/_app/immutable/nodes/16.wPzAPQGx.js +1 -0
- package/admin-build/_app/immutable/nodes/17.DayhKyEZ.js +1 -0
- package/admin-build/_app/immutable/nodes/18.DKwS0Ir0.js +1 -0
- package/admin-build/_app/immutable/nodes/19.wPzAPQGx.js +1 -0
- package/admin-build/_app/immutable/nodes/2.BKoKrw1i.js +1 -0
- package/admin-build/_app/immutable/nodes/20.BvIkkkrW.js +1 -0
- package/admin-build/_app/immutable/nodes/21.DMaFhdHk.js +128 -0
- package/admin-build/_app/immutable/nodes/22.3xdgwuK1.js +1 -0
- package/admin-build/_app/immutable/nodes/23.8Bvgjbsl.js +112 -0
- package/admin-build/_app/immutable/nodes/24.DzSSzRhG.js +2 -0
- package/admin-build/_app/immutable/nodes/25.9KKYBnAE.js +2 -0
- package/admin-build/_app/immutable/nodes/26.Bhn9dfhY.js +1 -0
- package/admin-build/_app/immutable/nodes/27.kRLiC24G.js +1 -0
- package/admin-build/_app/immutable/nodes/28.BVIN1-7N.js +1 -0
- package/admin-build/_app/immutable/nodes/29.3yabZWj4.js +1 -0
- package/admin-build/_app/immutable/nodes/3.BFtSOkX7.js +2 -0
- package/admin-build/_app/immutable/nodes/30.CyCQlwaP.js +1 -0
- package/admin-build/_app/immutable/nodes/31.C4LDXjES.js +1 -0
- package/admin-build/_app/immutable/nodes/4.CvbiMlCa.js +1 -0
- package/admin-build/_app/immutable/nodes/5.C6BLv2eM.js +1 -0
- package/admin-build/_app/immutable/nodes/6.BcXvfl2P.js +1 -0
- package/admin-build/_app/immutable/nodes/7.CIuqhPiK.js +1 -0
- package/admin-build/_app/immutable/nodes/8.BQOR_JfO.js +1 -0
- package/admin-build/_app/immutable/nodes/9.NZqXQxPy.js +1 -0
- package/admin-build/_app/version.json +1 -0
- package/admin-build/favicon.svg +26 -0
- package/admin-build/index.html +45 -0
- package/openapi.json +19543 -0
- package/package.json +66 -0
- package/src/__tests__/admin-assets.test.ts +55 -0
- package/src/__tests__/admin-data-routes.test.ts +488 -0
- package/src/__tests__/admin-db-target.test.ts +103 -0
- package/src/__tests__/admin-routing.test.ts +31 -0
- package/src/__tests__/admin-user-management.test.ts +311 -0
- package/src/__tests__/analytics-query.test.ts +75 -0
- package/src/__tests__/auth-d1.test.ts +749 -0
- package/src/__tests__/auth-db-adapter.test.ts +73 -0
- package/src/__tests__/auth-jwt.test.ts +440 -0
- package/src/__tests__/auth-oauth.test.ts +389 -0
- package/src/__tests__/auth-password.test.ts +367 -0
- package/src/__tests__/auth-redirect.test.ts +87 -0
- package/src/__tests__/backup-restore.test.ts +711 -0
- package/src/__tests__/broadcast.test.ts +128 -0
- package/src/__tests__/cli.test.ts +178 -0
- package/src/__tests__/cloudflare-realtime.test.ts +113 -0
- package/src/__tests__/config.test.ts +469 -0
- package/src/__tests__/cors.test.ts +154 -0
- package/src/__tests__/cron.test.ts +302 -0
- package/src/__tests__/d1-handler.test.ts +402 -0
- package/src/__tests__/d1-sql.test.ts +120 -0
- package/src/__tests__/database-live-config.test.ts +42 -0
- package/src/__tests__/database-live-emitter.test.ts +56 -0
- package/src/__tests__/database-live-filters.test.ts +63 -0
- package/src/__tests__/database-live-route.test.ts +113 -0
- package/src/__tests__/db-sql.test.ts +163 -0
- package/src/__tests__/do-lifecycle.test.ts +263 -0
- package/src/__tests__/do-router.test.ts +729 -0
- package/src/__tests__/email-provider.test.ts +128 -0
- package/src/__tests__/email-templates.test.ts +528 -0
- package/src/__tests__/error-format.test.ts +250 -0
- package/src/__tests__/field-ops.test.ts +242 -0
- package/src/__tests__/functions-context.test.ts +334 -0
- package/src/__tests__/functions-d1-proxy.test.ts +229 -0
- package/src/__tests__/functions-registry-runtime-config.test.ts +17 -0
- package/src/__tests__/functions-route.test.ts +139 -0
- package/src/__tests__/internal-request.test.ts +77 -0
- package/src/__tests__/log-writer.test.ts +44 -0
- package/src/__tests__/logger.test.ts +58 -0
- package/src/__tests__/meta-admin-proxy.test.ts +48 -0
- package/src/__tests__/meta-export-coverage.test.ts +191 -0
- package/src/__tests__/meta-route-registration.test.ts +47 -0
- package/src/__tests__/namespace-dump.test.ts +28 -0
- package/src/__tests__/oauth-providers.test.ts +337 -0
- package/src/__tests__/openapi-coverage.test.ts +144 -0
- package/src/__tests__/pagination.test.ts +59 -0
- package/src/__tests__/password-policy.test.ts +191 -0
- package/src/__tests__/plugin-migrations.test.ts +379 -0
- package/src/__tests__/postgres-batch-compat.test.ts +133 -0
- package/src/__tests__/postgres-dialect.test.ts +328 -0
- package/src/__tests__/postgres-executor.test.ts +79 -0
- package/src/__tests__/postgres-field-ops-compat.test.ts +222 -0
- package/src/__tests__/postgres-schema-init.test.ts +105 -0
- package/src/__tests__/postgres-table-utils.test.ts +107 -0
- package/src/__tests__/presence.test.ts +199 -0
- package/src/__tests__/provider.test.ts +550 -0
- package/src/__tests__/public-user-profile.test.ts +339 -0
- package/src/__tests__/push-handlers.test.ts +179 -0
- package/src/__tests__/push-provider.test.ts +80 -0
- package/src/__tests__/push-token.test.ts +418 -0
- package/src/__tests__/query.test.ts +771 -0
- package/src/__tests__/rate-limit.test.ts +260 -0
- package/src/__tests__/room-access-policy.test.ts +101 -0
- package/src/__tests__/room-handler-context.test.ts +130 -0
- package/src/__tests__/room-monitoring.test.ts +138 -0
- package/src/__tests__/room-runtime-routing.test.ts +222 -0
- package/src/__tests__/room.test.ts +254 -0
- package/src/__tests__/route-parser.test.ts +490 -0
- package/src/__tests__/rules.test.ts +234 -0
- package/src/__tests__/runtime-surface-accounting.test.ts +120 -0
- package/src/__tests__/scheduled.test.ts +80 -0
- package/src/__tests__/schema.test.ts +1273 -0
- package/src/__tests__/security-hardening.test.ts +312 -0
- package/src/__tests__/server.unit.test.ts +333 -0
- package/src/__tests__/service-key-db-proxy.test.ts +650 -0
- package/src/__tests__/service-key-provider-bypass.test.ts +138 -0
- package/src/__tests__/service-key.test.ts +757 -0
- package/src/__tests__/smoke-skip-report.test.ts +72 -0
- package/src/__tests__/sms-provider.test.ts +39 -0
- package/src/__tests__/sql-route.test.ts +218 -0
- package/src/__tests__/storage-hook-context.test.ts +115 -0
- package/src/__tests__/totp.test.ts +200 -0
- package/src/__tests__/uuid.test.ts +144 -0
- package/src/__tests__/validation.test.ts +773 -0
- package/src/__tests__/websocket-pending.test.ts +163 -0
- package/src/_functions-registry.ts +51 -0
- package/src/bench-entry.ts +9 -0
- package/src/cloudflare-test.d.ts +1 -0
- package/src/durable-objects/auth-do.ts +49 -0
- package/src/durable-objects/database-do.ts +2240 -0
- package/src/durable-objects/database-live-do.ts +949 -0
- package/src/durable-objects/logs-do.ts +1200 -0
- package/src/durable-objects/room-runtime-base.ts +1604 -0
- package/src/durable-objects/rooms-do.ts +2191 -0
- package/src/generated-config.ts +6 -0
- package/src/index.ts +382 -0
- package/src/lib/admin-assets.ts +54 -0
- package/src/lib/admin-db-target.ts +301 -0
- package/src/lib/admin-routing.ts +35 -0
- package/src/lib/admin-user-management.ts +464 -0
- package/src/lib/analytics-adapter.ts +103 -0
- package/src/lib/analytics-query.ts +579 -0
- package/src/lib/auth-d1-service.ts +1193 -0
- package/src/lib/auth-d1.ts +1056 -0
- package/src/lib/auth-db-adapter.ts +289 -0
- package/src/lib/auth-redirect.ts +116 -0
- package/src/lib/cidr.ts +115 -0
- package/src/lib/client-ip.ts +51 -0
- package/src/lib/cloudflare-realtime.ts +251 -0
- package/src/lib/control-db.ts +36 -0
- package/src/lib/cron.ts +163 -0
- package/src/lib/d1-handler.ts +1425 -0
- package/src/lib/d1-schema-init.ts +255 -0
- package/src/lib/d1-sql.ts +33 -0
- package/src/lib/database-live-config.ts +24 -0
- package/src/lib/database-live-emitter.ts +111 -0
- package/src/lib/db-sql.ts +66 -0
- package/src/lib/do-retry.ts +36 -0
- package/src/lib/do-router.ts +270 -0
- package/src/lib/do-sql.ts +73 -0
- package/src/lib/email-provider.ts +379 -0
- package/src/lib/email-templates.ts +285 -0
- package/src/lib/email-translations.ts +422 -0
- package/src/lib/errors.ts +151 -0
- package/src/lib/functions.ts +2091 -0
- package/src/lib/hono.ts +56 -0
- package/src/lib/internal-request.ts +56 -0
- package/src/lib/jwt.ts +354 -0
- package/src/lib/log-writer.ts +272 -0
- package/src/lib/namespace-dump.ts +125 -0
- package/src/lib/oauth-providers.ts +1225 -0
- package/src/lib/op-parser.ts +99 -0
- package/src/lib/openapi.ts +146 -0
- package/src/lib/pagination.ts +19 -0
- package/src/lib/password-policy.ts +102 -0
- package/src/lib/password.ts +145 -0
- package/src/lib/plugin-migrations.ts +612 -0
- package/src/lib/postgres-executor.ts +203 -0
- package/src/lib/postgres-handler.ts +1102 -0
- package/src/lib/postgres-schema-init.ts +341 -0
- package/src/lib/postgres-table-utils.ts +87 -0
- package/src/lib/public-user-profile.ts +187 -0
- package/src/lib/push-provider.ts +409 -0
- package/src/lib/push-token.ts +294 -0
- package/src/lib/query-engine.ts +768 -0
- package/src/lib/room-monitoring.ts +97 -0
- package/src/lib/room-runtime.ts +14 -0
- package/src/lib/route-parser.ts +434 -0
- package/src/lib/schema.ts +538 -0
- package/src/lib/schemas.ts +152 -0
- package/src/lib/service-key.ts +419 -0
- package/src/lib/sms-provider.ts +230 -0
- package/src/lib/startup-config.ts +99 -0
- package/src/lib/totp.ts +242 -0
- package/src/lib/uuid.ts +87 -0
- package/src/lib/validation.ts +205 -0
- package/src/lib/version.ts +2 -0
- package/src/lib/websocket-pending.ts +40 -0
- package/src/middleware/auth.ts +169 -0
- package/src/middleware/captcha-verify.ts +217 -0
- package/src/middleware/cors.ts +159 -0
- package/src/middleware/error-handler.ts +54 -0
- package/src/middleware/internal-guard.ts +26 -0
- package/src/middleware/logger.ts +126 -0
- package/src/middleware/rate-limit.ts +283 -0
- package/src/middleware/rules.ts +475 -0
- package/src/routes/admin-auth.ts +447 -0
- package/src/routes/admin.ts +3501 -0
- package/src/routes/analytics-api.ts +290 -0
- package/src/routes/auth.ts +4222 -0
- package/src/routes/backup.ts +1466 -0
- package/src/routes/config.ts +53 -0
- package/src/routes/d1.ts +109 -0
- package/src/routes/database-live.ts +281 -0
- package/src/routes/functions.ts +155 -0
- package/src/routes/health.ts +32 -0
- package/src/routes/kv.ts +167 -0
- package/src/routes/oauth.ts +1055 -0
- package/src/routes/push.ts +1465 -0
- package/src/routes/room.ts +639 -0
- package/src/routes/schema-endpoint.ts +76 -0
- package/src/routes/sql.ts +176 -0
- package/src/routes/storage.ts +1674 -0
- package/src/routes/tables.ts +699 -0
- package/src/routes/users.ts +21 -0
- package/src/routes/vectorize.ts +372 -0
- 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
|
+
};
|