@daloyjs/core 1.2.0 → 1.2.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/dist/app.js CHANGED
@@ -2,8 +2,8 @@ import { Router } from "./router.js";
2
2
  import { WebSocketRegistry, normalizeWebSocketOptions, } from "./websocket.js";
3
3
  import { BadRequestError, ForbiddenError, HttpError, InternalError, MethodNotAllowedError, NotFoundError, PayloadTooLargeError, RequestTimeoutError, TooManyRequestsError, UnsupportedMediaTypeError, ValidationError, } from "./errors.js";
4
4
  import { readBodyLimited, safeJsonParseLimited, randomId, assertInboundHeaderGuards, DEFAULT_MAX_HEADER_COUNT, assertStrongSecret, timingSafeEqual, isForbiddenObjectKey, } from "./security.js";
5
- import { createLogger, noopLogger, sanitizeUrlForLog } from "./logger.js";
6
- import { createAppTelemetry } from "./otlp.js";
5
+ import { createLogger, noopLogger, sanitizeUrlForLog, } from "./logger.js";
6
+ import { createAppTelemetry, } from "./otlp.js";
7
7
  import { generateOpenAPI, openapiToYAML, } from "./openapi.js";
8
8
  import { isSchemaValidatedResponse } from "./internal-response.js";
9
9
  import { asyncapiHtml, docsContentSecurityPolicy, redocHtml, scalarHtml, swaggerUiHtml, } from "./docs.js";
@@ -13,7 +13,7 @@ import { COMPRESSION_HOOK_MARKER } from "./compression.js";
13
13
  import { SESSION_HOOK_MARKER, SESSION_SECRETS_MARKER } from "./session.js";
14
14
  import { loadShedding as loadSheddingMiddleware, } from "./load-shedding.js";
15
15
  import { httpMetrics, MetricsRegistry, PROMETHEUS_CONTENT_TYPE, } from "./metrics.js";
16
- import { Scheduler } from "./scheduler.js";
16
+ import { Scheduler, } from "./scheduler.js";
17
17
  import { securitySchemeRequiresPayloadAuth } from "./security-schemes.js";
18
18
  import { assertBehindProxy } from "./conn-info.js";
19
19
  const AUTO_SECURE_HEADERS_MARKER = Symbol.for("daloyjs.app.autoSecureHeaders");
@@ -485,7 +485,9 @@ export class App {
485
485
  }
486
486
  get globalCorsAllows() {
487
487
  if (this._globalCorsAllowsCache === undefined) {
488
- this._globalCorsAllowsCache = corsOriginAllowsFromHooks([this.options.hooks ?? {}]);
488
+ this._globalCorsAllowsCache = corsOriginAllowsFromHooks([
489
+ this.options.hooks ?? {},
490
+ ]);
489
491
  }
490
492
  return this._globalCorsAllowsCache;
491
493
  }
@@ -498,7 +500,10 @@ export class App {
498
500
  _coldPathHooksCache;
499
501
  get coldPathHooks() {
500
502
  if (this._coldPathHooksCache === undefined) {
501
- this._coldPathHooksCache = mergeHooks([this.options.hooks ?? {}, ...this.groupHooks]);
503
+ this._coldPathHooksCache = mergeHooks([
504
+ this.options.hooks ?? {},
505
+ ...this.groupHooks,
506
+ ]);
502
507
  }
503
508
  return this._coldPathHooksCache;
504
509
  }
@@ -524,11 +529,14 @@ export class App {
524
529
  this.log =
525
530
  options.logger === false
526
531
  ? noopLogger
527
- : options.logger && typeof options.logger.info === "function"
532
+ : options.logger &&
533
+ typeof options.logger.info === "function"
528
534
  ? options.logger
529
535
  : createLogger({
530
536
  level: options.logger?.level ?? "info",
531
- ...(telemetryWrite !== undefined ? { write: telemetryWrite } : {}),
537
+ ...(telemetryWrite !== undefined
538
+ ? { write: telemetryWrite }
539
+ : {}),
532
540
  });
533
541
  this.warnOnEnvMismatch();
534
542
  this.assertDisconnectStatusCode();
@@ -659,7 +667,9 @@ export class App {
659
667
  secureHeaders: o.secureDefaults !== false && o.secureHeaders !== false,
660
668
  corsCrossOriginGuard: o.secureDefaults !== false && o.corsCrossOriginGuard !== false,
661
669
  csrf: o.csrf === "off" ? "off" : "on",
662
- crashOnUnhandledRejection: o.crashOnUnhandledRejection === undefined ? "default" : o.crashOnUnhandledRejection,
670
+ crashOnUnhandledRejection: o.crashOnUnhandledRejection === undefined
671
+ ? "default"
672
+ : o.crashOnUnhandledRejection,
663
673
  trustProxy: o.trustProxy === undefined ? "unconfigured" : o.trustProxy,
664
674
  bodyLimitBytes: this.options.bodyLimitBytes,
665
675
  requestTimeoutMs: this.options.requestTimeoutMs,
@@ -685,7 +695,8 @@ export class App {
685
695
  if (this.options.secureDefaults === false)
686
696
  return;
687
697
  if (this.options.secureHeaders !== false) {
688
- const opts = this.options.secureHeaders && typeof this.options.secureHeaders === "object"
698
+ const opts = this.options.secureHeaders &&
699
+ typeof this.options.secureHeaders === "object"
689
700
  ? this.options.secureHeaders
690
701
  : {};
691
702
  const auto = secureHeadersMiddleware(opts);
@@ -694,7 +705,9 @@ export class App {
694
705
  }
695
706
  // Opt-in load-shedding pressure monitor.
696
707
  if (this.options.loadShedding) {
697
- const lsOpts = typeof this.options.loadShedding === "object" ? this.options.loadShedding : {};
708
+ const lsOpts = typeof this.options.loadShedding === "object"
709
+ ? this.options.loadShedding
710
+ : {};
698
711
  this.groupHooks.push(loadSheddingMiddleware(lsOpts));
699
712
  }
700
713
  }
@@ -801,7 +814,10 @@ export class App {
801
814
  return;
802
815
  if (this.options.corsCrossOriginGuard === false)
803
816
  return;
804
- if (method !== "POST" && method !== "PUT" && method !== "PATCH" && method !== "DELETE") {
817
+ if (method !== "POST" &&
818
+ method !== "PUT" &&
819
+ method !== "PATCH" &&
820
+ method !== "DELETE") {
805
821
  return;
806
822
  }
807
823
  const origin = request.headers.get("origin");
@@ -816,7 +832,9 @@ export class App {
816
832
  // are identical on both paths.
817
833
  const fastHeaderOrigin = getOriginFast(origin);
818
834
  if (fastHeaderOrigin !== undefined) {
819
- const fastReqOrigin = typeof requestUrl === "string" ? getOriginFast(requestUrl) : requestUrl.origin;
835
+ const fastReqOrigin = typeof requestUrl === "string"
836
+ ? getOriginFast(requestUrl)
837
+ : requestUrl.origin;
820
838
  if (fastReqOrigin !== undefined) {
821
839
  if (fastHeaderOrigin === fastReqOrigin)
822
840
  return;
@@ -835,7 +853,9 @@ export class App {
835
853
  // Malformed Origin header — refuse loudly.
836
854
  throw new ForbiddenError("Cross-origin state-changing request rejected: malformed Origin header.");
837
855
  }
838
- const reqOrigin = typeof requestUrl === "string" ? new URL(requestUrl).origin : requestUrl.origin;
856
+ const reqOrigin = typeof requestUrl === "string"
857
+ ? new URL(requestUrl).origin
858
+ : requestUrl.origin;
839
859
  if (originUrl.origin === reqOrigin)
840
860
  return;
841
861
  if (corsOriginAllows.some((allows) => allows(origin)))
@@ -940,7 +960,8 @@ export class App {
940
960
  * `test`, which is a known, non-production answer).
941
961
  */
942
962
  isEnvIndeterminate() {
943
- if (this.options.env !== undefined || this.options.production !== undefined) {
963
+ if (this.options.env !== undefined ||
964
+ this.options.production !== undefined) {
944
965
  return false;
945
966
  }
946
967
  const nodeEnv = typeof process !== "undefined" && typeof process.env !== "undefined"
@@ -1100,7 +1121,8 @@ export class App {
1100
1121
  // follow them there because its `keyGenerator` is caller-supplied and may
1101
1122
  // read `ctx.state`, so the unsafe order is refused instead.
1102
1123
  const replayBeforeBudget = this.routeSecurityMarkers.find((r) => r.replayBeforeBudget !== null);
1103
- if (replayBeforeBudget !== undefined && this.bootGuard.error === undefined) {
1124
+ if (replayBeforeBudget !== undefined &&
1125
+ this.bootGuard.error === undefined) {
1104
1126
  this.bootGuard.error = new Error(`Route ${replayBeforeBudget.method} ${replayBeforeBudget.path} runs ` +
1105
1127
  `${replayBeforeBudget.replayBeforeBudget} before rateLimit() / loginThrottle() in its ` +
1106
1128
  `effective hook chain. Both act from beforeHandle, so a cache hit or an idempotent ` +
@@ -1194,7 +1216,8 @@ export class App {
1194
1216
  // refusal must stay visible — but drop the stack, so a client cannot
1195
1217
  // multiply the bytes it pushes into the error tier by replaying the header.
1196
1218
  // The actionable message is logged once per process by the warn above.
1197
- refusal[OMIT_STACK_IN_LOG] = true;
1219
+ refusal[OMIT_STACK_IN_LOG] =
1220
+ true;
1198
1221
  throw refusal;
1199
1222
  }
1200
1223
  /**
@@ -1247,11 +1270,15 @@ export class App {
1247
1270
  };
1248
1271
  const generate = async () => generateOpenAPI(this, {
1249
1272
  info: resolveInfo(),
1250
- ...(this.options.openapi?.servers ? { servers: this.options.openapi.servers } : {}),
1273
+ ...(this.options.openapi?.servers
1274
+ ? { servers: this.options.openapi.servers }
1275
+ : {}),
1251
1276
  ...(this.options.openapi?.securitySchemes
1252
1277
  ? { securitySchemes: this.options.openapi.securitySchemes }
1253
1278
  : {}),
1254
- ...(this.options.openapi?.webhooks ? { webhooks: this.options.openapi.webhooks } : {}),
1279
+ ...(this.options.openapi?.webhooks
1280
+ ? { webhooks: this.options.openapi.webhooks }
1281
+ : {}),
1255
1282
  });
1256
1283
  this.route({
1257
1284
  method: "GET",
@@ -1279,7 +1306,9 @@ export class App {
1279
1306
  summary: "OpenAPI 3.1 document (YAML)",
1280
1307
  acknowledgeNoResponseBodySchema: true,
1281
1308
  responses: {
1282
- 200: { description: "OpenAPI 3.1 document for this application, in YAML." },
1309
+ 200: {
1310
+ description: "OpenAPI 3.1 document for this application, in YAML.",
1311
+ },
1283
1312
  },
1284
1313
  handler: async () => ({
1285
1314
  status: 200,
@@ -1308,7 +1337,9 @@ export class App {
1308
1337
  ...(ui === "redoc"
1309
1338
  ? { ...opts.csp, allowBlobWorkers: opts.csp?.allowBlobWorkers ?? true }
1310
1339
  : opts.csp),
1311
- ...(docsConnectOrigins.length ? { connectOrigins: docsConnectOrigins } : {}),
1340
+ ...(docsConnectOrigins.length
1341
+ ? { connectOrigins: docsConnectOrigins }
1342
+ : {}),
1312
1343
  });
1313
1344
  this.route({
1314
1345
  method: "GET",
@@ -1393,7 +1424,9 @@ export class App {
1393
1424
  */
1394
1425
  mountAsyncAPI(opts) {
1395
1426
  const jsonPath = (opts.jsonPath ?? "/asyncapi.json");
1396
- const yamlPath = opts.yamlPath === false ? null : (opts.yamlPath ?? "/asyncapi.yaml");
1427
+ const yamlPath = opts.yamlPath === false
1428
+ ? null
1429
+ : (opts.yamlPath ?? "/asyncapi.yaml");
1397
1430
  const uiPath = (opts.path ?? "/asyncapi");
1398
1431
  const tags = opts.tags ?? ["AsyncAPI"];
1399
1432
  const resolveInfo = () => {
@@ -1416,7 +1449,9 @@ export class App {
1416
1449
  // Framework-owned bodies on the AsyncAPI surface, like mountDocs above.
1417
1450
  acknowledgeNoResponseBodySchema: true,
1418
1451
  responses: {
1419
- 200: { description: "AsyncAPI 3.0 document for this application's WebSocket channels." },
1452
+ 200: {
1453
+ description: "AsyncAPI 3.0 document for this application's WebSocket channels.",
1454
+ },
1420
1455
  },
1421
1456
  handler: async () => ({ status: 200, body: await generate() }),
1422
1457
  });
@@ -1429,7 +1464,9 @@ export class App {
1429
1464
  summary: "AsyncAPI 3.0 document (YAML)",
1430
1465
  acknowledgeNoResponseBodySchema: true,
1431
1466
  responses: {
1432
- 200: { description: "AsyncAPI 3.0 document for this application, in YAML." },
1467
+ 200: {
1468
+ description: "AsyncAPI 3.0 document for this application, in YAML.",
1469
+ },
1433
1470
  },
1434
1471
  handler: async () => ({
1435
1472
  status: 200,
@@ -1574,7 +1611,10 @@ export class App {
1574
1611
  ...corsOriginAllowsFromHooks([globalHookLayer]),
1575
1612
  ...corsOriginAllows,
1576
1613
  ];
1577
- const securityMarkers = securityMarkersFromHooks([globalHookLayer, ...sources]);
1614
+ const securityMarkers = securityMarkersFromHooks([
1615
+ globalHookLayer,
1616
+ ...sources,
1617
+ ]);
1578
1618
  // Capture the decorations of this route's scope at registration time so the
1579
1619
  // dispatch hot path reads the scope-local bag rather than the root app's.
1580
1620
  // `this.decorations` is this scope's own bag (the root's, or the child's
@@ -1641,7 +1681,9 @@ export class App {
1641
1681
  return this.addHttpShorthand("HEAD", path, options, handler);
1642
1682
  }
1643
1683
  addHttpShorthand(method, path, options, possibleHandler) {
1644
- if (options === null || typeof options !== "object" || typeof possibleHandler !== "function") {
1684
+ if (options === null ||
1685
+ typeof options !== "object" ||
1686
+ typeof possibleHandler !== "function") {
1645
1687
  throw new TypeError(`app.${method.toLowerCase()}(): expected (path, contract, handler); opaque responses require an explicit contract with acknowledgeNoResponseBodySchema: true`);
1646
1688
  }
1647
1689
  const contract = options;
@@ -1767,7 +1809,9 @@ export class App {
1767
1809
  */
1768
1810
  readinesscheck(opts = {}) {
1769
1811
  this.registerHealthRoute("readinesscheck", opts, () => {
1770
- if (this.draining || this.pendingPlugins.size > 0 || this.pluginBootError.failed) {
1812
+ if (this.draining ||
1813
+ this.pendingPlugins.size > 0 ||
1814
+ this.pluginBootError.failed) {
1771
1815
  return {
1772
1816
  status: 503,
1773
1817
  body: { status: "not-ready" },
@@ -1802,8 +1846,13 @@ export class App {
1802
1846
  *
1803
1847
  * Call this **before** registering the routes you want measured — like any
1804
1848
  * `app.use(...)` middleware, the instrumentation only wraps routes added
1805
- * afterwards. Pass `opts.registry` to register custom application metrics
1806
- * that are rendered alongside the built-in HTTP series.
1849
+ * afterwards. Calling it after routes already exist logs a
1850
+ * `metrics.late_install` warning listing the uninstrumented paths.
1851
+ * Pass `opts.registry` to register custom application metrics that are
1852
+ * rendered alongside the built-in HTTP series. Default series names are
1853
+ * unprefixed (`http_requests_total`, `process_resident_memory_bytes`);
1854
+ * construct the registry with `prefix: "daloy_"` if you want the old
1855
+ * names.
1807
1856
  *
1808
1857
  * @param opts - Path, auth, rate-limit, registry, and label configuration.
1809
1858
  * @returns `this` for chaining.
@@ -1812,7 +1861,9 @@ export class App {
1812
1861
  metrics(opts = {}) {
1813
1862
  const path = (opts.path ?? "/metrics");
1814
1863
  const registry = opts.registry ?? new MetricsRegistry();
1815
- const rateLimitConfig = opts.rateLimit === false ? null : { limit: 60, windowMs: 60_000, ...(opts.rateLimit ?? {}) };
1864
+ const rateLimitConfig = opts.rateLimit === false
1865
+ ? null
1866
+ : { limit: 60, windowMs: 60_000, ...(opts.rateLimit ?? {}) };
1816
1867
  const token = opts.token;
1817
1868
  // Refuse-to-boot: an unauthenticated metrics scrape in production is a
1818
1869
  // documented info-disclosure surface (route inventory, latency
@@ -1826,6 +1877,27 @@ export class App {
1826
1877
  `Authorization: Bearer <token>, or pass acknowledgeUnauthenticated: true ` +
1827
1878
  `to acknowledge that this scrape endpoint is reachable without credentials.`);
1828
1879
  }
1880
+ // Group hooks only wrap routes registered *after* this call. Framework
1881
+ // auto-mounts (docs / health / metrics / reporting) are tagged and
1882
+ // ignored; anything else already on `this.routes` is a footgun.
1883
+ const skipLateTags = new Set([
1884
+ "Docs",
1885
+ "Observability",
1886
+ "Health",
1887
+ "Reporting",
1888
+ ]);
1889
+ const late = this.routes.filter((r) => {
1890
+ const tags = r.tags ?? [];
1891
+ return !tags.some((t) => skipLateTags.has(t));
1892
+ });
1893
+ if (late.length > 0) {
1894
+ const listed = late.slice(0, 20).map((r) => `${r.method} ${r.path}`);
1895
+ this.log.warn({
1896
+ event: "metrics.late_install",
1897
+ count: late.length,
1898
+ routes: listed,
1899
+ }, `app.metrics() was called after ${late.length} route(s) were registered; those routes are not RED-instrumented. Call app.metrics() before app.get/post/...`);
1900
+ }
1829
1901
  // Install RED instrumentation as a group hook so it wraps every route
1830
1902
  // registered after this call. Always exclude the scrape path itself, plus
1831
1903
  // any caller-supplied predicate.
@@ -1838,7 +1910,9 @@ export class App {
1838
1910
  exclude,
1839
1911
  }));
1840
1912
  this._coldPathHooksCache = undefined;
1841
- const buckets = rateLimitConfig ? new Map() : null;
1913
+ const buckets = rateLimitConfig
1914
+ ? new Map()
1915
+ : null;
1842
1916
  const trustProxyHeaders = appTrustsProxyHeaders(this.options);
1843
1917
  this.route({
1844
1918
  method: "GET",
@@ -1854,7 +1928,10 @@ export class App {
1854
1928
  const now = Date.now();
1855
1929
  const entry = buckets.get(key);
1856
1930
  if (!entry || entry.resetMs <= now) {
1857
- buckets.set(key, { count: 1, resetMs: now + rateLimitConfig.windowMs });
1931
+ buckets.set(key, {
1932
+ count: 1,
1933
+ resetMs: now + rateLimitConfig.windowMs,
1934
+ });
1858
1935
  }
1859
1936
  else {
1860
1937
  entry.count++;
@@ -1922,7 +1999,9 @@ export class App {
1922
1999
  */
1923
2000
  cron(def, handler) {
1924
2001
  if (this.scheduler === undefined) {
1925
- const scheduler = new Scheduler({ logger: this.log.child({ component: "scheduler" }) });
2002
+ const scheduler = new Scheduler({
2003
+ logger: this.log.child({ component: "scheduler" }),
2004
+ });
1926
2005
  this.scheduler = scheduler;
1927
2006
  scheduler.start();
1928
2007
  // Drain the scheduler during the post-drain close phase so periodic
@@ -1945,7 +2024,9 @@ export class App {
1945
2024
  const isHealth = kind === "healthcheck";
1946
2025
  const defaultPath = (isHealth ? "/healthz" : "/readyz");
1947
2026
  const path = (opts.path ?? defaultPath);
1948
- const rateLimitConfig = opts.rateLimit === false ? null : { limit: 60, windowMs: 60_000, ...(opts.rateLimit ?? {}) };
2027
+ const rateLimitConfig = opts.rateLimit === false
2028
+ ? null
2029
+ : { limit: 60, windowMs: 60_000, ...(opts.rateLimit ?? {}) };
1949
2030
  const token = opts.token;
1950
2031
  // Refuse-to-boot: unauthenticated health/ready probes in
1951
2032
  // production are a documented info-disclosure surface (process uptime,
@@ -1959,7 +2040,9 @@ export class App {
1959
2040
  `Authorization: Bearer <token>, or pass acknowledgeUnauthenticated: true ` +
1960
2041
  `to acknowledge that this probe is reachable without credentials.`);
1961
2042
  }
1962
- const buckets = rateLimitConfig ? new Map() : null;
2043
+ const buckets = rateLimitConfig
2044
+ ? new Map()
2045
+ : null;
1963
2046
  const trustProxyHeaders = appTrustsProxyHeaders(this.options);
1964
2047
  this.route({
1965
2048
  method: "GET",
@@ -1975,7 +2058,10 @@ export class App {
1975
2058
  const now = Date.now();
1976
2059
  const entry = buckets.get(key);
1977
2060
  if (!entry || entry.resetMs <= now) {
1978
- buckets.set(key, { count: 1, resetMs: now + rateLimitConfig.windowMs });
2061
+ buckets.set(key, {
2062
+ count: 1,
2063
+ resetMs: now + rateLimitConfig.windowMs,
2064
+ });
1979
2065
  }
1980
2066
  else {
1981
2067
  entry.count++;
@@ -2033,8 +2119,12 @@ export class App {
2033
2119
  if (!Number.isInteger(maxBytes) || maxBytes <= 0 || maxBytes > HARD_MAX) {
2034
2120
  throw new Error(`cspReportRoute(): maxBodyBytes must be a positive integer <= ${HARD_MAX}.`);
2035
2121
  }
2036
- const rateLimitConfig = opts.rateLimit === false ? null : { limit: 60, windowMs: 60_000, ...(opts.rateLimit ?? {}) };
2037
- const buckets = rateLimitConfig ? new Map() : null;
2122
+ const rateLimitConfig = opts.rateLimit === false
2123
+ ? null
2124
+ : { limit: 60, windowMs: 60_000, ...(opts.rateLimit ?? {}) };
2125
+ const buckets = rateLimitConfig
2126
+ ? new Map()
2127
+ : null;
2038
2128
  const trustProxyHeaders = appTrustsProxyHeaders(this.options);
2039
2129
  const log = this.log;
2040
2130
  // Only log report bodies when explicitly enabled. In
@@ -2053,7 +2143,10 @@ export class App {
2053
2143
  const now = Date.now();
2054
2144
  const entry = buckets.get(key);
2055
2145
  if (!entry || entry.resetMs <= now) {
2056
- buckets.set(key, { count: 1, resetMs: now + rateLimitConfig.windowMs });
2146
+ buckets.set(key, {
2147
+ count: 1,
2148
+ resetMs: now + rateLimitConfig.windowMs,
2149
+ });
2057
2150
  }
2058
2151
  else {
2059
2152
  entry.count++;
@@ -2143,8 +2236,11 @@ export class App {
2143
2236
  this.assertSecureHookConfig(config.hooks);
2144
2237
  // Child apps share the parent's router/routes/etc. Disable docs auto-mount
2145
2238
  // on the child so it does not re-register the parent's `/openapi.json` and
2146
- // `/docs` routes (which would throw "Duplicate route").
2147
- const child = new App({ ...this.options, docs: false });
2239
+ // `/docs` routes (which would throw "Duplicate route"). Disable telemetry
2240
+ // too: spreading `this.options` would otherwise construct a second pair of
2241
+ // OTLP exporters, `unref`'d flush timers, and a duplicate `telemetry.otlp`
2242
+ // boot line per group/plugin. The parent's wiring is copied below.
2243
+ const child = new App({ ...this.options, docs: false, telemetry: false });
2148
2244
  child.router = this.router;
2149
2245
  child.routes = this.routes;
2150
2246
  child.webSocketRoutes = this.webSocketRoutes;
@@ -2152,7 +2248,10 @@ export class App {
2152
2248
  child.bootGuard = this.bootGuard;
2153
2249
  child.log = this.log;
2154
2250
  child.prefix = joinPath(this.prefix, prefix);
2155
- child.groupHooks = [...this.groupHooks, ...(config.hooks ? [config.hooks] : [])];
2251
+ child.groupHooks = [
2252
+ ...this.groupHooks,
2253
+ ...(config.hooks ? [config.hooks] : []),
2254
+ ];
2156
2255
  child.corsOriginAllows = corsOriginAllowsFromHooks(child.groupHooks);
2157
2256
  child.groupTags = [...this.groupTags, ...(config.tags ?? [])];
2158
2257
  child.groupAuth = config.auth ?? this.groupAuth;
@@ -2172,6 +2271,7 @@ export class App {
2172
2271
  child.shutdownListeners = this.shutdownListeners;
2173
2272
  child.pendingPlugins = this.pendingPlugins;
2174
2273
  child.pluginBootError = this.pluginBootError;
2274
+ child.telemetry = this.telemetry;
2175
2275
  register(child);
2176
2276
  return this;
2177
2277
  }
@@ -2204,7 +2304,8 @@ export class App {
2204
2304
  // "set only if absent" semantics mean the second installation would be
2205
2305
  // a silent no-op).
2206
2306
  if (hooks[SECURE_HEADERS_MARKER] === true) {
2207
- const autoIdx = this.groupHooks.findIndex((h) => h[AUTO_SECURE_HEADERS_MARKER] === true);
2307
+ const autoIdx = this.groupHooks.findIndex((h) => h[AUTO_SECURE_HEADERS_MARKER] ===
2308
+ true);
2208
2309
  if (autoIdx >= 0)
2209
2310
  this.groupHooks.splice(autoIdx, 1);
2210
2311
  }
@@ -2276,14 +2377,16 @@ export class App {
2276
2377
  * @throws Error if `key` is already decorated and `opts.override` is not `true`.
2277
2378
  */
2278
2379
  decorate(key, value, opts = {}) {
2279
- if (Object.prototype.hasOwnProperty.call(this.decorations, key) && opts.override !== true) {
2380
+ if (Object.prototype.hasOwnProperty.call(this.decorations, key) &&
2381
+ opts.override !== true) {
2280
2382
  // Namespace-protected decorators. Refuse to silently
2281
2383
  // shadow an existing decoration; emit a once-per-process warn naming
2282
2384
  // both decorators on the explicit-override path.
2283
2385
  throw new Error(`decorate(): key "${key}" is already decorated. ` +
2284
2386
  `Pass { override: true } to replace, or rename to avoid the collision.`);
2285
2387
  }
2286
- if (opts.override === true && Object.prototype.hasOwnProperty.call(this.decorations, key)) {
2388
+ if (opts.override === true &&
2389
+ Object.prototype.hasOwnProperty.call(this.decorations, key)) {
2287
2390
  this.log.warn({ event: "decorate.override", key }, `decorate("${key}") replaced an existing decoration.`);
2288
2391
  }
2289
2392
  const hadKey = Object.prototype.hasOwnProperty.call(this.decorations, key);
@@ -2355,7 +2458,10 @@ export class App {
2355
2458
  const dedupKey = name ? (seed ? `${name}#${seed}` : name) : undefined;
2356
2459
  const dependencies = descriptor?.dependencies ?? [];
2357
2460
  const stateful = descriptor?.stateful ?? false;
2358
- if (stateful && !name && this.isProduction() && this.options.secureDefaults !== false) {
2461
+ if (stateful &&
2462
+ !name &&
2463
+ this.isProduction() &&
2464
+ this.options.secureDefaults !== false) {
2359
2465
  throw new Error("register(): anonymous stateful plugin refused in production. " +
2360
2466
  "Declare { name } (and optional { seed }) so the plugin can be deduplicated.");
2361
2467
  }
@@ -2405,7 +2511,9 @@ export class App {
2405
2511
  throw err;
2406
2512
  });
2407
2513
  this.pendingPlugins.add(tracked);
2408
- void tracked.finally(() => this.pendingPlugins.delete(tracked)).catch(() => { });
2514
+ void tracked
2515
+ .finally(() => this.pendingPlugins.delete(tracked))
2516
+ .catch(() => { });
2409
2517
  }
2410
2518
  firePluginInstalled(event) {
2411
2519
  if (this.pluginInstalledListeners.length === 0)
@@ -2424,7 +2532,9 @@ export class App {
2424
2532
  this.log.error({ err, plugin: event.name }, "onPluginInstalled listener failed");
2425
2533
  }
2426
2534
  }
2427
- return promises.length > 0 ? Promise.all(promises).then(() => undefined) : undefined;
2535
+ return promises.length > 0
2536
+ ? Promise.all(promises).then(() => undefined)
2537
+ : undefined;
2428
2538
  }
2429
2539
  /**
2430
2540
  * Wait until every async plugin registered with {@link App.register} has
@@ -2643,7 +2753,9 @@ export class App {
2643
2753
  }
2644
2754
  if (coldPreBody !== undefined) {
2645
2755
  const guardResult = coldPreBody(ctx);
2646
- const guarded = isPromiseLike(guardResult) ? await guardResult : guardResult;
2756
+ const guarded = isPromiseLike(guardResult)
2757
+ ? await guardResult
2758
+ : guardResult;
2647
2759
  if (guarded instanceof Response) {
2648
2760
  copyContextHeaders(ctx, guarded);
2649
2761
  if (!guarded.headers.has("x-request-id")) {
@@ -2655,7 +2767,9 @@ export class App {
2655
2767
  }
2656
2768
  if (coldGuards !== undefined) {
2657
2769
  const guardResult = coldGuards(ctx);
2658
- const guarded = isPromiseLike(guardResult) ? await guardResult : guardResult;
2770
+ const guarded = isPromiseLike(guardResult)
2771
+ ? await guardResult
2772
+ : guardResult;
2659
2773
  if (guarded instanceof Response) {
2660
2774
  copyContextHeaders(ctx, guarded);
2661
2775
  if (!guarded.headers.has("x-request-id")) {
@@ -2703,7 +2817,7 @@ export class App {
2703
2817
  }
2704
2818
  throw new NotFoundError(`No route for ${request.method} ${pathname}`);
2705
2819
  }
2706
- const { def, hooks, mergedHooks: allHooks, hasFinalizeHook } = match.handler;
2820
+ const { def, hooks, mergedHooks: allHooks, hasFinalizeHook, } = match.handler;
2707
2821
  activeErrorHook = allHooks.onError;
2708
2822
  activeResponseHook = allHooks.onResponse;
2709
2823
  activeSendHook = allHooks.onSend;
@@ -2733,7 +2847,9 @@ export class App {
2733
2847
  Object.assign(state, routeDecorations);
2734
2848
  if (allHooks.preBody !== undefined) {
2735
2849
  const preBodyResult = allHooks.preBody(ctx);
2736
- const preBody = isPromiseLike(preBodyResult) ? await preBodyResult : preBodyResult;
2850
+ const preBody = isPromiseLike(preBodyResult)
2851
+ ? await preBodyResult
2852
+ : preBodyResult;
2737
2853
  const overriddenId = state.requestId;
2738
2854
  if (typeof overriddenId === "string" && overriddenId.length > 0) {
2739
2855
  requestId = overriddenId;
@@ -2757,7 +2873,9 @@ export class App {
2757
2873
  ctx = isPromiseLike(validatedCtx) ? await validatedCtx : validatedCtx;
2758
2874
  if (allHooks.beforeHandle !== undefined) {
2759
2875
  const beforeResult = allHooks.beforeHandle(ctx);
2760
- const before = isPromiseLike(beforeResult) ? await beforeResult : beforeResult;
2876
+ const before = isPromiseLike(beforeResult)
2877
+ ? await beforeResult
2878
+ : beforeResult;
2761
2879
  // Honor any request id override applied by middleware (e.g. the
2762
2880
  // `requestId()` Hooks bundle replaces the framework-generated value
2763
2881
  // with a trusted incoming header or a user-supplied generator).
@@ -2783,7 +2901,9 @@ export class App {
2783
2901
  let result = isPromiseLike(runResult) ? await runResult : runResult;
2784
2902
  if (allHooks.afterHandle !== undefined) {
2785
2903
  const afterResult = allHooks.afterHandle(ctx, result);
2786
- const afterReturn = isPromiseLike(afterResult) ? await afterResult : afterResult;
2904
+ const afterReturn = isPromiseLike(afterResult)
2905
+ ? await afterResult
2906
+ : afterResult;
2787
2907
  if (afterReturn !== undefined)
2788
2908
  result = afterReturn;
2789
2909
  }
@@ -2877,7 +2997,9 @@ export class App {
2877
2997
  // `err instanceof HttpError` first: the framework's own thrown
2878
2998
  // problem errors short-circuit before any signal/option lookup.
2879
2999
  const isHttp = err instanceof HttpError;
2880
- const disconnectCode = isHttp ? 0 : (this.options.disconnectStatusCode ?? 499);
3000
+ const disconnectCode = isHttp
3001
+ ? 0
3002
+ : (this.options.disconnectStatusCode ?? 499);
2881
3003
  if (disconnectCode > 0 && request.signal?.aborted === true) {
2882
3004
  log.info({ event: "request.disconnected", status: disconnectCode }, "Client disconnected before response was sent");
2883
3005
  const res = new Response(null, {
@@ -2935,7 +3057,9 @@ export class App {
2935
3057
  * @returns Fulfills with the `Response` produced by the matching handler.
2936
3058
  */
2937
3059
  request(input, init) {
2938
- const url = typeof input === "string" && input.startsWith("/") ? `http://test.local${input}` : input;
3060
+ const url = typeof input === "string" && input.startsWith("/")
3061
+ ? `http://test.local${input}`
3062
+ : input;
2939
3063
  const req = url instanceof Request ? url : new Request(url, init);
2940
3064
  return this.fetch(req);
2941
3065
  }
@@ -3092,7 +3216,9 @@ function inferOperationId(method, path) {
3092
3216
  const suffix = path
3093
3217
  .slice(1)
3094
3218
  .split("/")
3095
- .map((segment) => segment.startsWith(":") ? `By${capitalizeWords(segment.slice(1))}` : capitalizeWords(segment))
3219
+ .map((segment) => segment.startsWith(":")
3220
+ ? `By${capitalizeWords(segment.slice(1))}`
3221
+ : capitalizeWords(segment))
3096
3222
  .join("");
3097
3223
  return `${method.toLowerCase()}${suffix}`;
3098
3224
  }
@@ -3120,7 +3246,9 @@ function joinPath(a, b) {
3120
3246
  function healthRouteKey(request, trustProxyHeaders) {
3121
3247
  if (!trustProxyHeaders)
3122
3248
  return "global";
3123
- return request.headers.get("x-real-ip") ?? request.headers.get("fly-client-ip") ?? "global";
3249
+ return (request.headers.get("x-real-ip") ??
3250
+ request.headers.get("fly-client-ip") ??
3251
+ "global");
3124
3252
  }
3125
3253
  /** True when the app declared a trusted reverse-proxy posture. */
3126
3254
  function appTrustsProxyHeaders(options) {
@@ -3244,7 +3372,9 @@ export function topoSortExtensions(exts) {
3244
3372
  const bHeaders = b.responseHeaders;
3245
3373
  if (!bHeaders || bHeaders.length === 0)
3246
3374
  continue;
3247
- const overlap = bHeaders.map((h) => h.toLowerCase()).filter((h) => aSet.has(h));
3375
+ const overlap = bHeaders
3376
+ .map((h) => h.toLowerCase())
3377
+ .filter((h) => aSet.has(h));
3248
3378
  if (overlap.length === 0)
3249
3379
  continue;
3250
3380
  const declared = (a.before ?? []).includes(b.name) ||
@@ -3306,11 +3436,16 @@ function securityMarkersFromHooks(layers) {
3306
3436
  hasCsrf,
3307
3437
  hasAuth,
3308
3438
  cacheBeforeTenancy: cacheIndex !== -1 && tenancyIndex !== -1 && cacheIndex < tenancyIndex,
3309
- replayBeforeBudget: replayIndex !== -1 && budgetIndex !== -1 && replayIndex < budgetIndex ? replayName : null,
3439
+ replayBeforeBudget: replayIndex !== -1 && budgetIndex !== -1 && replayIndex < budgetIndex
3440
+ ? replayName
3441
+ : null,
3310
3442
  };
3311
3443
  }
3312
3444
  function isStateChangingMethod(method) {
3313
- return method === "POST" || method === "PUT" || method === "PATCH" || method === "DELETE";
3445
+ return (method === "POST" ||
3446
+ method === "PUT" ||
3447
+ method === "PATCH" ||
3448
+ method === "DELETE");
3314
3449
  }
3315
3450
  /**
3316
3451
  * Extract the pathname from a fully-qualified request URL without
@@ -3420,7 +3555,9 @@ function getOriginFast(url) {
3420
3555
  return url.slice(0, end);
3421
3556
  }
3422
3557
  function mergeHooks(layers) {
3423
- const pick = (key) => layers.map((h) => h[key]).filter((f) => typeof f === "function");
3558
+ const pick = (key) => layers
3559
+ .map((h) => h[key])
3560
+ .filter((f) => typeof f === "function");
3424
3561
  const requiredScopes = requiredScopesFromHooks(layers);
3425
3562
  const beforeHandle = mergeBeforeHandle(firstResponse(pick("beforeHandle")), requiredScopes);
3426
3563
  const hooks = {
@@ -3450,7 +3587,9 @@ function requiredScopesFromHooks(layers) {
3450
3587
  }
3451
3588
  function stampRequiredScopes(hooks, scopes) {
3452
3589
  if (scopes.length > 0) {
3453
- hooks[REQUIRE_SCOPES_HOOK_MARKER] = [...scopes];
3590
+ hooks[REQUIRE_SCOPES_HOOK_MARKER] = [
3591
+ ...scopes,
3592
+ ];
3454
3593
  }
3455
3594
  }
3456
3595
  function mergeBeforeHandle(beforeHandle, requiredScopes) {
@@ -3522,7 +3661,9 @@ function scalarConfigurationWithPreferredAuth(configuration, schemes) {
3522
3661
  return {
3523
3662
  ...(configuration ?? {}),
3524
3663
  authentication: {
3525
- ...(authentication && typeof authentication === "object" && !Array.isArray(authentication)
3664
+ ...(authentication &&
3665
+ typeof authentication === "object" &&
3666
+ !Array.isArray(authentication)
3526
3667
  ? authentication
3527
3668
  : {}),
3528
3669
  preferredSecurityScheme,
@@ -3559,7 +3700,9 @@ function finishFinalize(res, ctx, hooks, stripFingerprint) {
3559
3700
  return res;
3560
3701
  }
3561
3702
  function isPromiseLike(value) {
3562
- return value !== null && typeof value === "object" && typeof value.then === "function";
3703
+ return (value !== null &&
3704
+ typeof value === "object" &&
3705
+ typeof value.then === "function");
3563
3706
  }
3564
3707
  /**
3565
3708
  * Allocation-free finalizer for the common case (no `onSend`/`onResponse`
@@ -3789,7 +3932,10 @@ function validateContext(ctx, def, opts) {
3789
3932
  const rawParams = ctx.params;
3790
3933
  const buildHeaders = () => ctx.headers;
3791
3934
  const buildQuery = () => ctx.query;
3792
- const hasSchema = def.request?.params || def.request?.query || def.request?.headers || def.request?.body;
3935
+ const hasSchema = def.request?.params ||
3936
+ def.request?.query ||
3937
+ def.request?.headers ||
3938
+ def.request?.body;
3793
3939
  const finishContext = () => ctx;
3794
3940
  if (!hasSchema) {
3795
3941
  return finishContext();
@@ -3833,7 +3979,8 @@ function validateContext(ctx, def, opts) {
3833
3979
  const declared = request.headers.get("content-length");
3834
3980
  if (declared !== null) {
3835
3981
  const declaredBytes = Number(declared);
3836
- if (Number.isFinite(declaredBytes) && declaredBytes > opts.bodyLimitBytes) {
3982
+ if (Number.isFinite(declaredBytes) &&
3983
+ declaredBytes > opts.bodyLimitBytes) {
3837
3984
  throw new PayloadTooLargeError(opts.bodyLimitBytes);
3838
3985
  }
3839
3986
  }
@@ -3906,7 +4053,7 @@ function toIssues(issues) {
3906
4053
  return issues.map((i) => ({
3907
4054
  message: i.message,
3908
4055
  path: (i.path ?? [])
3909
- .map((p) => (typeof p === "object" && p && "key" in p ? p.key : p))
4056
+ .map((p) => typeof p === "object" && p && "key" in p ? p.key : p)
3910
4057
  .join("."),
3911
4058
  }));
3912
4059
  }
@@ -4037,7 +4184,8 @@ async function readBodySlow(req, ct, limit, multipart) {
4037
4184
  typeof v.arrayBuffer === "function";
4038
4185
  if (isFile) {
4039
4186
  files++;
4040
- if (multipart?.maxFileBytes !== undefined && v.size > multipart.maxFileBytes) {
4187
+ if (multipart?.maxFileBytes !== undefined &&
4188
+ v.size > multipart.maxFileBytes) {
4041
4189
  throw new PayloadTooLargeError(multipart.maxFileBytes);
4042
4190
  }
4043
4191
  }
@@ -4266,7 +4414,8 @@ function withTimeout(p, ms, request) {
4266
4414
  const OMIT_STACK_IN_LOG = Symbol.for("daloyjs.error.omitStackInLog");
4267
4415
  function serializeErr(err) {
4268
4416
  if (err instanceof Error) {
4269
- if (err[OMIT_STACK_IN_LOG] === true) {
4417
+ if (err[OMIT_STACK_IN_LOG] ===
4418
+ true) {
4270
4419
  return { name: err.name, message: err.message };
4271
4420
  }
4272
4421
  return { name: err.name, message: err.message, stack: err.stack };