@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 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
- * `production` option first, then falls back to `NODE_ENV === "production"`.
266
- * Used by the docs auto-mount and error response detail stripping.
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":"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"}
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
- * `production` option first, then falls back to `NODE_ENV === "production"`.
106
- * Used by the docs auto-mount and error response detail stripping.
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 hooks = mergeHooks([...this.groupHooks, def.hooks ?? {}]);
282
- this.router.add(def.method, fullPath, { def: merged, hooks }, def.operationId);
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.options.production,
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",