@daloyjs/core 1.0.0-rc.2 → 1.0.0-rc.4
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 +96 -30
- package/dist/adapters/vercel.d.ts +8 -10
- package/dist/adapters/vercel.js +0 -2
- package/dist/app.d.ts +125 -8
- package/dist/app.js +244 -183
- package/dist/cli.js +41 -1
- package/dist/client.d.ts +28 -11
- package/dist/client.js +29 -6
- package/dist/combine.d.ts +11 -11
- package/dist/combine.js +90 -47
- package/dist/docs.d.ts +5 -9
- package/dist/docs.js +36 -14
- package/dist/idempotency.js +2 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/internal-response.d.ts +15 -0
- package/dist/internal-response.js +27 -0
- package/dist/jwk.d.ts +11 -7
- package/dist/jwk.js +11 -7
- package/dist/mcp.js +11 -6
- package/dist/middleware.d.ts +48 -7
- package/dist/middleware.js +96 -40
- package/dist/mtls.d.ts +6 -5
- package/dist/mtls.js +3 -9
- package/dist/openapi.js +1 -1
- package/dist/pagination.js +4 -1
- package/dist/response-cache.js +2 -1
- package/dist/safe-redirect.d.ts +4 -1
- package/dist/safe-redirect.js +4 -1
- package/dist/sbom.cdx.json +9 -9
- package/dist/sbom.spdx.json +5 -5
- package/dist/security.d.ts +21 -0
- package/dist/security.js +89 -0
- package/dist/tenancy.d.ts +2 -2
- package/dist/types.d.ts +85 -20
- package/dist/types.js +16 -1
- package/package.json +7 -1
package/dist/pagination.js
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* @since 0.37.0
|
|
29
29
|
*/
|
|
30
30
|
import { BadRequestError } from "./errors.js";
|
|
31
|
+
import { safeJsonParseLimited } from "./security.js";
|
|
31
32
|
import { isForbiddenObjectKey } from "./security.js";
|
|
32
33
|
/**
|
|
33
34
|
* Hard cap on the length of an encoded cursor string accepted by
|
|
@@ -91,7 +92,9 @@ export function decodeCursor(cursor) {
|
|
|
91
92
|
}
|
|
92
93
|
let parsed;
|
|
93
94
|
try {
|
|
94
|
-
|
|
95
|
+
// Use limited parse for structural safety (wide/deep cursors).
|
|
96
|
+
// Cursors are length-capped at 4k so even default limits are very generous here.
|
|
97
|
+
parsed = safeJsonParseLimited(json, 1000, 20);
|
|
95
98
|
}
|
|
96
99
|
catch {
|
|
97
100
|
throw new BadRequestError("Malformed pagination cursor.");
|
package/dist/response-cache.js
CHANGED
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
* @module
|
|
40
40
|
* @since 0.37.0
|
|
41
41
|
*/
|
|
42
|
+
import { markSchemaValidatedResponse } from "./internal-response.js";
|
|
42
43
|
/** Internal `ctx.state` key carrying the pending cache key between hooks. */
|
|
43
44
|
const PENDING_STATE_KEY = "__responseCachePending";
|
|
44
45
|
/**
|
|
@@ -183,7 +184,7 @@ function buildResponseFromCache(entry, outcome, statusHeaderName, isHead) {
|
|
|
183
184
|
if (statusHeaderName)
|
|
184
185
|
headers.set(statusHeaderName, outcome);
|
|
185
186
|
const body = isHead || entry.body === "" ? null : base64ToBytes(entry.body);
|
|
186
|
-
return new Response(body, { status: entry.status, headers });
|
|
187
|
+
return markSchemaValidatedResponse(new Response(body, { status: entry.status, headers }));
|
|
187
188
|
}
|
|
188
189
|
function isPromiseLike(value) {
|
|
189
190
|
return (value !== null &&
|
package/dist/safe-redirect.d.ts
CHANGED
|
@@ -31,7 +31,10 @@
|
|
|
31
31
|
* ```ts
|
|
32
32
|
* import { safeRedirect } from "@daloyjs/core";
|
|
33
33
|
*
|
|
34
|
-
* app.get("/login/callback",
|
|
34
|
+
* app.get("/login/callback", {
|
|
35
|
+
* acknowledgeNoResponseBodySchema: true,
|
|
36
|
+
* responses: { 303: {} },
|
|
37
|
+
* }, (ctx) => {
|
|
35
38
|
* const next = new URL(ctx.request.url).searchParams.get("next") ?? "/";
|
|
36
39
|
* return safeRedirect(next, {
|
|
37
40
|
* allowedPaths: ["/", "/dashboard", "/account"],
|
package/dist/safe-redirect.js
CHANGED
|
@@ -31,7 +31,10 @@
|
|
|
31
31
|
* ```ts
|
|
32
32
|
* import { safeRedirect } from "@daloyjs/core";
|
|
33
33
|
*
|
|
34
|
-
* app.get("/login/callback",
|
|
34
|
+
* app.get("/login/callback", {
|
|
35
|
+
* acknowledgeNoResponseBodySchema: true,
|
|
36
|
+
* responses: { 303: {} },
|
|
37
|
+
* }, (ctx) => {
|
|
35
38
|
* const next = new URL(ctx.request.url).searchParams.get("next") ?? "/";
|
|
36
39
|
* return safeRedirect(next, {
|
|
37
40
|
* allowedPaths: ["/", "/dashboard", "/account"],
|
package/dist/sbom.cdx.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"bomFormat": "CycloneDX",
|
|
3
3
|
"specVersion": "1.5",
|
|
4
|
-
"serialNumber": "urn:uuid:
|
|
4
|
+
"serialNumber": "urn:uuid:6aa5f3fd-e017-5e4a-b206-49dc6d141768",
|
|
5
5
|
"version": 1,
|
|
6
6
|
"metadata": {
|
|
7
|
-
"timestamp": "2026-07-
|
|
7
|
+
"timestamp": "2026-07-12T20:39:15.454Z",
|
|
8
8
|
"tools": [
|
|
9
9
|
{
|
|
10
10
|
"vendor": "DaloyJS",
|
|
11
11
|
"name": "daloy-generate-sbom",
|
|
12
|
-
"version": "1.0.0-rc.
|
|
12
|
+
"version": "1.0.0-rc.4"
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"authors": [
|
|
@@ -19,11 +19,11 @@
|
|
|
19
19
|
],
|
|
20
20
|
"component": {
|
|
21
21
|
"type": "library",
|
|
22
|
-
"bom-ref": "pkg:npm/@daloyjs/core@1.0.0-rc.
|
|
22
|
+
"bom-ref": "pkg:npm/@daloyjs/core@1.0.0-rc.4",
|
|
23
23
|
"name": "@daloyjs/core",
|
|
24
|
-
"version": "1.0.0-rc.
|
|
24
|
+
"version": "1.0.0-rc.4",
|
|
25
25
|
"description": "DaloyJS is a runtime-portable, contract-first TypeScript web framework with built-in OpenAPI (Hey API), typed client generation, large-scale maintainability, and security-first defaults. Hono-grade portability, Elysia-grade DX, FastAPI-grade docs, Fastify-grade ops — distributed via pnpm.",
|
|
26
|
-
"purl": "pkg:npm/@daloyjs/core@1.0.0-rc.
|
|
26
|
+
"purl": "pkg:npm/@daloyjs/core@1.0.0-rc.4",
|
|
27
27
|
"licenses": [
|
|
28
28
|
{
|
|
29
29
|
"license": {
|
|
@@ -46,9 +46,9 @@
|
|
|
46
46
|
}
|
|
47
47
|
],
|
|
48
48
|
"swid": {
|
|
49
|
-
"tagId": "swidtag--daloyjs-core-1.0.0-rc.
|
|
49
|
+
"tagId": "swidtag--daloyjs-core-1.0.0-rc.4",
|
|
50
50
|
"name": "@daloyjs/core",
|
|
51
|
-
"version": "1.0.0-rc.
|
|
51
|
+
"version": "1.0.0-rc.4",
|
|
52
52
|
"tagVersion": 0,
|
|
53
53
|
"patch": false
|
|
54
54
|
}
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"components": [],
|
|
58
58
|
"dependencies": [
|
|
59
59
|
{
|
|
60
|
-
"ref": "pkg:npm/@daloyjs/core@1.0.0-rc.
|
|
60
|
+
"ref": "pkg:npm/@daloyjs/core@1.0.0-rc.4",
|
|
61
61
|
"dependsOn": []
|
|
62
62
|
}
|
|
63
63
|
]
|
package/dist/sbom.spdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"spdxVersion": "SPDX-2.3",
|
|
3
3
|
"dataLicense": "CC0-1.0",
|
|
4
4
|
"SPDXID": "SPDXRef-DOCUMENT",
|
|
5
|
-
"name": "@daloyjs/core-1.0.0-rc.
|
|
6
|
-
"documentNamespace": "https://github.com/daloyjs/daloy/sbom/@daloyjs/core-1.0.0-rc.
|
|
5
|
+
"name": "@daloyjs/core-1.0.0-rc.4",
|
|
6
|
+
"documentNamespace": "https://github.com/daloyjs/daloy/sbom/@daloyjs/core-1.0.0-rc.4-6aa5f3fd-e017-5e4a-b206-49dc6d141768",
|
|
7
7
|
"creationInfo": {
|
|
8
|
-
"created": "2026-07-
|
|
8
|
+
"created": "2026-07-12T20:39:15.454Z",
|
|
9
9
|
"creators": [
|
|
10
10
|
"Tool: daloy-generate-sbom",
|
|
11
11
|
"Organization: DaloyJS"
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
{
|
|
17
17
|
"SPDXID": "SPDXRef-Package--daloyjs-core",
|
|
18
18
|
"name": "@daloyjs/core",
|
|
19
|
-
"versionInfo": "1.0.0-rc.
|
|
19
|
+
"versionInfo": "1.0.0-rc.4",
|
|
20
20
|
"downloadLocation": "https://github.com/daloyjs/daloy",
|
|
21
21
|
"filesAnalyzed": false,
|
|
22
22
|
"licenseConcluded": "MIT",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
{
|
|
28
28
|
"referenceCategory": "PACKAGE-MANAGER",
|
|
29
29
|
"referenceType": "purl",
|
|
30
|
-
"referenceLocator": "pkg:npm/@daloyjs/core@1.0.0-rc.
|
|
30
|
+
"referenceLocator": "pkg:npm/@daloyjs/core@1.0.0-rc.4"
|
|
31
31
|
}
|
|
32
32
|
]
|
|
33
33
|
}
|
package/dist/security.d.ts
CHANGED
|
@@ -460,3 +460,24 @@ export declare function hasMongoOperatorKeys(value: unknown): boolean;
|
|
|
460
460
|
* @since 0.35.0
|
|
461
461
|
*/
|
|
462
462
|
export declare function assertNoMongoOperators(value: unknown): void;
|
|
463
|
+
/**
|
|
464
|
+
* Like {@link safeJsonParse}, but additionally enforces structural limits
|
|
465
|
+
* (total object keys across the tree and maximum nesting depth) to defend
|
|
466
|
+
* against wide-object / hash-flood and deep-nesting DoS payloads that stay
|
|
467
|
+
* under the byte cap.
|
|
468
|
+
*
|
|
469
|
+
* The limits are checked with a single pre-parse text scan
|
|
470
|
+
* ({@link assertJsonTextStructure}), so an oversized structure is rejected
|
|
471
|
+
* *before* it is parsed or allocated. On success the text is handed to
|
|
472
|
+
* {@link safeJsonParse}, which applies the same prototype-pollution stripping
|
|
473
|
+
* as every other body parser — so the security posture is identical, only the
|
|
474
|
+
* structural bounds are added.
|
|
475
|
+
*
|
|
476
|
+
* @param text - JSON text. Empty string returns `undefined`.
|
|
477
|
+
* @param maxKeys - Maximum total object keys (0 or negative = unlimited).
|
|
478
|
+
* @param maxDepth - Maximum nesting depth (0 or negative = unlimited).
|
|
479
|
+
* @returns Parsed value (with dangerous keys stripped).
|
|
480
|
+
* @throws {BadRequestError} on invalid JSON or when limits are exceeded.
|
|
481
|
+
* @since 1.0.0
|
|
482
|
+
*/
|
|
483
|
+
export declare function safeJsonParseLimited(text: string, maxKeys?: number, maxDepth?: number): unknown;
|
package/dist/security.js
CHANGED
|
@@ -876,3 +876,92 @@ export function assertNoMongoOperators(value) {
|
|
|
876
876
|
throw new BadRequestError("Operator-prefixed key rejected");
|
|
877
877
|
}
|
|
878
878
|
}
|
|
879
|
+
/**
|
|
880
|
+
* Enforces structural limits by scanning raw JSON *text* in a single
|
|
881
|
+
* allocation-free pass, before the value is parsed — so a wide-object or
|
|
882
|
+
* deep-nesting bomb is rejected without ever being materialized into memory.
|
|
883
|
+
*
|
|
884
|
+
* Object keys are counted via `:` delimiters that sit outside string literals:
|
|
885
|
+
* every JSON object member is `"key": value`, and array elements carry no
|
|
886
|
+
* colon, so the number of structural colons equals the total object-key count
|
|
887
|
+
* across the whole tree. Nesting depth tracks the running `{`/`[` … `}`/`]`
|
|
888
|
+
* balance, again ignoring characters inside strings. Both checks short-circuit
|
|
889
|
+
* the instant a limit is exceeded, giving bounded-time rejection.
|
|
890
|
+
*
|
|
891
|
+
* This mirrors the accounting of the older recursive object walk but costs one
|
|
892
|
+
* tight character loop instead of a second full traversal of the parsed graph,
|
|
893
|
+
* keeping the common (small-body) path close to a bare `JSON.parse`.
|
|
894
|
+
*
|
|
895
|
+
* @param text - The raw JSON text (already known to be non-empty).
|
|
896
|
+
* @param maxKeys - Maximum total object keys (`<= 0` disables the key check).
|
|
897
|
+
* @param maxDepth - Maximum nesting depth (`<= 0` disables the depth check).
|
|
898
|
+
* @throws {BadRequestError} When the key or depth limit is exceeded.
|
|
899
|
+
* @internal
|
|
900
|
+
*/
|
|
901
|
+
function assertJsonTextStructure(text, maxKeys, maxDepth) {
|
|
902
|
+
let depth = 0;
|
|
903
|
+
let keyCount = 0;
|
|
904
|
+
let inString = false;
|
|
905
|
+
let escaped = false;
|
|
906
|
+
for (let i = 0; i < text.length; i++) {
|
|
907
|
+
const c = text.charCodeAt(i);
|
|
908
|
+
if (inString) {
|
|
909
|
+
if (escaped)
|
|
910
|
+
escaped = false;
|
|
911
|
+
else if (c === 0x5c /* \ */)
|
|
912
|
+
escaped = true;
|
|
913
|
+
else if (c === 0x22 /* " */)
|
|
914
|
+
inString = false;
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
switch (c) {
|
|
918
|
+
case 0x22 /* " */:
|
|
919
|
+
inString = true;
|
|
920
|
+
break;
|
|
921
|
+
case 0x7b /* { */:
|
|
922
|
+
case 0x5b /* [ */:
|
|
923
|
+
depth++;
|
|
924
|
+
if (maxDepth > 0 && depth > maxDepth) {
|
|
925
|
+
throw new BadRequestError("JSON exceeds maximum nesting depth");
|
|
926
|
+
}
|
|
927
|
+
break;
|
|
928
|
+
case 0x7d /* } */:
|
|
929
|
+
case 0x5d /* ] */:
|
|
930
|
+
depth--;
|
|
931
|
+
break;
|
|
932
|
+
case 0x3a /* : */:
|
|
933
|
+
if (maxKeys > 0 && ++keyCount > maxKeys) {
|
|
934
|
+
throw new BadRequestError("JSON exceeds maximum key count");
|
|
935
|
+
}
|
|
936
|
+
break;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Like {@link safeJsonParse}, but additionally enforces structural limits
|
|
942
|
+
* (total object keys across the tree and maximum nesting depth) to defend
|
|
943
|
+
* against wide-object / hash-flood and deep-nesting DoS payloads that stay
|
|
944
|
+
* under the byte cap.
|
|
945
|
+
*
|
|
946
|
+
* The limits are checked with a single pre-parse text scan
|
|
947
|
+
* ({@link assertJsonTextStructure}), so an oversized structure is rejected
|
|
948
|
+
* *before* it is parsed or allocated. On success the text is handed to
|
|
949
|
+
* {@link safeJsonParse}, which applies the same prototype-pollution stripping
|
|
950
|
+
* as every other body parser — so the security posture is identical, only the
|
|
951
|
+
* structural bounds are added.
|
|
952
|
+
*
|
|
953
|
+
* @param text - JSON text. Empty string returns `undefined`.
|
|
954
|
+
* @param maxKeys - Maximum total object keys (0 or negative = unlimited).
|
|
955
|
+
* @param maxDepth - Maximum nesting depth (0 or negative = unlimited).
|
|
956
|
+
* @returns Parsed value (with dangerous keys stripped).
|
|
957
|
+
* @throws {BadRequestError} on invalid JSON or when limits are exceeded.
|
|
958
|
+
* @since 1.0.0
|
|
959
|
+
*/
|
|
960
|
+
export function safeJsonParseLimited(text, maxKeys = 10_000, maxDepth = 50) {
|
|
961
|
+
if (text.length === 0)
|
|
962
|
+
return undefined;
|
|
963
|
+
if (maxKeys > 0 || maxDepth > 0) {
|
|
964
|
+
assertJsonTextStructure(text, maxKeys, maxDepth);
|
|
965
|
+
}
|
|
966
|
+
return safeJsonParse(text);
|
|
967
|
+
}
|
package/dist/tenancy.d.ts
CHANGED
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
*
|
|
49
49
|
* @since 0.42.0
|
|
50
50
|
*/
|
|
51
|
-
import type { BaseContext, Hooks } from "./types.js";
|
|
51
|
+
import type { BaseContext, Hooks, PreBodyContext } from "./types.js";
|
|
52
52
|
/**
|
|
53
53
|
* Resolves a raw (un-normalized) tenant id from a request, or a nullish value
|
|
54
54
|
* when this strategy cannot determine one. Resolvers are tried in order and
|
|
@@ -249,4 +249,4 @@ export interface TenantScopeOptions {
|
|
|
249
249
|
* @returns A key function suitable for `keyGenerator` / `scope`.
|
|
250
250
|
* @since 0.42.0
|
|
251
251
|
*/
|
|
252
|
-
export declare function tenantScope(opts?: TenantScopeOptions): (ctx: BaseContext<any, any>) => string;
|
|
252
|
+
export declare function tenantScope(opts?: TenantScopeOptions): (ctx: BaseContext<any, any> | PreBodyContext<any>) => string;
|
package/dist/types.d.ts
CHANGED
|
@@ -103,7 +103,8 @@ export type InferRequest<R extends RequestSchemas | undefined, P extends string>
|
|
|
103
103
|
/**
|
|
104
104
|
* Describes a single HTTP response variant declared by a route.
|
|
105
105
|
*
|
|
106
|
-
* - `description` —
|
|
106
|
+
* - `description` — optional OpenAPI documentation; omitted values receive a
|
|
107
|
+
* stable `HTTP <status> response` fallback.
|
|
107
108
|
* - `body` — Standard-Schema validator for the response body; when
|
|
108
109
|
* present, DaloyJS validates the handler's return value against it
|
|
109
110
|
* (controlled by `AppOptions.validateResponses`).
|
|
@@ -114,8 +115,8 @@ export type InferRequest<R extends RequestSchemas | undefined, P extends string>
|
|
|
114
115
|
* @since 0.1.0
|
|
115
116
|
*/
|
|
116
117
|
export interface ResponseSpec {
|
|
117
|
-
/** Human-readable description
|
|
118
|
-
description
|
|
118
|
+
/** Human-readable OpenAPI description. Defaults to `HTTP <status> response`. */
|
|
119
|
+
description?: string;
|
|
119
120
|
/** Response-body validator; handler return values are checked against it when `AppOptions.validateResponses` is on. */
|
|
120
121
|
body?: StandardSchemaV1;
|
|
121
122
|
/** Documented response headers keyed by header name; surfaced in the OpenAPI document. */
|
|
@@ -273,20 +274,53 @@ export interface BaseContext<P extends string, R extends RequestSchemas | undefi
|
|
|
273
274
|
headers: Headers;
|
|
274
275
|
};
|
|
275
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* Minimal request context exposed before request-body I/O or schema
|
|
279
|
+
* validation. It is intended for cheap perimeter decisions such as bearer,
|
|
280
|
+
* API-key, mTLS, and header-only JWT authentication.
|
|
281
|
+
*
|
|
282
|
+
* Path, query, and header values are raw at this phase, and `body` is always
|
|
283
|
+
* `undefined`. Middleware that requires validated input or body bytes belongs
|
|
284
|
+
* in {@link Hooks.beforeHandle}.
|
|
285
|
+
*
|
|
286
|
+
* @typeParam P - Literal route path used to infer raw path-parameter names.
|
|
287
|
+
* @since 1.0.0
|
|
288
|
+
*/
|
|
289
|
+
export interface PreBodyContext<P extends string = string> {
|
|
290
|
+
/** Original web-standard Request. Its body stream has not been consumed by DaloyJS. */
|
|
291
|
+
request: Request;
|
|
292
|
+
/** Raw router path parameters. */
|
|
293
|
+
params: PathParams<P>;
|
|
294
|
+
/** Raw query-string values, materialized lazily. */
|
|
295
|
+
query: Record<string, string | string[] | undefined>;
|
|
296
|
+
/** Raw request headers, materialized lazily. */
|
|
297
|
+
headers: Record<string, string | undefined>;
|
|
298
|
+
/** Always `undefined`; request-body I/O has not started. */
|
|
299
|
+
body: undefined;
|
|
300
|
+
/** Mutable per-request state shared with later hooks and the handler. */
|
|
301
|
+
state: AppState & Record<string, unknown>;
|
|
302
|
+
/** Response headers/status available to a short-circuiting perimeter hook. */
|
|
303
|
+
set: {
|
|
304
|
+
status?: number;
|
|
305
|
+
headers: Headers;
|
|
306
|
+
};
|
|
307
|
+
}
|
|
276
308
|
/**
|
|
277
309
|
* Lifecycle hooks fired around request handling. Hooks compose pipeline-style
|
|
278
310
|
* — the global hooks (`AppOptions.hooks`) run first, then group hooks added
|
|
279
311
|
* with `app.use()`, then per-route hooks. Returning a `Response` from
|
|
280
|
-
* `beforeHandle
|
|
312
|
+
* `preBody`, `beforeHandle`, or `onSend` short-circuits/replaces the response.
|
|
281
313
|
*
|
|
282
314
|
* Ordering for a successful request:
|
|
283
315
|
* 1. `onRequest` — before any context is built (raw `Request`).
|
|
284
|
-
* 2. `
|
|
285
|
-
* 3. *
|
|
286
|
-
* 4. `
|
|
287
|
-
* 5. *
|
|
288
|
-
* 6. `
|
|
289
|
-
* 7.
|
|
316
|
+
* 2. `preBody` — after routing, before body I/O or validation.
|
|
317
|
+
* 3. *request schemas are validated and the body is read when declared*
|
|
318
|
+
* 4. `beforeHandle` — with the validated context; may short-circuit.
|
|
319
|
+
* 5. *handler runs*
|
|
320
|
+
* 6. `afterHandle` — may transform the handler return value.
|
|
321
|
+
* 7. *response is serialized + validated*
|
|
322
|
+
* 8. `onSend` — may mutate or replace the outgoing `Response`.
|
|
323
|
+
* 9. `onResponse` — fire-and-forget observer (cannot change anything).
|
|
290
324
|
*
|
|
291
325
|
* `onError` runs on the error path before serialization.
|
|
292
326
|
*
|
|
@@ -295,7 +329,9 @@ export interface BaseContext<P extends string, R extends RequestSchemas | undefi
|
|
|
295
329
|
export interface Hooks {
|
|
296
330
|
/** Runs first, before validation or context building. Receives the raw web-standard `Request`. */
|
|
297
331
|
onRequest?: (req: Request) => void | Promise<void>;
|
|
298
|
-
/** Runs
|
|
332
|
+
/** Runs after route matching but before request-body I/O or schema validation. Use for cheap header/certificate auth; `ctx.body` is always undefined. A successful raw `Response` requires the route's `acknowledgeNoResponseBodySchema` flag; `4xx`/`5xx` denials do not. */
|
|
333
|
+
preBody?: (ctx: PreBodyContext<any>) => void | Response | Promise<void | Response>;
|
|
334
|
+
/** Runs with the validated {@link BaseContext} before the handler. Returning a `Response` short-circuits the handler entirely. Successful raw responses require the route's `acknowledgeNoResponseBodySchema` flag; `4xx`/`5xx` auth denials do not. */
|
|
299
335
|
beforeHandle?: (ctx: BaseContext<any, any>) => void | Response | Promise<void | Response>;
|
|
300
336
|
/** Runs after the handler with its raw return value. Return a non-`undefined` value to replace the result before serialization and response-schema validation. */
|
|
301
337
|
afterHandle?: (ctx: BaseContext<any, any>, result: unknown) => void | unknown | Promise<void | unknown>;
|
|
@@ -358,16 +394,22 @@ export interface RouteDefinition<P extends PathString = PathString, M extends Ht
|
|
|
358
394
|
/** Optional per-route API version label. Informational metadata only; not emitted into the OpenAPI document. */
|
|
359
395
|
version?: string;
|
|
360
396
|
/**
|
|
361
|
-
* Acknowledge that this route's
|
|
362
|
-
* response body schema —
|
|
363
|
-
*
|
|
397
|
+
* Acknowledge that this route's output intentionally is not protected by a
|
|
398
|
+
* response body schema — either because its `2xx` response declares no body
|
|
399
|
+
* schema or because it returns an opaque, framework-controlled raw
|
|
400
|
+
* `Response` (an HTML page, stream, spec document, or proxied payload).
|
|
364
401
|
*
|
|
365
402
|
* Setting this suppresses the `security.response.bodySchemaMissing` boot
|
|
366
403
|
* warning and the `audit.response.bodySchema` `daloy doctor` finding for
|
|
367
|
-
* this route only. It
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
404
|
+
* this route only. It also explicitly authorizes a handler or `afterHandle`
|
|
405
|
+
* hook to return a raw `Response`, or a `preBody` / `beforeHandle` hook to
|
|
406
|
+
* short-circuit with a successful (`2xx`/`3xx`) raw `Response`. DaloyJS fails
|
|
407
|
+
* closed with a `500` when those cases lack this acknowledgement. Ordinary
|
|
408
|
+
* `4xx`/`5xx` hook denials and errors remain available without opting out of
|
|
409
|
+
* response validation. This flag documents intent; it does not add
|
|
410
|
+
* protection — response field-level stripping (OWASP API3) does not run for
|
|
411
|
+
* an opaque body, so never set this on a route whose handler builds JSON from
|
|
412
|
+
* domain objects.
|
|
371
413
|
*/
|
|
372
414
|
acknowledgeNoResponseBodySchema?: boolean;
|
|
373
415
|
/**
|
|
@@ -489,8 +531,11 @@ export interface RouteDefinition<P extends PathString = PathString, M extends Ht
|
|
|
489
531
|
* forwarded verbatim).
|
|
490
532
|
*
|
|
491
533
|
* A returned `Response` **bypasses response-schema validation and the
|
|
492
|
-
* typed-client body type by design**
|
|
493
|
-
*
|
|
534
|
+
* typed-client body type by design** and therefore requires
|
|
535
|
+
* `acknowledgeNoResponseBodySchema: true` on the route. Without that explicit
|
|
536
|
+
* acknowledgement, DaloyJS refuses the response with a `500` instead of
|
|
537
|
+
* silently weakening the contract. An acknowledged raw response is still
|
|
538
|
+
* finalized exactly like every other response,
|
|
494
539
|
* so no security control is skipped: headers set via `ctx.set` (including
|
|
495
540
|
* `secureHeaders()` and CORS) are copied onto it, `x-request-id` is added
|
|
496
541
|
* when absent, any `onSend` / `onResponse` hooks run, server-fingerprint
|
|
@@ -503,6 +548,26 @@ export interface RouteDefinition<P extends PathString = PathString, M extends Ht
|
|
|
503
548
|
*/
|
|
504
549
|
handler: (ctx: BaseContext<P, Req>) => HandlerReturn<Res> | Response | Promise<HandlerReturn<Res> | Response>;
|
|
505
550
|
}
|
|
551
|
+
/**
|
|
552
|
+
* Define a route contract outside an {@link "./app.js".App} while preserving
|
|
553
|
+
* its literal path, method, operation id, schemas, and contextually typed
|
|
554
|
+
* handler.
|
|
555
|
+
*
|
|
556
|
+
* Export contracts from route modules, collect them in a literal tuple, then
|
|
557
|
+
* register the tuple with {@link "./app.js".App.registerRoutes}. The returned
|
|
558
|
+
* App retains the complete tuple for the no-codegen typed client.
|
|
559
|
+
*
|
|
560
|
+
* @param definition - Complete route contract and handler.
|
|
561
|
+
* @returns The same definition object, unchanged at runtime.
|
|
562
|
+
* @since 1.0.0
|
|
563
|
+
*/
|
|
564
|
+
export declare function defineRoute<const P extends PathString, const M extends HttpMethod, Req extends RequestSchemas | undefined, Res extends ResponsesMap, const Op extends string | undefined = undefined>(definition: RouteDefinition<P, M, Req, Res> & {
|
|
565
|
+
operationId?: Op;
|
|
566
|
+
}): RouteDefinition<P, M, Req, Res> & (Op extends string ? {
|
|
567
|
+
operationId: Op;
|
|
568
|
+
} : {
|
|
569
|
+
operationId?: undefined;
|
|
570
|
+
});
|
|
506
571
|
/**
|
|
507
572
|
* One operation inside an OpenAPI Callback Object. Mirrors a route minus
|
|
508
573
|
* `path` (the URL is supplied at runtime via the expression key) and
|
package/dist/types.js
CHANGED
|
@@ -1 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Define a route contract outside an {@link "./app.js".App} while preserving
|
|
3
|
+
* its literal path, method, operation id, schemas, and contextually typed
|
|
4
|
+
* handler.
|
|
5
|
+
*
|
|
6
|
+
* Export contracts from route modules, collect them in a literal tuple, then
|
|
7
|
+
* register the tuple with {@link "./app.js".App.registerRoutes}. The returned
|
|
8
|
+
* App retains the complete tuple for the no-codegen typed client.
|
|
9
|
+
*
|
|
10
|
+
* @param definition - Complete route contract and handler.
|
|
11
|
+
* @returns The same definition object, unchanged at runtime.
|
|
12
|
+
* @since 1.0.0
|
|
13
|
+
*/
|
|
14
|
+
export function defineRoute(definition) {
|
|
15
|
+
return definition;
|
|
16
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@daloyjs/core",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.4",
|
|
4
4
|
"description": "DaloyJS is a runtime-portable, contract-first TypeScript web framework with built-in OpenAPI (Hey API), typed client generation, large-scale maintainability, and security-first defaults. Hono-grade portability, Elysia-grade DX, FastAPI-grade docs, Fastify-grade ops — distributed via pnpm.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -46,6 +46,10 @@
|
|
|
46
46
|
"types": "./dist/index.d.ts",
|
|
47
47
|
"import": "./dist/index.js"
|
|
48
48
|
},
|
|
49
|
+
"./app": {
|
|
50
|
+
"types": "./dist/app.d.ts",
|
|
51
|
+
"import": "./dist/app.js"
|
|
52
|
+
},
|
|
49
53
|
"./node": {
|
|
50
54
|
"types": "./dist/adapters/node.d.ts",
|
|
51
55
|
"import": "./dist/adapters/node.js"
|
|
@@ -246,6 +250,8 @@
|
|
|
246
250
|
"example": "node --import tsx examples/basic.ts",
|
|
247
251
|
"bench": "node --import tsx bench/router.bench.ts",
|
|
248
252
|
"bench:serverless": "node --import tsx bench/serverless-cold-path.bench.ts",
|
|
253
|
+
"bench:json": "node --import tsx bench/json-body.bench.ts",
|
|
254
|
+
"bench:json-e2e": "node --import tsx bench/json-body-e2e.bench.ts",
|
|
249
255
|
"test": "node --import tsx --test tests/**/*.test.ts",
|
|
250
256
|
"test:red-team": "node --import tsx --test tests/red-team-attacks.test.ts tests/red-team-attacks-2.test.ts tests/red-team-attacks-3.test.ts tests/red-team-attacks-4.test.ts tests/red-team-attacks-5.test.ts tests/red-team-attacks-6.test.ts tests/red-team-attacks-7.test.ts tests/red-team-attacks-8.test.ts tests/red-team-attacks-9.test.ts tests/red-team-attacks-10.test.ts",
|
|
251
257
|
"red-team:live": "node --import tsx red-team-live/run.ts",
|