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