@daloyjs/core 0.34.3 → 0.35.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 +151 -32
- package/dist/adapters/bun.d.ts +0 -1
- package/dist/adapters/bun.js +0 -1
- package/dist/adapters/cloudflare.d.ts +0 -1
- package/dist/adapters/cloudflare.js +0 -1
- package/dist/adapters/deno.d.ts +0 -1
- package/dist/adapters/deno.js +0 -1
- package/dist/adapters/fastly.d.ts +0 -1
- package/dist/adapters/fastly.js +0 -1
- package/dist/adapters/lambda.d.ts +0 -1
- package/dist/adapters/lambda.js +0 -1
- package/dist/adapters/node.d.ts +0 -1
- package/dist/adapters/node.js +175 -35
- package/dist/adapters/vercel.d.ts +0 -1
- package/dist/adapters/vercel.js +0 -1
- package/dist/app.d.ts +18 -1
- package/dist/app.js +396 -118
- package/dist/banner.d.ts +0 -1
- package/dist/banner.js +0 -1
- package/dist/cli.d.ts +32 -1
- package/dist/cli.js +62 -5
- package/dist/client.d.ts +0 -1
- package/dist/client.js +0 -1
- package/dist/combine.d.ts +0 -1
- package/dist/combine.js +0 -1
- package/dist/compression.d.ts +0 -1
- package/dist/compression.js +0 -1
- package/dist/config.d.ts +0 -1
- package/dist/config.js +0 -1
- package/dist/conn-info.d.ts +0 -1
- package/dist/conn-info.js +0 -1
- package/dist/contract.d.ts +0 -1
- package/dist/contract.js +0 -1
- package/dist/cookie.d.ts +0 -1
- package/dist/cookie.js +0 -1
- package/dist/dependency.d.ts +0 -1
- package/dist/dependency.js +0 -1
- package/dist/discriminator.d.ts +0 -1
- package/dist/discriminator.js +0 -1
- package/dist/docs.d.ts +0 -1
- package/dist/docs.js +0 -1
- package/dist/errors.d.ts +0 -1
- package/dist/errors.js +0 -1
- package/dist/etag.d.ts +0 -1
- package/dist/etag.js +0 -1
- package/dist/fetch-guard.d.ts +50 -6
- package/dist/fetch-guard.js +83 -18
- package/dist/hashing.d.ts +0 -1
- package/dist/hashing.js +0 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +3 -3
- package/dist/ip-restriction.d.ts +0 -1
- package/dist/ip-restriction.js +0 -1
- package/dist/jwk.d.ts +0 -1
- package/dist/jwk.js +0 -1
- package/dist/jwt.d.ts +15 -1
- package/dist/jwt.js +56 -3
- package/dist/load-shedding.d.ts +0 -1
- package/dist/load-shedding.js +0 -1
- package/dist/logger.d.ts +0 -1
- package/dist/logger.js +0 -1
- package/dist/middleware.d.ts +78 -1
- package/dist/middleware.js +150 -18
- package/dist/multipart.d.ts +0 -1
- package/dist/multipart.js +0 -1
- package/dist/openapi.d.ts +0 -1
- package/dist/openapi.js +0 -1
- package/dist/rate-limit-redis.d.ts +0 -1
- package/dist/rate-limit-redis.js +0 -1
- package/dist/router.d.ts +0 -1
- package/dist/router.js +0 -1
- package/dist/safe-redirect.d.ts +91 -0
- package/dist/safe-redirect.js +168 -0
- package/dist/sbom.cdx.json +13 -9
- package/dist/sbom.spdx.json +5 -5
- package/dist/schema.d.ts +0 -1
- package/dist/schema.js +0 -1
- package/dist/security-schemes.d.ts +0 -1
- package/dist/security-schemes.js +0 -1
- package/dist/security.d.ts +134 -3
- package/dist/security.js +265 -6
- package/dist/session.d.ts +0 -1
- package/dist/session.js +0 -1
- package/dist/streaming.d.ts +0 -1
- package/dist/streaming.js +0 -1
- package/dist/subdomains.d.ts +0 -1
- package/dist/subdomains.js +0 -1
- package/dist/time-claims.d.ts +0 -1
- package/dist/time-claims.js +0 -1
- package/dist/tracing.d.ts +0 -1
- package/dist/tracing.js +0 -1
- package/dist/types.d.ts +0 -1
- package/dist/types.js +0 -1
- package/dist/websocket.d.ts +0 -1
- package/dist/websocket.js +0 -1
- package/package.json +35 -15
- package/dist/adapters/bun.d.ts.map +0 -1
- package/dist/adapters/bun.js.map +0 -1
- package/dist/adapters/cloudflare.d.ts.map +0 -1
- package/dist/adapters/cloudflare.js.map +0 -1
- package/dist/adapters/deno.d.ts.map +0 -1
- package/dist/adapters/deno.js.map +0 -1
- package/dist/adapters/fastly.d.ts.map +0 -1
- package/dist/adapters/fastly.js.map +0 -1
- package/dist/adapters/lambda.d.ts.map +0 -1
- package/dist/adapters/lambda.js.map +0 -1
- package/dist/adapters/node.d.ts.map +0 -1
- package/dist/adapters/node.js.map +0 -1
- package/dist/adapters/vercel.d.ts.map +0 -1
- package/dist/adapters/vercel.js.map +0 -1
- package/dist/app.d.ts.map +0 -1
- package/dist/app.js.map +0 -1
- package/dist/banner.d.ts.map +0 -1
- package/dist/banner.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js.map +0 -1
- package/dist/combine.d.ts.map +0 -1
- package/dist/combine.js.map +0 -1
- package/dist/compression.d.ts.map +0 -1
- package/dist/compression.js.map +0 -1
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/conn-info.d.ts.map +0 -1
- package/dist/conn-info.js.map +0 -1
- package/dist/contract.d.ts.map +0 -1
- package/dist/contract.js.map +0 -1
- package/dist/cookie.d.ts.map +0 -1
- package/dist/cookie.js.map +0 -1
- package/dist/dependency.d.ts.map +0 -1
- package/dist/dependency.js.map +0 -1
- package/dist/discriminator.d.ts.map +0 -1
- package/dist/discriminator.js.map +0 -1
- package/dist/docs.d.ts.map +0 -1
- package/dist/docs.js.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/etag.d.ts.map +0 -1
- package/dist/etag.js.map +0 -1
- package/dist/fetch-guard.d.ts.map +0 -1
- package/dist/fetch-guard.js.map +0 -1
- package/dist/hashing.d.ts.map +0 -1
- package/dist/hashing.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/ip-restriction.d.ts.map +0 -1
- package/dist/ip-restriction.js.map +0 -1
- package/dist/jwk.d.ts.map +0 -1
- package/dist/jwk.js.map +0 -1
- package/dist/jwt.d.ts.map +0 -1
- package/dist/jwt.js.map +0 -1
- package/dist/load-shedding.d.ts.map +0 -1
- package/dist/load-shedding.js.map +0 -1
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js.map +0 -1
- package/dist/middleware.d.ts.map +0 -1
- package/dist/middleware.js.map +0 -1
- package/dist/multipart.d.ts.map +0 -1
- package/dist/multipart.js.map +0 -1
- package/dist/openapi.d.ts.map +0 -1
- package/dist/openapi.js.map +0 -1
- package/dist/rate-limit-redis.d.ts.map +0 -1
- package/dist/rate-limit-redis.js.map +0 -1
- package/dist/router.d.ts.map +0 -1
- package/dist/router.js.map +0 -1
- package/dist/schema.d.ts.map +0 -1
- package/dist/schema.js.map +0 -1
- package/dist/security-schemes.d.ts.map +0 -1
- package/dist/security-schemes.js.map +0 -1
- package/dist/security.d.ts.map +0 -1
- package/dist/security.js.map +0 -1
- package/dist/session.d.ts.map +0 -1
- package/dist/session.js.map +0 -1
- package/dist/streaming.d.ts.map +0 -1
- package/dist/streaming.js.map +0 -1
- package/dist/subdomains.d.ts.map +0 -1
- package/dist/subdomains.js.map +0 -1
- package/dist/time-claims.d.ts.map +0 -1
- package/dist/time-claims.js.map +0 -1
- package/dist/tracing.d.ts.map +0 -1
- package/dist/tracing.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/websocket.d.ts.map +0 -1
- package/dist/websocket.js.map +0 -1
package/dist/app.js
CHANGED
|
@@ -79,6 +79,16 @@ const DEFAULTS = {
|
|
|
79
79
|
requestTimeoutMs: 30_000,
|
|
80
80
|
validateResponses: true,
|
|
81
81
|
};
|
|
82
|
+
const TEXT_ENCODER = new TextEncoder();
|
|
83
|
+
/**
|
|
84
|
+
* Internal Symbol used to stash the raw Uint8Array (or null) body on a
|
|
85
|
+
* `Response` produced by {@link serializeResult}. Adapters that want to
|
|
86
|
+
* avoid the cost of `await response.arrayBuffer()` for buffer-backed
|
|
87
|
+
* responses can read this property and write the bytes directly. The
|
|
88
|
+
* Symbol is intentionally module-private (not exported) to keep this an
|
|
89
|
+
* implementation detail — userland code should never depend on it.
|
|
90
|
+
*/
|
|
91
|
+
export const DALOY_RAW_BODY = Symbol.for("daloyjs.response.rawBody");
|
|
82
92
|
/**
|
|
83
93
|
* Contract-first HTTP application.
|
|
84
94
|
*
|
|
@@ -181,6 +191,25 @@ export class App {
|
|
|
181
191
|
* dashboards see the misconfiguration without flooding on every retry.
|
|
182
192
|
*/
|
|
183
193
|
trustProxyWarned = false;
|
|
194
|
+
/**
|
|
195
|
+
* Cached merge of `options.hooks` only. Used on the cold 404/405 path
|
|
196
|
+
* and as the baseline for cross-origin guard decisions when no route
|
|
197
|
+
* matches.
|
|
198
|
+
*/
|
|
199
|
+
_globalHooksCache;
|
|
200
|
+
_globalCorsAllowsCache;
|
|
201
|
+
get globalHooks() {
|
|
202
|
+
if (this._globalHooksCache === undefined) {
|
|
203
|
+
this._globalHooksCache = mergeHooks([this.options.hooks ?? {}]);
|
|
204
|
+
}
|
|
205
|
+
return this._globalHooksCache;
|
|
206
|
+
}
|
|
207
|
+
get globalCorsAllows() {
|
|
208
|
+
if (this._globalCorsAllowsCache === undefined) {
|
|
209
|
+
this._globalCorsAllowsCache = corsOriginAllowsFromHooks([this.options.hooks ?? {}]);
|
|
210
|
+
}
|
|
211
|
+
return this._globalCorsAllowsCache;
|
|
212
|
+
}
|
|
184
213
|
constructor(options = {}) {
|
|
185
214
|
this.options = {
|
|
186
215
|
validateResponses: options.validateResponses ?? DEFAULTS.validateResponses,
|
|
@@ -377,7 +406,7 @@ export class App {
|
|
|
377
406
|
* routes whose hook chain includes a `cors()` policy allowing the origin
|
|
378
407
|
* are unaffected.
|
|
379
408
|
*/
|
|
380
|
-
assertCrossOriginAllowed(request,
|
|
409
|
+
assertCrossOriginAllowed(request, requestUrl, method, corsOriginAllows) {
|
|
381
410
|
if (this.options.secureDefaults === false)
|
|
382
411
|
return;
|
|
383
412
|
if (this.options.corsCrossOriginGuard === false)
|
|
@@ -399,7 +428,8 @@ export class App {
|
|
|
399
428
|
// Malformed Origin header — refuse loudly.
|
|
400
429
|
throw new ForbiddenError("Cross-origin state-changing request rejected: malformed Origin header.");
|
|
401
430
|
}
|
|
402
|
-
|
|
431
|
+
const reqOrigin = typeof requestUrl === "string" ? new URL(requestUrl).origin : requestUrl.origin;
|
|
432
|
+
if (originUrl.origin === reqOrigin)
|
|
403
433
|
return;
|
|
404
434
|
if (corsOriginAllows.some((allows) => allows(origin)))
|
|
405
435
|
return;
|
|
@@ -728,11 +758,18 @@ export class App {
|
|
|
728
758
|
const sources = [...this.groupHooks, def.hooks ?? {}];
|
|
729
759
|
const hooks = mergeHooks(sources);
|
|
730
760
|
const corsOriginAllows = corsOriginAllowsFromHooks(sources);
|
|
761
|
+
const globalHookLayer = this.options.hooks ?? {};
|
|
762
|
+
const mergedHooks = mergeHooks([globalHookLayer, ...sources]);
|
|
763
|
+
const hasFinalizeHook = mergedHooks.onSend !== undefined || mergedHooks.onResponse !== undefined;
|
|
764
|
+
const fullCorsOriginAllows = [
|
|
765
|
+
...corsOriginAllowsFromHooks([globalHookLayer]),
|
|
766
|
+
...corsOriginAllows,
|
|
767
|
+
];
|
|
731
768
|
const securityMarkers = securityMarkersFromHooks([
|
|
732
|
-
|
|
769
|
+
globalHookLayer,
|
|
733
770
|
...sources,
|
|
734
771
|
]);
|
|
735
|
-
this.router.add(def.method, fullPath, { def: merged, hooks, corsOriginAllows }, def.operationId);
|
|
772
|
+
this.router.add(def.method, fullPath, { def: merged, hooks, mergedHooks, hasFinalizeHook, corsOriginAllows, fullCorsOriginAllows }, def.operationId);
|
|
736
773
|
this.routes.push(merged);
|
|
737
774
|
this.routeSecurityMarkers.push({
|
|
738
775
|
method: merged.method,
|
|
@@ -1426,7 +1463,7 @@ export class App {
|
|
|
1426
1463
|
});
|
|
1427
1464
|
const stripFingerprint = this.options.stripServerHeaders !== false;
|
|
1428
1465
|
let ctx;
|
|
1429
|
-
const globalHooks =
|
|
1466
|
+
const globalHooks = this.globalHooks;
|
|
1430
1467
|
let activeErrorHook = globalHooks.onError;
|
|
1431
1468
|
let activeResponseHook = globalHooks.onResponse;
|
|
1432
1469
|
let activeSendHook = globalHooks.onSend;
|
|
@@ -1435,26 +1472,36 @@ export class App {
|
|
|
1435
1472
|
assertNoReservedInternalHeaders(request.headers);
|
|
1436
1473
|
this.assertTrustProxyConfigured(request);
|
|
1437
1474
|
this.assertBootGuards();
|
|
1438
|
-
|
|
1439
|
-
|
|
1475
|
+
if (globalHooks.onRequest !== undefined) {
|
|
1476
|
+
const onRequestResult = globalHooks.onRequest(request);
|
|
1477
|
+
if (isPromiseLike(onRequestResult))
|
|
1478
|
+
await onRequestResult;
|
|
1479
|
+
}
|
|
1440
1480
|
const method = request.method;
|
|
1481
|
+
const requestUrl = request.url;
|
|
1482
|
+
const pathname = getPathnameFast(requestUrl);
|
|
1483
|
+
let url;
|
|
1484
|
+
const getUrl = () => (url ??= new URL(requestUrl));
|
|
1441
1485
|
const headFallback = method === "HEAD";
|
|
1442
|
-
const match = this.router.find(method,
|
|
1443
|
-
(headFallback ? this.router.find("GET",
|
|
1486
|
+
const match = this.router.find(method, pathname) ??
|
|
1487
|
+
(headFallback ? this.router.find("GET", pathname) : undefined);
|
|
1444
1488
|
// Hide internal routes from the public adapter surface. The router
|
|
1445
1489
|
// still finds them so app.inject() can dispatch normally, but
|
|
1446
1490
|
// app.fetch() responds 404 to avoid leaking existence.
|
|
1447
1491
|
const internalHidden = match?.handler.def.internal === true && opts.allowInternal !== true;
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1492
|
+
if (match && !internalHidden) {
|
|
1493
|
+
this.assertCrossOriginAllowed(request, requestUrl, method, match.handler.fullCorsOriginAllows);
|
|
1494
|
+
}
|
|
1495
|
+
else {
|
|
1496
|
+
this.assertCrossOriginAllowed(request, requestUrl, method, [...this.globalCorsAllows, ...this.corsOriginAllows]);
|
|
1497
|
+
}
|
|
1452
1498
|
if (!match || internalHidden) {
|
|
1499
|
+
const url404 = getUrl();
|
|
1453
1500
|
if (internalHidden) {
|
|
1454
1501
|
// Don't leak existence via 405/Allow header. Always 404.
|
|
1455
|
-
throw new NotFoundError(`No route for ${request.method} ${
|
|
1502
|
+
throw new NotFoundError(`No route for ${request.method} ${url404.pathname}`);
|
|
1456
1503
|
}
|
|
1457
|
-
const rawAllowed = this.router.allowedMethods(
|
|
1504
|
+
const rawAllowed = this.router.allowedMethods(url404.pathname);
|
|
1458
1505
|
// Filter out methods whose route definitions are marked
|
|
1459
1506
|
// `internal: true` unless the caller explicitly opted in via
|
|
1460
1507
|
// app.inject(). This prevents 405/Allow from leaking the
|
|
@@ -1462,13 +1509,13 @@ export class App {
|
|
|
1462
1509
|
const allowed = opts.allowInternal
|
|
1463
1510
|
? rawAllowed
|
|
1464
1511
|
: rawAllowed.filter((m) => {
|
|
1465
|
-
const candidate = this.router.find(m,
|
|
1512
|
+
const candidate = this.router.find(m, url404.pathname);
|
|
1466
1513
|
return candidate?.handler.def.internal !== true;
|
|
1467
1514
|
});
|
|
1468
1515
|
ctx = {
|
|
1469
1516
|
request,
|
|
1470
1517
|
params: {},
|
|
1471
|
-
query: Object.fromEntries(
|
|
1518
|
+
query: Object.fromEntries(url404.searchParams.entries()),
|
|
1472
1519
|
headers: headersToObject(request.headers),
|
|
1473
1520
|
body: undefined,
|
|
1474
1521
|
state: { ...this.decorations, requestId, log },
|
|
@@ -1492,10 +1539,12 @@ export class App {
|
|
|
1492
1539
|
this.options.hooks ?? {},
|
|
1493
1540
|
...this.groupHooks,
|
|
1494
1541
|
]);
|
|
1495
|
-
const
|
|
1542
|
+
const interceptedResult = preflightHooks.beforeHandle?.(synthCtx);
|
|
1543
|
+
const intercepted = isPromiseLike(interceptedResult) ? await interceptedResult : interceptedResult;
|
|
1496
1544
|
if (intercepted instanceof Response) {
|
|
1497
1545
|
copyContextHeaders(synthCtx, intercepted);
|
|
1498
|
-
|
|
1546
|
+
const fin = finalizeResponse(intercepted, synthCtx, preflightHooks, stripFingerprint);
|
|
1547
|
+
return isPromiseLike(fin) ? await fin : fin;
|
|
1499
1548
|
}
|
|
1500
1549
|
const res = new Response(null, {
|
|
1501
1550
|
status: 204,
|
|
@@ -1503,35 +1552,61 @@ export class App {
|
|
|
1503
1552
|
});
|
|
1504
1553
|
copyContextHeaders(synthCtx, res);
|
|
1505
1554
|
res.headers.set("x-request-id", requestId);
|
|
1506
|
-
|
|
1555
|
+
const fin2 = finalizeResponse(res, synthCtx, preflightHooks, stripFingerprint);
|
|
1556
|
+
return isPromiseLike(fin2) ? await fin2 : fin2;
|
|
1507
1557
|
}
|
|
1508
1558
|
throw new MethodNotAllowedError(allowed);
|
|
1509
1559
|
}
|
|
1510
|
-
throw new NotFoundError(`No route for ${request.method} ${
|
|
1560
|
+
throw new NotFoundError(`No route for ${request.method} ${url404.pathname}`);
|
|
1511
1561
|
}
|
|
1512
|
-
const { def, hooks } = match.handler;
|
|
1513
|
-
const allHooks = mergeHooks([globalHooks, hooks]);
|
|
1562
|
+
const { def, hooks, mergedHooks: allHooks, hasFinalizeHook } = match.handler;
|
|
1514
1563
|
activeErrorHook = allHooks.onError;
|
|
1515
1564
|
activeResponseHook = allHooks.onResponse;
|
|
1516
1565
|
activeSendHook = allHooks.onSend;
|
|
1517
|
-
|
|
1518
|
-
|
|
1566
|
+
if (hooks.onRequest !== undefined) {
|
|
1567
|
+
const routeOnRequestResult = hooks.onRequest(request);
|
|
1568
|
+
if (isPromiseLike(routeOnRequestResult))
|
|
1569
|
+
await routeOnRequestResult;
|
|
1570
|
+
}
|
|
1571
|
+
ctx = await buildContext(request, getUrl, match.params, def, this.options);
|
|
1519
1572
|
Object.assign(ctx.state, this.decorations, { requestId, log });
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1573
|
+
if (allHooks.beforeHandle !== undefined) {
|
|
1574
|
+
const beforeResult = allHooks.beforeHandle(ctx);
|
|
1575
|
+
const before = isPromiseLike(beforeResult) ? await beforeResult : beforeResult;
|
|
1576
|
+
if (before instanceof Response) {
|
|
1577
|
+
copyContextHeaders(ctx, before);
|
|
1578
|
+
if (!before.headers.has("x-request-id"))
|
|
1579
|
+
before.headers.set("x-request-id", requestId);
|
|
1580
|
+
if (hasFinalizeHook) {
|
|
1581
|
+
const fin = finalizeResponse(before, ctx, allHooks, stripFingerprint);
|
|
1582
|
+
return isPromiseLike(fin) ? await fin : fin;
|
|
1583
|
+
}
|
|
1584
|
+
return finalizeFast(before, stripFingerprint);
|
|
1585
|
+
}
|
|
1525
1586
|
}
|
|
1526
|
-
|
|
1587
|
+
const runResult = this.options.mockMode
|
|
1527
1588
|
? mockResponseFor(def)
|
|
1528
|
-
:
|
|
1529
|
-
|
|
1530
|
-
if (
|
|
1531
|
-
|
|
1532
|
-
|
|
1589
|
+
: runHandler(def, ctx, this.options.requestTimeoutMs);
|
|
1590
|
+
let result = isPromiseLike(runResult) ? await runResult : runResult;
|
|
1591
|
+
if (allHooks.afterHandle !== undefined) {
|
|
1592
|
+
const afterResult = allHooks.afterHandle(ctx, result);
|
|
1593
|
+
const afterReturn = isPromiseLike(afterResult) ? await afterResult : afterResult;
|
|
1594
|
+
if (afterReturn !== undefined)
|
|
1595
|
+
result = afterReturn;
|
|
1596
|
+
}
|
|
1597
|
+
const serializeResultRes = serializeResult(result, def, this.options.validateResponses ?? true);
|
|
1598
|
+
let response = isPromiseLike(serializeResultRes) ? await serializeResultRes : serializeResultRes;
|
|
1533
1599
|
copyContextHeaders(ctx, response);
|
|
1534
|
-
|
|
1600
|
+
if (!response.headers.has("x-request-id"))
|
|
1601
|
+
response.headers.set("x-request-id", requestId);
|
|
1602
|
+
let finalized;
|
|
1603
|
+
if (hasFinalizeHook) {
|
|
1604
|
+
const fin = finalizeResponse(response, ctx, allHooks, stripFingerprint);
|
|
1605
|
+
finalized = isPromiseLike(fin) ? await fin : fin;
|
|
1606
|
+
}
|
|
1607
|
+
else {
|
|
1608
|
+
finalized = finalizeFast(response, stripFingerprint);
|
|
1609
|
+
}
|
|
1535
1610
|
if (method === "HEAD") {
|
|
1536
1611
|
return new Response(null, {
|
|
1537
1612
|
status: finalized.status,
|
|
@@ -1903,6 +1978,34 @@ function isStateChangingMethod(method) {
|
|
|
1903
1978
|
method === "PATCH" ||
|
|
1904
1979
|
method === "DELETE");
|
|
1905
1980
|
}
|
|
1981
|
+
/**
|
|
1982
|
+
* Extract the pathname from a fully-qualified request URL without
|
|
1983
|
+
* constructing a `URL` object. ~10x cheaper than `new URL(...).pathname`
|
|
1984
|
+
* and used on the dispatch hot path; full `URL` parsing is deferred until
|
|
1985
|
+
* something actually reads `searchParams`. Mirrors Hono's `getPath`
|
|
1986
|
+
* approach.
|
|
1987
|
+
*
|
|
1988
|
+
* Handles the standard `http(s)://host[:port]/path[?query][#hash]` shape
|
|
1989
|
+
* produced by the Node adapter and the Fetch standard. Falls back to a
|
|
1990
|
+
* `URL` parse for inputs that don't match (e.g. opaque schemes), so
|
|
1991
|
+
* correctness is preserved even when the fast path doesn't apply.
|
|
1992
|
+
*/
|
|
1993
|
+
function getPathnameFast(url) {
|
|
1994
|
+
const schemeEnd = url.indexOf("://");
|
|
1995
|
+
if (schemeEnd === -1)
|
|
1996
|
+
return new URL(url).pathname;
|
|
1997
|
+
const pathStart = url.indexOf("/", schemeEnd + 3);
|
|
1998
|
+
if (pathStart === -1)
|
|
1999
|
+
return "/";
|
|
2000
|
+
let end = url.length;
|
|
2001
|
+
const q = url.indexOf("?", pathStart);
|
|
2002
|
+
if (q !== -1)
|
|
2003
|
+
end = q;
|
|
2004
|
+
const h = url.indexOf("#", pathStart);
|
|
2005
|
+
if (h !== -1 && h < end)
|
|
2006
|
+
end = h;
|
|
2007
|
+
return url.slice(pathStart, end);
|
|
2008
|
+
}
|
|
1906
2009
|
function mergeHooks(layers) {
|
|
1907
2010
|
const pick = (key) => layers
|
|
1908
2011
|
.map((h) => h[key])
|
|
@@ -1941,6 +2044,11 @@ function stampRequiredScopes(hooks, scopes) {
|
|
|
1941
2044
|
function mergeBeforeHandle(beforeHandle, requiredScopes) {
|
|
1942
2045
|
if (!beforeHandle && requiredScopes.length === 0)
|
|
1943
2046
|
return undefined;
|
|
2047
|
+
// No scope aggregation needed → return the inner fn as-is so the dispatch
|
|
2048
|
+
// hot path can use the single-fn fast path and avoid an extra closure +
|
|
2049
|
+
// microtask wrapper around a possibly-sync beforeHandle.
|
|
2050
|
+
if (requiredScopes.length === 0)
|
|
2051
|
+
return beforeHandle;
|
|
1944
2052
|
return async (ctx) => {
|
|
1945
2053
|
if (requiredScopes.length > 0) {
|
|
1946
2054
|
const state = ctx.state;
|
|
@@ -1958,6 +2066,8 @@ function mergeBeforeHandle(beforeHandle, requiredScopes) {
|
|
|
1958
2066
|
function responsePipeline(fns) {
|
|
1959
2067
|
if (fns.length === 0)
|
|
1960
2068
|
return undefined;
|
|
2069
|
+
if (fns.length === 1)
|
|
2070
|
+
return fns[0];
|
|
1961
2071
|
return async (res, ctx) => {
|
|
1962
2072
|
let current = res;
|
|
1963
2073
|
for (const fn of fns) {
|
|
@@ -1968,19 +2078,59 @@ function responsePipeline(fns) {
|
|
|
1968
2078
|
return current;
|
|
1969
2079
|
};
|
|
1970
2080
|
}
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
const
|
|
2081
|
+
function finalizeResponse(res, ctx, hooks, stripFingerprint = true) {
|
|
2082
|
+
let final = res;
|
|
2083
|
+
const finish = (f) => {
|
|
2084
|
+
if (stripFingerprint) {
|
|
2085
|
+
f.headers.delete("server");
|
|
2086
|
+
f.headers.delete("x-powered-by");
|
|
2087
|
+
}
|
|
2088
|
+
if (hooks.onResponse !== undefined) {
|
|
2089
|
+
const onResponseResult = hooks.onResponse(f);
|
|
2090
|
+
if (isPromiseLike(onResponseResult)) {
|
|
2091
|
+
return onResponseResult.then(() => f);
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
return f;
|
|
2095
|
+
};
|
|
2096
|
+
if (hooks.onSend !== undefined) {
|
|
2097
|
+
const sentResult = hooks.onSend(res, ctx);
|
|
2098
|
+
if (isPromiseLike(sentResult)) {
|
|
2099
|
+
return sentResult.then((sent) => {
|
|
2100
|
+
if (sent instanceof Response)
|
|
2101
|
+
final = sent;
|
|
2102
|
+
return finish(final);
|
|
2103
|
+
});
|
|
2104
|
+
}
|
|
2105
|
+
else {
|
|
2106
|
+
if (sentResult instanceof Response)
|
|
2107
|
+
final = sentResult;
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
return finish(final);
|
|
2111
|
+
}
|
|
2112
|
+
function isPromiseLike(value) {
|
|
2113
|
+
return value !== null && typeof value === "object" && typeof value.then === "function";
|
|
2114
|
+
}
|
|
2115
|
+
/**
|
|
2116
|
+
* Allocation-free finalizer for the common case (no `onSend`/`onResponse`
|
|
2117
|
+
* hooks). Avoids the two `await undefined` microtasks per request that
|
|
2118
|
+
* dominate sub-millisecond dispatch overhead in the no-middleware
|
|
2119
|
+
* benchmark. Behaviour parity with {@link finalizeResponse}: only the
|
|
2120
|
+
* server-fingerprint headers are stripped.
|
|
2121
|
+
*/
|
|
2122
|
+
function finalizeFast(res, stripFingerprint) {
|
|
1974
2123
|
if (stripFingerprint) {
|
|
1975
|
-
|
|
1976
|
-
|
|
2124
|
+
res.headers.delete("server");
|
|
2125
|
+
res.headers.delete("x-powered-by");
|
|
1977
2126
|
}
|
|
1978
|
-
|
|
1979
|
-
return final;
|
|
2127
|
+
return res;
|
|
1980
2128
|
}
|
|
1981
2129
|
function chain(fns) {
|
|
1982
2130
|
if (fns.length === 0)
|
|
1983
2131
|
return undefined;
|
|
2132
|
+
if (fns.length === 1)
|
|
2133
|
+
return fns[0];
|
|
1984
2134
|
return (async (...args) => {
|
|
1985
2135
|
for (const fn of fns)
|
|
1986
2136
|
await fn(...args);
|
|
@@ -1989,6 +2139,8 @@ function chain(fns) {
|
|
|
1989
2139
|
function firstResponse(fns) {
|
|
1990
2140
|
if (fns.length === 0)
|
|
1991
2141
|
return undefined;
|
|
2142
|
+
if (fns.length === 1)
|
|
2143
|
+
return fns[0];
|
|
1992
2144
|
return (async (...args) => {
|
|
1993
2145
|
for (const fn of fns) {
|
|
1994
2146
|
const r = await fn(...args);
|
|
@@ -2001,6 +2153,8 @@ function firstResponse(fns) {
|
|
|
2001
2153
|
function pipeline(fns) {
|
|
2002
2154
|
if (fns.length === 0)
|
|
2003
2155
|
return undefined;
|
|
2156
|
+
if (fns.length === 1)
|
|
2157
|
+
return fns[0];
|
|
2004
2158
|
return (async (ctx, value) => {
|
|
2005
2159
|
let v = value;
|
|
2006
2160
|
for (const fn of fns) {
|
|
@@ -2011,7 +2165,37 @@ function pipeline(fns) {
|
|
|
2011
2165
|
return v;
|
|
2012
2166
|
});
|
|
2013
2167
|
}
|
|
2168
|
+
/**
|
|
2169
|
+
* Marker stamped on `ctx.set` once the `headers` getter has been
|
|
2170
|
+
* materialised. {@link copyContextHeaders} consults this to skip the
|
|
2171
|
+
* forEach loop entirely on requests where no middleware or handler ever
|
|
2172
|
+
* touched `ctx.set.headers` — the default case for the no-middleware
|
|
2173
|
+
* benchmark.
|
|
2174
|
+
*/
|
|
2175
|
+
const SET_HEADERS_TOUCHED = Symbol.for("daloyjs.app.setHeadersTouched");
|
|
2176
|
+
function makeLazySet() {
|
|
2177
|
+
let h;
|
|
2178
|
+
const set = {};
|
|
2179
|
+
Object.defineProperty(set, "headers", {
|
|
2180
|
+
get() {
|
|
2181
|
+
if (h === undefined) {
|
|
2182
|
+
h = new Headers();
|
|
2183
|
+
set[SET_HEADERS_TOUCHED] = true;
|
|
2184
|
+
}
|
|
2185
|
+
return h;
|
|
2186
|
+
},
|
|
2187
|
+
set(v) {
|
|
2188
|
+
h = v;
|
|
2189
|
+
set[SET_HEADERS_TOUCHED] = true;
|
|
2190
|
+
},
|
|
2191
|
+
enumerable: true,
|
|
2192
|
+
configurable: true,
|
|
2193
|
+
});
|
|
2194
|
+
return set;
|
|
2195
|
+
}
|
|
2014
2196
|
function copyContextHeaders(ctx, res) {
|
|
2197
|
+
if (!ctx.set[SET_HEADERS_TOUCHED])
|
|
2198
|
+
return;
|
|
2015
2199
|
ctx.set.headers.forEach((v, k) => {
|
|
2016
2200
|
if (!res.headers.has(k))
|
|
2017
2201
|
res.headers.set(k, v);
|
|
@@ -2020,49 +2204,100 @@ function copyContextHeaders(ctx, res) {
|
|
|
2020
2204
|
function hasRequestSchema(request, key) {
|
|
2021
2205
|
return !!request && !!request[key];
|
|
2022
2206
|
}
|
|
2023
|
-
|
|
2024
|
-
const set =
|
|
2025
|
-
const
|
|
2026
|
-
const
|
|
2207
|
+
function buildContext(request, getUrl, rawParams, def, opts) {
|
|
2208
|
+
const set = makeLazySet();
|
|
2209
|
+
const hasHeadersSchema = !!def.request?.headers;
|
|
2210
|
+
const hasQuerySchema = !!def.request?.query;
|
|
2211
|
+
let headersObj;
|
|
2212
|
+
let queryObj;
|
|
2213
|
+
const buildHeaders = () => (headersObj ??= headersToObject(request.headers));
|
|
2214
|
+
const buildQuery = () => (queryObj ??= queryToObject(getUrl().searchParams));
|
|
2027
2215
|
let params = rawParams;
|
|
2028
|
-
let query
|
|
2029
|
-
let headers
|
|
2216
|
+
let query;
|
|
2217
|
+
let headers;
|
|
2030
2218
|
let body = undefined;
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2219
|
+
const hasSchema = def.request?.params || def.request?.query || def.request?.headers || def.request?.body;
|
|
2220
|
+
const finishContext = () => {
|
|
2221
|
+
const ctx = {
|
|
2222
|
+
request,
|
|
2223
|
+
params,
|
|
2224
|
+
body,
|
|
2225
|
+
state: {},
|
|
2226
|
+
set,
|
|
2227
|
+
};
|
|
2228
|
+
if (hasQuerySchema) {
|
|
2229
|
+
ctx.query = query;
|
|
2230
|
+
}
|
|
2231
|
+
else {
|
|
2232
|
+
defineLazyContextProperty(ctx, "query", buildQuery);
|
|
2233
|
+
}
|
|
2234
|
+
if (hasHeadersSchema) {
|
|
2235
|
+
ctx.headers = headers;
|
|
2236
|
+
}
|
|
2237
|
+
else {
|
|
2238
|
+
defineLazyContextProperty(ctx, "headers", buildHeaders);
|
|
2239
|
+
}
|
|
2240
|
+
return ctx;
|
|
2241
|
+
};
|
|
2242
|
+
if (!hasSchema) {
|
|
2243
|
+
return finishContext();
|
|
2244
|
+
}
|
|
2245
|
+
return (async () => {
|
|
2246
|
+
if (def.request?.params) {
|
|
2247
|
+
const r = await validate(def.request.params, rawParams);
|
|
2248
|
+
if (r.issues)
|
|
2249
|
+
throw new ValidationError("params", toIssues(r.issues));
|
|
2250
|
+
params = r.value;
|
|
2251
|
+
}
|
|
2252
|
+
if (hasQuerySchema) {
|
|
2253
|
+
const r = await validate(def.request.query, buildQuery());
|
|
2254
|
+
if (r.issues)
|
|
2255
|
+
throw new ValidationError("query", toIssues(r.issues));
|
|
2256
|
+
query = r.value;
|
|
2257
|
+
}
|
|
2258
|
+
if (hasHeadersSchema) {
|
|
2259
|
+
const r = await validate(def.request.headers, buildHeaders());
|
|
2260
|
+
if (r.issues)
|
|
2261
|
+
throw new ValidationError("headers", toIssues(r.issues));
|
|
2262
|
+
headers = r.value;
|
|
2263
|
+
}
|
|
2264
|
+
if (def.request?.body) {
|
|
2265
|
+
const ct = (request.headers.get("content-type") ?? "").toLowerCase();
|
|
2266
|
+
const allowed = def.accepts ?? opts.allowedContentTypes ?? [
|
|
2267
|
+
"application/json",
|
|
2268
|
+
"application/x-www-form-urlencoded",
|
|
2269
|
+
"multipart/form-data",
|
|
2270
|
+
];
|
|
2271
|
+
if (!allowed.some((a) => ct.includes(a))) {
|
|
2272
|
+
throw new UnsupportedMediaTypeError(ct || "(none)", allowed);
|
|
2273
|
+
}
|
|
2274
|
+
const raw = await readBody(request, ct, opts.bodyLimitBytes, opts.multipart);
|
|
2275
|
+
const r = await validate(def.request.body, raw);
|
|
2276
|
+
if (r.issues)
|
|
2277
|
+
throw new ValidationError("body", toIssues(r.issues));
|
|
2278
|
+
body = r.value;
|
|
2279
|
+
}
|
|
2280
|
+
return finishContext();
|
|
2281
|
+
})();
|
|
2282
|
+
}
|
|
2283
|
+
function defineLazyContextProperty(ctx, key, build) {
|
|
2284
|
+
let initialized = false;
|
|
2285
|
+
let value;
|
|
2286
|
+
Object.defineProperty(ctx, key, {
|
|
2287
|
+
get() {
|
|
2288
|
+
if (!initialized) {
|
|
2289
|
+
value = build();
|
|
2290
|
+
initialized = true;
|
|
2291
|
+
}
|
|
2292
|
+
return value;
|
|
2293
|
+
},
|
|
2294
|
+
set(next) {
|
|
2295
|
+
value = next;
|
|
2296
|
+
initialized = true;
|
|
2297
|
+
},
|
|
2298
|
+
enumerable: true,
|
|
2299
|
+
configurable: true,
|
|
2300
|
+
});
|
|
2066
2301
|
}
|
|
2067
2302
|
function headersToObject(h) {
|
|
2068
2303
|
const o = {};
|
|
@@ -2155,41 +2390,84 @@ async function readBody(req, ct, limit, multipart) {
|
|
|
2155
2390
|
const bytes = await readBodyLimited(req, limit);
|
|
2156
2391
|
return new TextDecoder().decode(bytes);
|
|
2157
2392
|
}
|
|
2158
|
-
|
|
2393
|
+
function serializeResult(result, def, validateResponses) {
|
|
2159
2394
|
const spec = def.responses[result.status];
|
|
2160
2395
|
if (!spec) {
|
|
2161
2396
|
throw new InternalError(`Handler returned status ${result.status} which is not declared in responses for ${def.method} ${def.path}`);
|
|
2162
2397
|
}
|
|
2398
|
+
const finish = () => {
|
|
2399
|
+
const headers = new Headers(result.headers);
|
|
2400
|
+
const explicitCt = headers.get("content-type");
|
|
2401
|
+
const treatAsJson = !explicitCt || explicitCt.includes("application/json");
|
|
2402
|
+
if (!explicitCt)
|
|
2403
|
+
headers.set("content-type", "application/json");
|
|
2404
|
+
let body;
|
|
2405
|
+
let rawBody = null;
|
|
2406
|
+
let isStream = false;
|
|
2407
|
+
if (result.body === undefined || result.body === null) {
|
|
2408
|
+
body = null;
|
|
2409
|
+
}
|
|
2410
|
+
else if (!treatAsJson && typeof result.body === "string") {
|
|
2411
|
+
const bytes = TEXT_ENCODER.encode(result.body);
|
|
2412
|
+
setContentLength(headers, bytes.byteLength);
|
|
2413
|
+
body = bytes;
|
|
2414
|
+
rawBody = bytes;
|
|
2415
|
+
}
|
|
2416
|
+
else if (!treatAsJson && result.body instanceof Uint8Array) {
|
|
2417
|
+
setContentLength(headers, result.body.byteLength);
|
|
2418
|
+
body = result.body;
|
|
2419
|
+
rawBody = result.body;
|
|
2420
|
+
}
|
|
2421
|
+
else if (!treatAsJson && result.body instanceof ArrayBuffer) {
|
|
2422
|
+
setContentLength(headers, result.body.byteLength);
|
|
2423
|
+
body = result.body;
|
|
2424
|
+
rawBody = new Uint8Array(result.body);
|
|
2425
|
+
}
|
|
2426
|
+
else if (!treatAsJson && result.body instanceof ReadableStream) {
|
|
2427
|
+
body = result.body;
|
|
2428
|
+
isStream = true;
|
|
2429
|
+
}
|
|
2430
|
+
else {
|
|
2431
|
+
const bytes = TEXT_ENCODER.encode(JSON.stringify(result.body));
|
|
2432
|
+
setContentLength(headers, bytes.byteLength);
|
|
2433
|
+
body = bytes;
|
|
2434
|
+
rawBody = bytes;
|
|
2435
|
+
}
|
|
2436
|
+
const response = new Response(body, { status: result.status, headers });
|
|
2437
|
+
if (!isStream) {
|
|
2438
|
+
response[DALOY_RAW_BODY] = rawBody;
|
|
2439
|
+
}
|
|
2440
|
+
return response;
|
|
2441
|
+
};
|
|
2163
2442
|
if (validateResponses && spec.body) {
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2443
|
+
// Call the Standard Schema validator directly so a sync validator
|
|
2444
|
+
// stays on the sync fast path. The public `validate()` helper always
|
|
2445
|
+
// returns a Promise (stable API surface), but here we own the schema
|
|
2446
|
+
// and can branch on whether validation actually needed to suspend.
|
|
2447
|
+
const r = spec.body["~standard"].validate(result.body);
|
|
2448
|
+
if (isPromiseLike(r)) {
|
|
2449
|
+
return r.then((resolved) => {
|
|
2450
|
+
if (resolved.issues) {
|
|
2451
|
+
throw new InternalError(`Response body for ${def.method} ${def.path} failed schema validation: ${resolved.issues
|
|
2452
|
+
.map((i) => i.message)
|
|
2453
|
+
.join("; ")}`);
|
|
2454
|
+
}
|
|
2455
|
+
return finish();
|
|
2456
|
+
});
|
|
2457
|
+
}
|
|
2458
|
+
else {
|
|
2459
|
+
if (r.issues) {
|
|
2460
|
+
throw new InternalError(`Response body for ${def.method} ${def.path} failed schema validation: ${r.issues
|
|
2461
|
+
.map((i) => i.message)
|
|
2462
|
+
.join("; ")}`);
|
|
2463
|
+
}
|
|
2169
2464
|
}
|
|
2170
2465
|
}
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
if (!
|
|
2175
|
-
headers.set("content-
|
|
2176
|
-
let body;
|
|
2177
|
-
if (result.body === undefined || result.body === null) {
|
|
2178
|
-
body = null;
|
|
2179
|
-
}
|
|
2180
|
-
else if (!treatAsJson &&
|
|
2181
|
-
(typeof result.body === "string" ||
|
|
2182
|
-
result.body instanceof Uint8Array ||
|
|
2183
|
-
result.body instanceof ArrayBuffer)) {
|
|
2184
|
-
body = result.body;
|
|
2185
|
-
}
|
|
2186
|
-
else if (!treatAsJson && result.body instanceof ReadableStream) {
|
|
2187
|
-
body = result.body;
|
|
2188
|
-
}
|
|
2189
|
-
else {
|
|
2190
|
-
body = JSON.stringify(result.body);
|
|
2191
|
-
}
|
|
2192
|
-
return new Response(body, { status: result.status, headers });
|
|
2466
|
+
return finish();
|
|
2467
|
+
}
|
|
2468
|
+
function setContentLength(headers, byteLength) {
|
|
2469
|
+
if (!headers.has("content-length"))
|
|
2470
|
+
headers.set("content-length", String(byteLength));
|
|
2193
2471
|
}
|
|
2194
2472
|
function mockResponseFor(def) {
|
|
2195
2473
|
const statuses = Object.keys(def.responses).map(Number).sort();
|
|
@@ -2203,10 +2481,11 @@ function mockResponseFor(def) {
|
|
|
2203
2481
|
return { status, body: example };
|
|
2204
2482
|
}
|
|
2205
2483
|
function runHandler(def, ctx, requestTimeoutMs) {
|
|
2206
|
-
const
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2484
|
+
const result = def.handler(ctx);
|
|
2485
|
+
if (requestTimeoutMs === 0 || !isPromiseLike(result)) {
|
|
2486
|
+
return result;
|
|
2487
|
+
}
|
|
2488
|
+
return withTimeout(result, requestTimeoutMs);
|
|
2210
2489
|
}
|
|
2211
2490
|
function withTimeout(p, ms) {
|
|
2212
2491
|
return new Promise((resolve, reject) => {
|
|
@@ -2347,4 +2626,3 @@ function readHostPackageJsonInfo() {
|
|
|
2347
2626
|
export function _resetPackageJsonCacheForTests() {
|
|
2348
2627
|
PACKAGE_JSON_CACHE.value = undefined;
|
|
2349
2628
|
}
|
|
2350
|
-
//# sourceMappingURL=app.js.map
|