@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/README.md +2 -2
- package/dist/adapters/cloudflare.d.ts +8 -1
- package/dist/adapters/cloudflare.js +15 -1
- package/dist/adapters/lambda.js +15 -5
- package/dist/adapters/vercel.d.ts +5 -0
- package/dist/adapters/vercel.js +27 -2
- package/dist/app.d.ts +16 -10
- package/dist/app.js +218 -69
- package/dist/combine.js +14 -8
- package/dist/metrics.d.ts +23 -9
- package/dist/metrics.js +33 -10
- package/dist/otlp.d.ts +31 -5
- package/dist/otlp.js +99 -37
- package/dist/sbom.cdx.json +9 -9
- package/dist/sbom.spdx.json +5 -5
- package/package.json +1 -1
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([
|
|
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([
|
|
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 &&
|
|
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
|
|
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
|
|
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 &&
|
|
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"
|
|
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" &&
|
|
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"
|
|
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"
|
|
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 ||
|
|
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 &&
|
|
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] =
|
|
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
|
|
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
|
|
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: {
|
|
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
|
|
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
|
|
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: {
|
|
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: {
|
|
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([
|
|
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 ||
|
|
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 ||
|
|
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.
|
|
1806
|
-
*
|
|
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
|
|
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
|
|
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, {
|
|
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({
|
|
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
|
|
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
|
|
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, {
|
|
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
|
|
2037
|
-
|
|
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, {
|
|
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
|
-
|
|
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 = [
|
|
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] ===
|
|
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) &&
|
|
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 &&
|
|
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 &&
|
|
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
|
|
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
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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
|
|
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("/")
|
|
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(":")
|
|
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") ??
|
|
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
|
|
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
|
|
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" ||
|
|
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
|
|
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] = [
|
|
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 &&
|
|
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 &&
|
|
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 ||
|
|
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) &&
|
|
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) =>
|
|
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 &&
|
|
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] ===
|
|
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 };
|