@daloyjs/core 0.14.2 → 0.16.0
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/README.md +9 -1
- package/dist/app.d.ts +102 -2
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +158 -15
- package/dist/app.js.map +1 -1
- package/dist/hashing.d.ts +59 -0
- package/dist/hashing.d.ts.map +1 -0
- package/dist/hashing.js +154 -0
- package/dist/hashing.js.map +1 -0
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +53 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +100 -1
- package/dist/logger.js.map +1 -1
- package/dist/middleware.d.ts +36 -0
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +42 -2
- package/dist/middleware.js.map +1 -1
- package/dist/security.d.ts +86 -0
- package/dist/security.d.ts.map +1 -1
- package/dist/security.js +185 -0
- package/dist/security.js.map +1 -1
- package/dist/types.d.ts +24 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -380,7 +380,7 @@ What works today, at a glance:
|
|
|
380
380
|
|
|
381
381
|
- Contract-first routing, Standard Schema validation (Zod 4 / Valibot / ArkType / TypeBox), and OpenAPI 3.1 from a single source of truth.
|
|
382
382
|
- Adapters for Node (Heroku/Railway/Render/Fly.io), Bun, Deno, Cloudflare Workers, Vercel Node / Edge / Next.js / Netlify Edge, Fastly Compute, and AWS Lambda / Netlify Functions / Lambda Function URLs.
|
|
383
|
-
- Built-in security primitives (body limits, prototype-pollution-safe JSON, path-traversal guard, request timeouts, header injection guards) plus first-party middleware (`secureHeaders` with CSP nonce + Trusted Types, `cors`, `rateLimit`, `requestId`, `bearerAuth`, `basicAuth`, `csrf` with **double-submit cookie** + **Fetch-Metadata** strategies, `session`, `timing` / `timingSafeEqual`).
|
|
383
|
+
- Built-in security primitives (body limits, prototype-pollution-safe JSON, path-traversal guard, request timeouts, header injection guards, **duplicate `Host` / `Content-Length` rejection**, **stripped `Server` / `X-Powered-By` headers by default**, **structured-log redaction defaults** for authorization / cookie / password / token / JWT-shaped values, **`secureHeaders()` auto-applied since `0.16.0`** with user-installed overrides automatically replacing the auto instance, **cross-origin state-changing requests rejected with `403` since `0.16.0`** unless a route's `cors()` policy allows the request origin) plus first-party middleware (`secureHeaders` with CSP nonce + Trusted Types, `cors`, `rateLimit`, `requestId`, `bearerAuth`, `basicAuth`, `csrf` with **double-submit cookie** + **Fetch-Metadata** strategies, `session`, `timing` / `timingSafeEqual`) and **zero-knob crypto helpers** (`passwordHash` / `passwordVerify` at `@daloyjs/core/hashing`, `verifyWebhookSignature` / `signWebhookPayload`).
|
|
384
384
|
- Streaming helpers (SSE + NDJSON), multipart ergonomics, OpenTelemetry-compatible tracing, signed-cookie sessions with pluggable stores, and a Redis-backed rate-limit store at `@daloyjs/core/rate-limit-redis`.
|
|
385
385
|
- WebSocket primitives with the same Bun-style handler shape (`open`/`message`/`close`/`drain`/`error`) running on both Node and Bun adapters, plus typed `app.ws(path, handler)` registration and route-table awareness so the upgrade listener is only installed when WS routes exist.
|
|
386
386
|
- Pretty `printStartupBanner()` / `formatStartupBanner()` startup helpers at `@daloyjs/core/banner`, used by every starter template so `pnpm dev` greets you with a colorized boxed panel (TTY + `NO_COLOR` / `FORCE_COLOR` aware, with an ASCII fallback for dumb terminals).
|
|
@@ -396,6 +396,14 @@ What works today, at a glance:
|
|
|
396
396
|
|
|
397
397
|
Roadmap, version-by-version plan, and shipped/in-progress checklists live in [ROADMAP.md](./ROADMAP.md).
|
|
398
398
|
|
|
399
|
+
## Contributing
|
|
400
|
+
|
|
401
|
+
DaloyJS is **public and MIT-licensed, but contributions-closed**. Pull requests
|
|
402
|
+
from accounts that are not invited maintainers or explicit repository
|
|
403
|
+
collaborators are closed automatically. Bug reports, feature requests, and
|
|
404
|
+
security disclosures are very welcome; see [CONTRIBUTING.md](./CONTRIBUTING.md)
|
|
405
|
+
and [SECURITY.md](./SECURITY.md) for the channels that *are* open.
|
|
406
|
+
|
|
399
407
|
## License
|
|
400
408
|
|
|
401
409
|
MIT
|
package/dist/app.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { type Logger } from "./logger.js";
|
|
|
3
3
|
import type { HttpMethod, Hooks, PathString, RequestSchemas, ResponsesMap, RouteDefinition } from "./types.js";
|
|
4
4
|
import { type OpenAPIInfo, type OpenAPIOptions } from "./openapi.js";
|
|
5
5
|
import { type DocsContentSecurityPolicyOptions, type ScalarReferenceConfiguration } from "./docs.js";
|
|
6
|
+
import { type SecureHeadersOptions } from "./middleware.js";
|
|
6
7
|
/**
|
|
7
8
|
* Configuration accepted by {@link App}'s constructor. Every field is
|
|
8
9
|
* optional; sensible production defaults are applied.
|
|
@@ -38,6 +39,63 @@ export interface AppOptions {
|
|
|
38
39
|
};
|
|
39
40
|
/** Production mode hides 5xx detail in error responses. Default: NODE_ENV === "production". */
|
|
40
41
|
production?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Explicit runtime environment. Takes precedence over `NODE_ENV` and
|
|
44
|
+
* {@link AppOptions.production}. When set, a one-time `warn` is logged if
|
|
45
|
+
* the value disagrees with `process.env.NODE_ENV` so deploy mismatches
|
|
46
|
+
* surface loudly instead of silently shipping a misconfigured production
|
|
47
|
+
* build. Accepted values mirror the canonical Node ecosystem strings.
|
|
48
|
+
*
|
|
49
|
+
* @since 0.15.0
|
|
50
|
+
*/
|
|
51
|
+
env?: "development" | "production" | "test";
|
|
52
|
+
/**
|
|
53
|
+
* Strip the `Server` and `X-Powered-By` response headers from every
|
|
54
|
+
* response (including those produced by user middleware). Defaults to
|
|
55
|
+
* `true` — fingerprinting parity with the rest of the secure-by-default
|
|
56
|
+
* surface. Set `false` only when you need the headers for downstream
|
|
57
|
+
* observability tooling.
|
|
58
|
+
*
|
|
59
|
+
* @since 0.15.0
|
|
60
|
+
*/
|
|
61
|
+
stripServerHeaders?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Master switch for the secure-by-default Wave 2 surface (auto-applied
|
|
64
|
+
* {@link secureHeaders}, cross-origin guard for state-changing requests
|
|
65
|
+
* when no {@link cors} hook allows the request origin). Defaults to `true`
|
|
66
|
+
* in `@daloyjs/core@0.16.0` and later. Per-feature opt-outs
|
|
67
|
+
* (`secureHeaders: false`, `corsCrossOriginGuard: false`) remain available
|
|
68
|
+
* when this is left on. Pass `false` to restore the pre-0.16 behavior
|
|
69
|
+
* wholesale.
|
|
70
|
+
*
|
|
71
|
+
* @since 0.16.0
|
|
72
|
+
*/
|
|
73
|
+
secureDefaults?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Auto-install {@link secureHeaders} as a global hook so every response
|
|
76
|
+
* carries hardened defaults (HSTS, X-Frame-Options, nosniff, strict
|
|
77
|
+
* referrer, COOP/CORP, default CSP, etc.). Active when
|
|
78
|
+
* {@link AppOptions.secureDefaults} is not `false`. Pass `false` to skip
|
|
79
|
+
* the auto-install (you may still register your own `secureHeaders()` via
|
|
80
|
+
* `app.use(...)`). Pass an options object to override the defaults of the
|
|
81
|
+
* auto-installed instance.
|
|
82
|
+
*
|
|
83
|
+
* @since 0.16.0
|
|
84
|
+
*/
|
|
85
|
+
secureHeaders?: SecureHeadersOptions | false;
|
|
86
|
+
/**
|
|
87
|
+
* Reject state-changing requests (`POST`, `PUT`, `PATCH`, `DELETE`) that
|
|
88
|
+
* carry a cross-origin `Origin` header when no {@link cors} hook in the
|
|
89
|
+
* matched route's hook chain allows that origin. Active when
|
|
90
|
+
* {@link AppOptions.secureDefaults} is not `false`. Returns
|
|
91
|
+
* `403 application/problem+json` so the rejection is loud at the network
|
|
92
|
+
* boundary instead of silently allowing a CSRF / SSRF surface. Set to
|
|
93
|
+
* `false` if you intentionally serve a cross-origin API without `cors()`
|
|
94
|
+
* (rare; almost always a misconfiguration).
|
|
95
|
+
*
|
|
96
|
+
* @since 0.16.0
|
|
97
|
+
*/
|
|
98
|
+
corsCrossOriginGuard?: boolean;
|
|
41
99
|
/** Pluggable logger. Default: structured JSON logger at "info" (or noop in test). */
|
|
42
100
|
logger?: Logger | {
|
|
43
101
|
level?: "trace" | "debug" | "info" | "warn" | "error" | "fatal";
|
|
@@ -259,13 +317,55 @@ export declare class App {
|
|
|
259
317
|
/** In-flight request count for graceful shutdown. */
|
|
260
318
|
private inflight;
|
|
261
319
|
private draining;
|
|
320
|
+
/**
|
|
321
|
+
* CORS origin allowlist predicates from the currently active group-level
|
|
322
|
+
* hooks. Used for unmatched routes; matched routes use the snapshot stored
|
|
323
|
+
* on their compiled route so later `app.use(cors(...))` calls do not
|
|
324
|
+
* retroactively loosen earlier routes.
|
|
325
|
+
*/
|
|
326
|
+
private corsOriginAllows;
|
|
262
327
|
constructor(options?: AppOptions);
|
|
328
|
+
/**
|
|
329
|
+
* Install the Wave 2 secure-by-default global hooks. Currently:
|
|
330
|
+
* - {@link secureHeaders} as a group-level hook so every response carries
|
|
331
|
+
* the hardened baseline (HSTS, X-Frame-Options, nosniff, default CSP).
|
|
332
|
+
*
|
|
333
|
+
* Called once during construction — the CORS cross-origin guard lives
|
|
334
|
+
* inside {@link App.fetch} because it needs the live request to decide.
|
|
335
|
+
* The auto-installed `secureHeaders` instance only sets headers when the
|
|
336
|
+
* response does not already carry them, so user-supplied
|
|
337
|
+
* `app.use(secureHeaders({...}))` still wins per-header.
|
|
338
|
+
*/
|
|
339
|
+
private installSecureDefaults;
|
|
340
|
+
/**
|
|
341
|
+
* Emit a one-time `warn` when the explicit {@link AppOptions.env} option
|
|
342
|
+
* disagrees with `process.env.NODE_ENV`. Silent when either signal is
|
|
343
|
+
* missing — surfacing only real misconfiguration.
|
|
344
|
+
*/
|
|
345
|
+
private warnOnEnvMismatch;
|
|
263
346
|
/**
|
|
264
347
|
* Resolve whether the app is running in production. Honours the explicit
|
|
265
|
-
*
|
|
266
|
-
*
|
|
348
|
+
* {@link AppOptions.env} option first, then {@link AppOptions.production},
|
|
349
|
+
* then falls back to `NODE_ENV === "production"`. Used by the docs
|
|
350
|
+
* auto-mount and error response detail stripping.
|
|
267
351
|
*/
|
|
268
352
|
private isProduction;
|
|
353
|
+
/**
|
|
354
|
+
* Wave 2 cross-origin guard. Rejects state-changing requests (`POST` /
|
|
355
|
+
* `PUT` / `PATCH` / `DELETE`) that carry an `Origin` header pointing at a
|
|
356
|
+
* different origin than the request URL when no {@link cors} hook is
|
|
357
|
+
* registered (neither at the app level nor on the matched route). Throws
|
|
358
|
+
* a {@link ForbiddenError} that surfaces as `403 application/problem+json`
|
|
359
|
+
* so the rejection is loud rather than silently allowing the unintended
|
|
360
|
+
* cross-origin write.
|
|
361
|
+
*
|
|
362
|
+
* Disabled when {@link AppOptions.secureDefaults} is `false` or
|
|
363
|
+
* {@link AppOptions.corsCrossOriginGuard} is `false`. Same-origin
|
|
364
|
+
* requests, GET / HEAD / OPTIONS, requests with no `Origin` header, and
|
|
365
|
+
* routes whose hook chain includes a `cors()` policy allowing the origin
|
|
366
|
+
* are unaffected.
|
|
367
|
+
*/
|
|
368
|
+
private assertCrossOriginAllowed;
|
|
269
369
|
/**
|
|
270
370
|
* Resolve the {@link AppOptions.docs} option and, when enabled, register
|
|
271
371
|
* the `/openapi.json` + `/docs` routes. Called once during construction so
|
package/dist/app.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,KAAK,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
|
package/dist/app.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { Router } from "./router.js";
|
|
2
2
|
import { WebSocketRegistry } from "./websocket.js";
|
|
3
|
-
import { BadRequestError, HttpError, InternalError, MethodNotAllowedError, NotFoundError, PayloadTooLargeError, RequestTimeoutError, UnsupportedMediaTypeError, ValidationError, } from "./errors.js";
|
|
3
|
+
import { BadRequestError, ForbiddenError, HttpError, InternalError, MethodNotAllowedError, NotFoundError, PayloadTooLargeError, RequestTimeoutError, UnsupportedMediaTypeError, ValidationError, } from "./errors.js";
|
|
4
4
|
import { validate } from "./schema.js";
|
|
5
|
-
import { readBodyLimited, safeJsonParse, randomId } from "./security.js";
|
|
5
|
+
import { readBodyLimited, safeJsonParse, randomId, assertNoDuplicateSingletonHeaders } from "./security.js";
|
|
6
6
|
import { createLogger, noopLogger } from "./logger.js";
|
|
7
7
|
import { generateOpenAPI, openapiToYAML, } from "./openapi.js";
|
|
8
8
|
import { docsContentSecurityPolicy, scalarHtml, swaggerUiHtml, } from "./docs.js";
|
|
9
|
+
import { secureHeaders as secureHeadersMiddleware, CORS_HOOK_MARKER, CORS_ORIGIN_ALLOW_MARKER, SECURE_HEADERS_MARKER, } from "./middleware.js";
|
|
10
|
+
const AUTO_SECURE_HEADERS_MARKER = Symbol.for("daloyjs.app.autoSecureHeaders");
|
|
9
11
|
const DEFAULTS = {
|
|
10
12
|
bodyLimitBytes: 1024 * 1024,
|
|
11
13
|
requestTimeoutMs: 30_000,
|
|
@@ -84,6 +86,13 @@ export class App {
|
|
|
84
86
|
/** In-flight request count for graceful shutdown. */
|
|
85
87
|
inflight = 0;
|
|
86
88
|
draining = false;
|
|
89
|
+
/**
|
|
90
|
+
* CORS origin allowlist predicates from the currently active group-level
|
|
91
|
+
* hooks. Used for unmatched routes; matched routes use the snapshot stored
|
|
92
|
+
* on their compiled route so later `app.use(cors(...))` calls do not
|
|
93
|
+
* retroactively loosen earlier routes.
|
|
94
|
+
*/
|
|
95
|
+
corsOriginAllows = [];
|
|
87
96
|
constructor(options = {}) {
|
|
88
97
|
this.options = {
|
|
89
98
|
validateResponses: options.validateResponses ?? DEFAULTS.validateResponses,
|
|
@@ -98,20 +107,110 @@ export class App {
|
|
|
98
107
|
typeof options.logger.info === "function"
|
|
99
108
|
? options.logger
|
|
100
109
|
: createLogger({ level: options.logger?.level ?? "info" });
|
|
110
|
+
this.warnOnEnvMismatch();
|
|
111
|
+
this.installSecureDefaults();
|
|
101
112
|
this.maybeMountDocs();
|
|
102
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Install the Wave 2 secure-by-default global hooks. Currently:
|
|
116
|
+
* - {@link secureHeaders} as a group-level hook so every response carries
|
|
117
|
+
* the hardened baseline (HSTS, X-Frame-Options, nosniff, default CSP).
|
|
118
|
+
*
|
|
119
|
+
* Called once during construction — the CORS cross-origin guard lives
|
|
120
|
+
* inside {@link App.fetch} because it needs the live request to decide.
|
|
121
|
+
* The auto-installed `secureHeaders` instance only sets headers when the
|
|
122
|
+
* response does not already carry them, so user-supplied
|
|
123
|
+
* `app.use(secureHeaders({...}))` still wins per-header.
|
|
124
|
+
*/
|
|
125
|
+
installSecureDefaults() {
|
|
126
|
+
if (this.options.secureDefaults === false)
|
|
127
|
+
return;
|
|
128
|
+
if (this.options.secureHeaders !== false) {
|
|
129
|
+
const opts = this.options.secureHeaders &&
|
|
130
|
+
typeof this.options.secureHeaders === "object"
|
|
131
|
+
? this.options.secureHeaders
|
|
132
|
+
: {};
|
|
133
|
+
const auto = secureHeadersMiddleware(opts);
|
|
134
|
+
auto[AUTO_SECURE_HEADERS_MARKER] = true;
|
|
135
|
+
this.groupHooks.push(auto);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Emit a one-time `warn` when the explicit {@link AppOptions.env} option
|
|
140
|
+
* disagrees with `process.env.NODE_ENV`. Silent when either signal is
|
|
141
|
+
* missing — surfacing only real misconfiguration.
|
|
142
|
+
*/
|
|
143
|
+
warnOnEnvMismatch() {
|
|
144
|
+
const env = this.options.env;
|
|
145
|
+
if (env === undefined)
|
|
146
|
+
return;
|
|
147
|
+
const nodeEnv = typeof process !== "undefined" && typeof process.env !== "undefined"
|
|
148
|
+
? process.env.NODE_ENV
|
|
149
|
+
: undefined;
|
|
150
|
+
if (nodeEnv && nodeEnv !== env) {
|
|
151
|
+
this.log.warn({ event: "env.mismatch", env, nodeEnv }, `app({ env: "${env}" }) disagrees with NODE_ENV="${nodeEnv}"`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
103
154
|
/**
|
|
104
155
|
* Resolve whether the app is running in production. Honours the explicit
|
|
105
|
-
*
|
|
106
|
-
*
|
|
156
|
+
* {@link AppOptions.env} option first, then {@link AppOptions.production},
|
|
157
|
+
* then falls back to `NODE_ENV === "production"`. Used by the docs
|
|
158
|
+
* auto-mount and error response detail stripping.
|
|
107
159
|
*/
|
|
108
160
|
isProduction() {
|
|
161
|
+
if (this.options.env !== undefined)
|
|
162
|
+
return this.options.env === "production";
|
|
109
163
|
if (this.options.production !== undefined)
|
|
110
164
|
return this.options.production;
|
|
111
165
|
return (typeof process !== "undefined" &&
|
|
112
166
|
typeof process.env !== "undefined" &&
|
|
113
167
|
process.env.NODE_ENV === "production");
|
|
114
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* Wave 2 cross-origin guard. Rejects state-changing requests (`POST` /
|
|
171
|
+
* `PUT` / `PATCH` / `DELETE`) that carry an `Origin` header pointing at a
|
|
172
|
+
* different origin than the request URL when no {@link cors} hook is
|
|
173
|
+
* registered (neither at the app level nor on the matched route). Throws
|
|
174
|
+
* a {@link ForbiddenError} that surfaces as `403 application/problem+json`
|
|
175
|
+
* so the rejection is loud rather than silently allowing the unintended
|
|
176
|
+
* cross-origin write.
|
|
177
|
+
*
|
|
178
|
+
* Disabled when {@link AppOptions.secureDefaults} is `false` or
|
|
179
|
+
* {@link AppOptions.corsCrossOriginGuard} is `false`. Same-origin
|
|
180
|
+
* requests, GET / HEAD / OPTIONS, requests with no `Origin` header, and
|
|
181
|
+
* routes whose hook chain includes a `cors()` policy allowing the origin
|
|
182
|
+
* are unaffected.
|
|
183
|
+
*/
|
|
184
|
+
assertCrossOriginAllowed(request, url, method, corsOriginAllows) {
|
|
185
|
+
if (this.options.secureDefaults === false)
|
|
186
|
+
return;
|
|
187
|
+
if (this.options.corsCrossOriginGuard === false)
|
|
188
|
+
return;
|
|
189
|
+
if (method !== "POST" &&
|
|
190
|
+
method !== "PUT" &&
|
|
191
|
+
method !== "PATCH" &&
|
|
192
|
+
method !== "DELETE") {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const origin = request.headers.get("origin");
|
|
196
|
+
if (!origin || origin === "null")
|
|
197
|
+
return;
|
|
198
|
+
let originUrl;
|
|
199
|
+
try {
|
|
200
|
+
originUrl = new URL(origin);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// Malformed Origin header — refuse loudly.
|
|
204
|
+
throw new ForbiddenError("Cross-origin state-changing request rejected: malformed Origin header.");
|
|
205
|
+
}
|
|
206
|
+
if (originUrl.origin === url.origin)
|
|
207
|
+
return;
|
|
208
|
+
if (corsOriginAllows.some((allows) => allows(origin)))
|
|
209
|
+
return;
|
|
210
|
+
throw new ForbiddenError(`Cross-origin ${method} from "${originUrl.origin}" rejected: no registered cors() policy allows that origin. ` +
|
|
211
|
+
`Register cors({ origin: [...] }) via app.use(...) to allow it, or pass ` +
|
|
212
|
+
`app({ corsCrossOriginGuard: false }) / app({ secureDefaults: false }) to disable this guard.`);
|
|
213
|
+
}
|
|
115
214
|
/**
|
|
116
215
|
* Resolve the {@link AppOptions.docs} option and, when enabled, register
|
|
117
216
|
* the `/openapi.json` + `/docs` routes. Called once during construction so
|
|
@@ -278,8 +377,10 @@ export class App {
|
|
|
278
377
|
tags: [...(this.groupTags ?? []), ...(def.tags ?? [])],
|
|
279
378
|
auth: def.auth ?? this.groupAuth,
|
|
280
379
|
};
|
|
281
|
-
const
|
|
282
|
-
|
|
380
|
+
const sources = [...this.groupHooks, def.hooks ?? {}];
|
|
381
|
+
const hooks = mergeHooks(sources);
|
|
382
|
+
const corsOriginAllows = corsOriginAllowsFromHooks(sources);
|
|
383
|
+
this.router.add(def.method, fullPath, { def: merged, hooks, corsOriginAllows }, def.operationId);
|
|
283
384
|
this.routes.push(merged);
|
|
284
385
|
return this;
|
|
285
386
|
}
|
|
@@ -333,6 +434,7 @@ export class App {
|
|
|
333
434
|
...this.groupHooks,
|
|
334
435
|
...(config.hooks ? [config.hooks] : []),
|
|
335
436
|
];
|
|
437
|
+
child.corsOriginAllows = corsOriginAllowsFromHooks(child.groupHooks);
|
|
336
438
|
child.groupTags = [...this.groupTags, ...(config.tags ?? [])];
|
|
337
439
|
child.groupAuth = config.auth ?? this.groupAuth;
|
|
338
440
|
child.decorations = this.decorations;
|
|
@@ -362,7 +464,21 @@ export class App {
|
|
|
362
464
|
* @returns This `App` instance for chaining.
|
|
363
465
|
*/
|
|
364
466
|
use(hooks) {
|
|
467
|
+
// If the developer installs their own secureHeaders(), drop the
|
|
468
|
+
// Wave 2 auto-installed instance so the user's overrides win instead of
|
|
469
|
+
// being shadowed (the auto one runs first and the per-header
|
|
470
|
+
// "set only if absent" semantics mean the second installation would be
|
|
471
|
+
// a silent no-op).
|
|
472
|
+
if (hooks[SECURE_HEADERS_MARKER] === true) {
|
|
473
|
+
const autoIdx = this.groupHooks.findIndex((h) => h[AUTO_SECURE_HEADERS_MARKER] ===
|
|
474
|
+
true);
|
|
475
|
+
if (autoIdx >= 0)
|
|
476
|
+
this.groupHooks.splice(autoIdx, 1);
|
|
477
|
+
}
|
|
365
478
|
this.groupHooks.push(hooks);
|
|
479
|
+
if (hooks[CORS_HOOK_MARKER] === true) {
|
|
480
|
+
this.corsOriginAllows = corsOriginAllowsFromHooks(this.groupHooks);
|
|
481
|
+
}
|
|
366
482
|
return this;
|
|
367
483
|
}
|
|
368
484
|
/**
|
|
@@ -546,18 +662,24 @@ export class App {
|
|
|
546
662
|
method: request.method,
|
|
547
663
|
url: request.url,
|
|
548
664
|
});
|
|
665
|
+
const stripFingerprint = this.options.stripServerHeaders !== false;
|
|
549
666
|
let ctx;
|
|
550
667
|
const globalHooks = mergeHooks([this.options.hooks ?? {}]);
|
|
551
668
|
let activeErrorHook = globalHooks.onError;
|
|
552
669
|
let activeResponseHook = globalHooks.onResponse;
|
|
553
670
|
let activeSendHook = globalHooks.onSend;
|
|
554
671
|
try {
|
|
672
|
+
assertNoDuplicateSingletonHeaders(request.headers);
|
|
555
673
|
await globalHooks.onRequest?.(request);
|
|
556
674
|
const url = new URL(request.url);
|
|
557
675
|
const method = request.method;
|
|
558
676
|
const headFallback = method === "HEAD";
|
|
559
677
|
const match = this.router.find(method, url.pathname) ??
|
|
560
678
|
(headFallback ? this.router.find("GET", url.pathname) : undefined);
|
|
679
|
+
this.assertCrossOriginAllowed(request, url, method, [
|
|
680
|
+
...corsOriginAllowsFromHooks([this.options.hooks ?? {}]),
|
|
681
|
+
...(match ? match.handler.corsOriginAllows : this.corsOriginAllows),
|
|
682
|
+
]);
|
|
561
683
|
if (!match) {
|
|
562
684
|
const allowed = this.router.allowedMethods(url.pathname);
|
|
563
685
|
ctx = {
|
|
@@ -590,7 +712,7 @@ export class App {
|
|
|
590
712
|
const intercepted = await preflightHooks.beforeHandle?.(synthCtx);
|
|
591
713
|
if (intercepted instanceof Response) {
|
|
592
714
|
copyContextHeaders(synthCtx, intercepted);
|
|
593
|
-
return await finalizeResponse(intercepted, synthCtx, preflightHooks);
|
|
715
|
+
return await finalizeResponse(intercepted, synthCtx, preflightHooks, stripFingerprint);
|
|
594
716
|
}
|
|
595
717
|
const res = new Response(null, {
|
|
596
718
|
status: 204,
|
|
@@ -598,7 +720,7 @@ export class App {
|
|
|
598
720
|
});
|
|
599
721
|
copyContextHeaders(synthCtx, res);
|
|
600
722
|
res.headers.set("x-request-id", requestId);
|
|
601
|
-
return await finalizeResponse(res, synthCtx, preflightHooks);
|
|
723
|
+
return await finalizeResponse(res, synthCtx, preflightHooks, stripFingerprint);
|
|
602
724
|
}
|
|
603
725
|
throw new MethodNotAllowedError(allowed);
|
|
604
726
|
}
|
|
@@ -616,7 +738,7 @@ export class App {
|
|
|
616
738
|
const before = await allHooks.beforeHandle?.(ctx);
|
|
617
739
|
if (before instanceof Response) {
|
|
618
740
|
copyContextHeaders(ctx, before);
|
|
619
|
-
return await finalizeResponse(before, ctx, allHooks);
|
|
741
|
+
return await finalizeResponse(before, ctx, allHooks, stripFingerprint);
|
|
620
742
|
}
|
|
621
743
|
let result = this.options.mockMode
|
|
622
744
|
? mockResponseFor(def)
|
|
@@ -632,7 +754,7 @@ export class App {
|
|
|
632
754
|
});
|
|
633
755
|
}
|
|
634
756
|
copyContextHeaders(ctx, response);
|
|
635
|
-
return await finalizeResponse(response, ctx, allHooks);
|
|
757
|
+
return await finalizeResponse(response, ctx, allHooks, stripFingerprint);
|
|
636
758
|
}
|
|
637
759
|
catch (err) {
|
|
638
760
|
const handled = await activeErrorHook?.(err, ctx);
|
|
@@ -644,7 +766,7 @@ export class App {
|
|
|
644
766
|
return finalizeResponse(handled, ctx, {
|
|
645
767
|
onSend: activeSendHook,
|
|
646
768
|
onResponse: activeResponseHook,
|
|
647
|
-
});
|
|
769
|
+
}, stripFingerprint);
|
|
648
770
|
}
|
|
649
771
|
const httpErr = err instanceof HttpError
|
|
650
772
|
? err
|
|
@@ -654,7 +776,7 @@ export class App {
|
|
|
654
776
|
if (httpErr.status < 500)
|
|
655
777
|
log.warn({ status: httpErr.status }, httpErr.problem.title);
|
|
656
778
|
const res = httpErr.toResponse({
|
|
657
|
-
production: this.
|
|
779
|
+
production: this.isProduction(),
|
|
658
780
|
requestId,
|
|
659
781
|
});
|
|
660
782
|
if (ctx)
|
|
@@ -664,7 +786,7 @@ export class App {
|
|
|
664
786
|
return finalizeResponse(res, ctx, {
|
|
665
787
|
onSend: activeSendHook,
|
|
666
788
|
onResponse: activeResponseHook,
|
|
667
|
-
});
|
|
789
|
+
}, stripFingerprint);
|
|
668
790
|
}
|
|
669
791
|
finally {
|
|
670
792
|
this.inflight--;
|
|
@@ -778,6 +900,23 @@ function joinPath(a, b) {
|
|
|
778
900
|
const joined = `${left}${right}`;
|
|
779
901
|
return joined === "" ? "/" : joined;
|
|
780
902
|
}
|
|
903
|
+
function corsOriginAllowsFromHooks(layers) {
|
|
904
|
+
const allows = [];
|
|
905
|
+
for (const hooks of layers) {
|
|
906
|
+
const record = hooks;
|
|
907
|
+
const allow = record[CORS_ORIGIN_ALLOW_MARKER];
|
|
908
|
+
if (typeof allow === "function") {
|
|
909
|
+
allows.push(allow);
|
|
910
|
+
}
|
|
911
|
+
else if (record[CORS_HOOK_MARKER] === true) {
|
|
912
|
+
// Third-party CORS helpers can still opt out of the guard with the
|
|
913
|
+
// original marker. The first-party cors() stamps a stricter predicate
|
|
914
|
+
// above, so disallowed origins are rejected before the handler runs.
|
|
915
|
+
allows.push(() => true);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
return allows;
|
|
919
|
+
}
|
|
781
920
|
function mergeHooks(layers) {
|
|
782
921
|
const pick = (key) => layers
|
|
783
922
|
.map((h) => h[key])
|
|
@@ -804,9 +943,13 @@ function responsePipeline(fns) {
|
|
|
804
943
|
return current;
|
|
805
944
|
};
|
|
806
945
|
}
|
|
807
|
-
async function finalizeResponse(res, ctx, hooks) {
|
|
946
|
+
async function finalizeResponse(res, ctx, hooks, stripFingerprint = true) {
|
|
808
947
|
const sent = await hooks.onSend?.(res, ctx);
|
|
809
948
|
const final = sent instanceof Response ? sent : res;
|
|
949
|
+
if (stripFingerprint) {
|
|
950
|
+
final.headers.delete("server");
|
|
951
|
+
final.headers.delete("x-powered-by");
|
|
952
|
+
}
|
|
810
953
|
await hooks.onResponse?.(final);
|
|
811
954
|
return final;
|
|
812
955
|
}
|
|
@@ -880,7 +1023,7 @@ async function buildContext(request, url, rawParams, def, opts) {
|
|
|
880
1023
|
}
|
|
881
1024
|
if (def.request?.body) {
|
|
882
1025
|
const ct = (request.headers.get("content-type") ?? "").toLowerCase();
|
|
883
|
-
const allowed = opts.allowedContentTypes ?? [
|
|
1026
|
+
const allowed = def.accepts ?? opts.allowedContentTypes ?? [
|
|
884
1027
|
"application/json",
|
|
885
1028
|
"application/x-www-form-urlencoded",
|
|
886
1029
|
"multipart/form-data",
|