@daloyjs/core 1.0.0-rc.3 → 1.0.0-rc.5
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 +103 -41
- package/dist/adapters/bun.d.ts +20 -2
- package/dist/adapters/bun.js +41 -5
- package/dist/adapters/deno.js +24 -7
- package/dist/adapters/lambda.d.ts +59 -2
- package/dist/adapters/lambda.js +136 -20
- package/dist/adapters/node.d.ts +8 -1
- package/dist/adapters/node.js +104 -19
- package/dist/app.d.ts +131 -11
- package/dist/app.js +305 -217
- package/dist/bot-guard.js +30 -3
- package/dist/cli.js +41 -1
- package/dist/client.d.ts +64 -18
- package/dist/client.js +36 -6
- package/dist/combine.d.ts +11 -11
- package/dist/combine.js +90 -47
- package/dist/compression.d.ts +9 -0
- package/dist/compression.js +72 -1
- package/dist/conn-info.d.ts +5 -2
- package/dist/conn-info.js +5 -2
- package/dist/docs.d.ts +5 -9
- package/dist/docs.js +36 -14
- package/dist/errors.d.ts +12 -3
- package/dist/errors.js +12 -3
- package/dist/fetch-guard.d.ts +27 -19
- package/dist/fetch-guard.js +50 -8
- package/dist/http-signatures.d.ts +4 -1
- package/dist/http-signatures.js +13 -1
- package/dist/idempotency.js +2 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +3 -3
- 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/logger.d.ts +45 -0
- package/dist/logger.js +137 -0
- package/dist/mcp.js +21 -15
- package/dist/middleware.d.ts +48 -7
- package/dist/middleware.js +129 -43
- package/dist/mtls.d.ts +6 -5
- package/dist/mtls.js +8 -9
- package/dist/openapi.js +1 -1
- package/dist/pagination.js +4 -1
- package/dist/response-cache.js +2 -1
- package/dist/router.d.ts +2 -2
- package/dist/router.js +24 -9
- package/dist/safe-redirect.d.ts +9 -2
- package/dist/safe-redirect.js +29 -4
- package/dist/sbom.cdx.json +9 -9
- package/dist/sbom.spdx.json +5 -5
- package/dist/security.d.ts +62 -0
- package/dist/security.js +220 -15
- package/dist/session.d.ts +13 -2
- package/dist/session.js +111 -17
- package/dist/tenancy.d.ts +2 -2
- package/dist/time-claims.js +3 -1
- package/dist/types.d.ts +85 -20
- package/dist/types.js +16 -1
- package/dist/waf.js +86 -26
- package/package.json +11 -4
package/dist/bot-guard.js
CHANGED
|
@@ -76,8 +76,14 @@ function matchesUserAgent(ua, patterns) {
|
|
|
76
76
|
if (pattern && lower.includes(pattern.toLowerCase()))
|
|
77
77
|
return true;
|
|
78
78
|
}
|
|
79
|
-
else
|
|
80
|
-
|
|
79
|
+
else {
|
|
80
|
+
// Reset lastIndex so caller-supplied /g or /y regexes cannot flip-flop
|
|
81
|
+
// between match and miss across requests (intermittent allowlist bypass).
|
|
82
|
+
pattern.lastIndex = 0;
|
|
83
|
+
const hit = pattern.test(ua);
|
|
84
|
+
pattern.lastIndex = 0;
|
|
85
|
+
if (hit)
|
|
86
|
+
return true;
|
|
81
87
|
}
|
|
82
88
|
}
|
|
83
89
|
return false;
|
|
@@ -225,12 +231,28 @@ export function botGuard(opts = {}) {
|
|
|
225
231
|
};
|
|
226
232
|
const writeCache = (key, verified) => {
|
|
227
233
|
const now = Date.now();
|
|
234
|
+
// Move this key to the newest insertion slot on every (re)write. Eviction
|
|
235
|
+
// below is therefore FIFO over WRITE-recency (Map preserves insertion
|
|
236
|
+
// order), not true LRU: cache *reads* on the verification path do not
|
|
237
|
+
// reorder entries, so a frequently-read-but-never-rewritten key can still
|
|
238
|
+
// be evicted. That is intentional — reordering on read would add a Map
|
|
239
|
+
// delete+set to the hot lookup path for no security benefit.
|
|
240
|
+
if (cache.has(key))
|
|
241
|
+
cache.delete(key);
|
|
228
242
|
cache.set(key, { verified, expiresMs: now + cacheTtlMs });
|
|
229
243
|
if (cache.size > cacheMax) {
|
|
230
244
|
for (const [k, v] of cache)
|
|
231
245
|
if (v.expiresMs <= now)
|
|
232
246
|
cache.delete(k);
|
|
233
247
|
}
|
|
248
|
+
// Still over the cap after pruning expired entries: evict the
|
|
249
|
+
// oldest-written live keys (front of insertion order) until within cacheMax.
|
|
250
|
+
while (cache.size > cacheMax) {
|
|
251
|
+
const oldest = cache.keys().next().value;
|
|
252
|
+
if (oldest === undefined)
|
|
253
|
+
break;
|
|
254
|
+
cache.delete(oldest);
|
|
255
|
+
}
|
|
234
256
|
};
|
|
235
257
|
const reject = (event) => {
|
|
236
258
|
opts.onBlock?.(event);
|
|
@@ -252,7 +274,12 @@ export function botGuard(opts = {}) {
|
|
|
252
274
|
reject({ reason: "blocked-user-agent", userAgent: ua });
|
|
253
275
|
return undefined;
|
|
254
276
|
}
|
|
255
|
-
const rule = verifiedBots.find((r) =>
|
|
277
|
+
const rule = verifiedBots.find((r) => {
|
|
278
|
+
r.userAgent.lastIndex = 0;
|
|
279
|
+
const hit = r.userAgent.test(ua);
|
|
280
|
+
r.userAgent.lastIndex = 0;
|
|
281
|
+
return hit;
|
|
282
|
+
});
|
|
256
283
|
if (!rule)
|
|
257
284
|
return undefined;
|
|
258
285
|
const ip = resolveIp(ctx);
|
package/dist/cli.js
CHANGED
|
@@ -704,6 +704,44 @@ async function runDoctor(opts, io) {
|
|
|
704
704
|
"header-count amplification defence.",
|
|
705
705
|
});
|
|
706
706
|
}
|
|
707
|
+
// JSON structural limits audit. The new jsonMaxKeys / jsonMaxDepth
|
|
708
|
+
// guards protect against hash-flood / deep-nesting DoS inside the byte
|
|
709
|
+
// limit. Surface when disabled (0) or raised to an implausibly high
|
|
710
|
+
// value.
|
|
711
|
+
const jsonMaxKeys = o.jsonMaxKeys;
|
|
712
|
+
if (jsonMaxKeys === 0) {
|
|
713
|
+
findings.push({
|
|
714
|
+
level: "warn",
|
|
715
|
+
code: "audit.jsonMaxKeys.disabled",
|
|
716
|
+
message: "jsonMaxKeys is 0 — the wide-object / hash-flood structural limit " +
|
|
717
|
+
"is disabled. An attacker can send tens or hundreds of thousands " +
|
|
718
|
+
"of keys in a body that still fits under bodyLimitBytes.",
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
else if (typeof jsonMaxKeys === "number" && jsonMaxKeys > 100_000) {
|
|
722
|
+
findings.push({
|
|
723
|
+
level: "warn",
|
|
724
|
+
code: "audit.jsonMaxKeys.blanket",
|
|
725
|
+
message: `jsonMaxKeys is ${jsonMaxKeys} (> 100k). A cap this high weakens ` +
|
|
726
|
+
"protection against wide-object DoS payloads.",
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
const jsonMaxDepth = o.jsonMaxDepth;
|
|
730
|
+
if (jsonMaxDepth === 0) {
|
|
731
|
+
findings.push({
|
|
732
|
+
level: "warn",
|
|
733
|
+
code: "audit.jsonMaxDepth.disabled",
|
|
734
|
+
message: "jsonMaxDepth is 0 — deep nesting DoS protection is disabled.",
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
else if (typeof jsonMaxDepth === "number" && jsonMaxDepth > 200) {
|
|
738
|
+
findings.push({
|
|
739
|
+
level: "warn",
|
|
740
|
+
code: "audit.jsonMaxDepth.blanket",
|
|
741
|
+
message: `jsonMaxDepth is ${jsonMaxDepth} (> 200). Extremely deep JSON is ` +
|
|
742
|
+
"almost never legitimate and can amplify CPU during validation.",
|
|
743
|
+
});
|
|
744
|
+
}
|
|
707
745
|
// Idle-timeout / request-timeout audit. Reaffirms the
|
|
708
746
|
// existing requestTimeoutMs check; also surface an explicit zero
|
|
709
747
|
// idleTimeoutMs in production. The framework also keeps adapter
|
|
@@ -921,7 +959,9 @@ function aiResponses(responses) {
|
|
|
921
959
|
for (const [status, spec] of Object.entries(responses)) {
|
|
922
960
|
if (!spec)
|
|
923
961
|
continue;
|
|
924
|
-
const entry = {
|
|
962
|
+
const entry = {
|
|
963
|
+
description: spec.description ?? `HTTP ${status} response`,
|
|
964
|
+
};
|
|
925
965
|
if (spec.body)
|
|
926
966
|
entry.body = aiSchema(spec.body);
|
|
927
967
|
if (spec.examples)
|
package/dist/client.d.ts
CHANGED
|
@@ -10,38 +10,58 @@
|
|
|
10
10
|
* can still be generated from the OpenAPI doc for non-TS clients).
|
|
11
11
|
*/
|
|
12
12
|
import type { App } from "./app.js";
|
|
13
|
-
import type { HandlerReturn, InferRequest, RequestSchemas, ResponsesMap, RouteDefinition } from "./types.js";
|
|
13
|
+
import type { HandlerReturn, InferRequest, ParamsOf, RequestSchemas, ResponsesMap, RouteDefinition } from "./types.js";
|
|
14
14
|
/** Union of every {@link RouteDefinition} registered on an `App`. */
|
|
15
15
|
export type RoutesOf<A extends App> = A["routes"][number];
|
|
16
16
|
/**
|
|
17
17
|
* Typed client surface generated from an `App`. The result is a record keyed
|
|
18
18
|
* by each route's `operationId` whose values are async methods inferred from
|
|
19
|
-
* the route's request and response schemas.
|
|
19
|
+
* the route's request and response schemas. Required query and header fields
|
|
20
|
+
* remain required on the client input, while schemas that accept an empty
|
|
21
|
+
* object keep their corresponding client field optional.
|
|
20
22
|
*
|
|
21
23
|
* The per-method types are recovered from the `App`'s accumulated route tuple,
|
|
22
|
-
*
|
|
23
|
-
* is widened back to
|
|
24
|
-
*
|
|
25
|
-
* rather than a chain — the tuple is erased and this type collapses to an
|
|
26
|
-
* untyped, string-indexed record.
|
|
24
|
+
* built by chained registrations or `app.registerRoutes([...])`. If the result
|
|
25
|
+
* is widened back to a bare `App` annotation, the tuple is intentionally erased
|
|
26
|
+
* and this type becomes a string-indexed record.
|
|
27
27
|
*/
|
|
28
28
|
export type ClientFor<A extends App> = {
|
|
29
29
|
[R in Extract<RoutesOf<A>, {
|
|
30
30
|
operationId: string;
|
|
31
31
|
}> as R["operationId"]]: ClientMethod<R>;
|
|
32
32
|
};
|
|
33
|
-
type ClientMethod<R> = R extends RouteDefinition<infer P, infer _M, infer Req, infer Res> ? (input: ClientInput<P, Req>) => Promise<ClientOutput<Res>> : never;
|
|
34
|
-
type ClientInput<P extends string, Req extends RequestSchemas | undefined> =
|
|
33
|
+
type ClientMethod<R> = R extends RouteDefinition<infer P, infer _M, infer Req, infer Res> ? {} extends ClientInput<P, Req> ? (input?: ClientInput<P, Req>) => Promise<ClientOutput<Res>> : (input: ClientInput<P, Req>) => Promise<ClientOutput<Res>> : never;
|
|
34
|
+
type ClientInput<P extends string, Req extends RequestSchemas | undefined> = ([
|
|
35
|
+
ParamsOf<P>
|
|
36
|
+
] extends [never] ? {
|
|
37
|
+
params?: Record<string, never>;
|
|
38
|
+
} : {
|
|
35
39
|
params: InferRequest<Req, P>["params"];
|
|
36
|
-
|
|
37
|
-
headers?: Record<string, string>;
|
|
38
|
-
} & (Req extends {
|
|
40
|
+
}) & ClientQueryInput<P, Req> & ClientHeadersInput<P, Req> & (Req extends {
|
|
39
41
|
body: infer _B;
|
|
40
42
|
} ? {
|
|
41
43
|
body: InferRequest<Req, P>["body"];
|
|
42
44
|
} : {
|
|
43
45
|
body?: undefined;
|
|
44
46
|
});
|
|
47
|
+
type ClientQueryInput<P extends string, Req extends RequestSchemas | undefined> = Req extends {
|
|
48
|
+
query: infer _Query;
|
|
49
|
+
} ? {} extends NonNullable<InferRequest<Req, P>["query"]> ? {
|
|
50
|
+
query?: NonNullable<InferRequest<Req, P>["query"]>;
|
|
51
|
+
} : {
|
|
52
|
+
query: NonNullable<InferRequest<Req, P>["query"]>;
|
|
53
|
+
} : {
|
|
54
|
+
query?: Record<string, string | string[] | number | boolean | undefined>;
|
|
55
|
+
};
|
|
56
|
+
type ClientHeadersInput<P extends string, Req extends RequestSchemas | undefined> = Req extends {
|
|
57
|
+
headers: infer _Headers;
|
|
58
|
+
} ? {} extends NonNullable<InferRequest<Req, P>["headers"]> ? {
|
|
59
|
+
headers?: NonNullable<InferRequest<Req, P>["headers"]>;
|
|
60
|
+
} : {
|
|
61
|
+
headers: NonNullable<InferRequest<Req, P>["headers"]>;
|
|
62
|
+
} : {
|
|
63
|
+
headers?: Record<string, string>;
|
|
64
|
+
};
|
|
45
65
|
type ClientOutput<Res extends ResponsesMap> = HandlerReturn<Res>;
|
|
46
66
|
/** Options for {@link createClient}. */
|
|
47
67
|
export interface ClientOptions {
|
|
@@ -52,11 +72,21 @@ export interface ClientOptions {
|
|
|
52
72
|
/** Default headers merged into every request (per-call `input.headers` wins). */
|
|
53
73
|
headers?: Record<string, string>;
|
|
54
74
|
}
|
|
75
|
+
/** Options for {@link createInProcessClient}. */
|
|
76
|
+
export interface InProcessClientOptions {
|
|
77
|
+
/** Synthetic absolute origin used while constructing requests. Default: `http://daloy.local`. */
|
|
78
|
+
baseUrl?: string;
|
|
79
|
+
/** Default headers merged into every request. Per-call headers win. */
|
|
80
|
+
headers?: Record<string, string>;
|
|
81
|
+
}
|
|
55
82
|
/**
|
|
56
|
-
* Build a typed
|
|
83
|
+
* Build a typed fetch client whose methods are keyed by
|
|
57
84
|
* `operationId`. Parameters and response types are inferred from the same
|
|
58
85
|
* route definitions registered on `app`, so the client and server cannot
|
|
59
86
|
* drift apart at the type level.
|
|
87
|
+
* Required `params`, `query`, `headers`, and `body` inputs are preserved from
|
|
88
|
+
* the route contract; query or header schemas that accept an empty object keep
|
|
89
|
+
* those top-level fields optional.
|
|
60
90
|
*
|
|
61
91
|
* The returned object is a plain `Record<operationId, (input) => Promise<...>>`
|
|
62
92
|
* — each call serializes `params`/`query`/`headers`/`body` and dispatches
|
|
@@ -64,13 +94,14 @@ export interface ClientOptions {
|
|
|
64
94
|
*
|
|
65
95
|
* For non-TypeScript consumers, run `pnpm gen` to emit a fully-typed SDK
|
|
66
96
|
* from the OpenAPI document instead.
|
|
97
|
+
* Routes without path parameters omit the `params` input, and routes with no
|
|
98
|
+
* required request inputs may be called without an argument.
|
|
67
99
|
*
|
|
68
100
|
* @remarks
|
|
69
|
-
* The method signatures are inferred from the `App`'s accumulated route tuple
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* per-route types and yields an untyped client.
|
|
101
|
+
* The method signatures are inferred from the `App`'s accumulated route tuple.
|
|
102
|
+
* Chain registrations or compose independently exported contracts with
|
|
103
|
+
* `app.registerRoutes([...])`, and avoid widening the result to a bare `App`
|
|
104
|
+
* annotation because that deliberately discards the per-route tuple.
|
|
74
105
|
*
|
|
75
106
|
* @example
|
|
76
107
|
* ```ts
|
|
@@ -96,4 +127,19 @@ export interface ClientOptions {
|
|
|
96
127
|
* @since 0.1.0
|
|
97
128
|
*/
|
|
98
129
|
export declare function createClient<A extends App>(app: A, opts: ClientOptions): ClientFor<A>;
|
|
130
|
+
/**
|
|
131
|
+
* Build a typed client that dispatches directly through an App without
|
|
132
|
+
* opening a socket or binding a port.
|
|
133
|
+
*
|
|
134
|
+
* Requests still traverse the complete validation, middleware, security, and
|
|
135
|
+
* serialization pipeline through {@link "./app.js".App.fetch}.
|
|
136
|
+
* Routes without path parameters omit the `params` input, and routes with no
|
|
137
|
+
* required request inputs may be called without an argument.
|
|
138
|
+
*
|
|
139
|
+
* @param app - App whose registered route tuple drives the client surface.
|
|
140
|
+
* @param opts - Optional synthetic origin and default request headers.
|
|
141
|
+
* @returns A typed operation-id client backed by in-process dispatch.
|
|
142
|
+
* @since 1.0.0
|
|
143
|
+
*/
|
|
144
|
+
export declare function createInProcessClient<A extends App>(app: A, opts?: InProcessClientOptions): ClientFor<A>;
|
|
99
145
|
export {};
|
package/dist/client.js
CHANGED
|
@@ -10,10 +10,13 @@
|
|
|
10
10
|
* can still be generated from the OpenAPI doc for non-TS clients).
|
|
11
11
|
*/
|
|
12
12
|
/**
|
|
13
|
-
* Build a typed
|
|
13
|
+
* Build a typed fetch client whose methods are keyed by
|
|
14
14
|
* `operationId`. Parameters and response types are inferred from the same
|
|
15
15
|
* route definitions registered on `app`, so the client and server cannot
|
|
16
16
|
* drift apart at the type level.
|
|
17
|
+
* Required `params`, `query`, `headers`, and `body` inputs are preserved from
|
|
18
|
+
* the route contract; query or header schemas that accept an empty object keep
|
|
19
|
+
* those top-level fields optional.
|
|
17
20
|
*
|
|
18
21
|
* The returned object is a plain `Record<operationId, (input) => Promise<...>>`
|
|
19
22
|
* — each call serializes `params`/`query`/`headers`/`body` and dispatches
|
|
@@ -21,13 +24,14 @@
|
|
|
21
24
|
*
|
|
22
25
|
* For non-TypeScript consumers, run `pnpm gen` to emit a fully-typed SDK
|
|
23
26
|
* from the OpenAPI document instead.
|
|
27
|
+
* Routes without path parameters omit the `params` input, and routes with no
|
|
28
|
+
* required request inputs may be called without an argument.
|
|
24
29
|
*
|
|
25
30
|
* @remarks
|
|
26
|
-
* The method signatures are inferred from the `App`'s accumulated route tuple
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* per-route types and yields an untyped client.
|
|
31
|
+
* The method signatures are inferred from the `App`'s accumulated route tuple.
|
|
32
|
+
* Chain registrations or compose independently exported contracts with
|
|
33
|
+
* `app.registerRoutes([...])`, and avoid widening the result to a bare `App`
|
|
34
|
+
* annotation because that deliberately discards the per-route tuple.
|
|
31
35
|
*
|
|
32
36
|
* @example
|
|
33
37
|
* ```ts
|
|
@@ -93,6 +97,32 @@ export function createClient(app, opts) {
|
|
|
93
97
|
}
|
|
94
98
|
return out;
|
|
95
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Build a typed client that dispatches directly through an App without
|
|
102
|
+
* opening a socket or binding a port.
|
|
103
|
+
*
|
|
104
|
+
* Requests still traverse the complete validation, middleware, security, and
|
|
105
|
+
* serialization pipeline through {@link "./app.js".App.fetch}.
|
|
106
|
+
* Routes without path parameters omit the `params` input, and routes with no
|
|
107
|
+
* required request inputs may be called without an argument.
|
|
108
|
+
*
|
|
109
|
+
* @param app - App whose registered route tuple drives the client surface.
|
|
110
|
+
* @param opts - Optional synthetic origin and default request headers.
|
|
111
|
+
* @returns A typed operation-id client backed by in-process dispatch.
|
|
112
|
+
* @since 1.0.0
|
|
113
|
+
*/
|
|
114
|
+
export function createInProcessClient(app, opts = {}) {
|
|
115
|
+
const clientOptions = {
|
|
116
|
+
baseUrl: opts.baseUrl ?? "http://daloy.local",
|
|
117
|
+
fetch: (input, init) => {
|
|
118
|
+
const request = input instanceof Request ? input : new Request(input, init);
|
|
119
|
+
return app.fetch(request);
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
if (opts.headers)
|
|
123
|
+
clientOptions.headers = opts.headers;
|
|
124
|
+
return createClient(app, clientOptions);
|
|
125
|
+
}
|
|
96
126
|
function safeJson(text) {
|
|
97
127
|
try {
|
|
98
128
|
return JSON.parse(text);
|
package/dist/combine.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* @since 0.19.0
|
|
7
7
|
*/
|
|
8
|
-
import type { Hooks, BaseContext } from "./types.js";
|
|
8
|
+
import type { Hooks, BaseContext, PreBodyContext } from "./types.js";
|
|
9
9
|
/**
|
|
10
10
|
* Run every supplied {@link Hooks} bundle in order, pipeline-style.
|
|
11
11
|
* Equivalent to passing the bundles to `app.use(...)` one after another,
|
|
@@ -13,7 +13,7 @@ import type { Hooks, BaseContext } from "./types.js";
|
|
|
13
13
|
* stack for the admin section"). All lifecycle phases compose:
|
|
14
14
|
*
|
|
15
15
|
* - `onRequest` / `onResponse` run in registration order.
|
|
16
|
-
* - `beforeHandle` / `onError` short-circuit on the first `Response`.
|
|
16
|
+
* - `preBody` / `beforeHandle` / `onError` short-circuit on the first `Response`.
|
|
17
17
|
* - `afterHandle` / `onSend` thread the value through every bundle.
|
|
18
18
|
*
|
|
19
19
|
* Symbol-keyed security markers (CORS / CSRF / session / secure-headers)
|
|
@@ -35,14 +35,14 @@ import type { Hooks, BaseContext } from "./types.js";
|
|
|
35
35
|
*/
|
|
36
36
|
export declare function every(...layers: Hooks[]): Hooks;
|
|
37
37
|
/**
|
|
38
|
-
* Run the supplied bundles until one of them passes its
|
|
38
|
+
* Run the supplied bundles until one of them passes its auth gate
|
|
39
39
|
* check without throwing. Useful for "this route accepts a bearer token
|
|
40
40
|
* OR a signed cookie OR an API key" patterns where any single proof of
|
|
41
41
|
* identity is enough.
|
|
42
42
|
*
|
|
43
43
|
* Semantics:
|
|
44
44
|
*
|
|
45
|
-
* - The bundles' `beforeHandle` hooks are awaited in order. The first one
|
|
45
|
+
* - The bundles' `preBody` or `beforeHandle` hooks are awaited in order. The first one
|
|
46
46
|
* that resolves without throwing wins; its `ctx` mutations (headers,
|
|
47
47
|
* `ctx.state`, etc.) are preserved.
|
|
48
48
|
* - When a bundle returns a `Response`, that response is treated as a
|
|
@@ -52,7 +52,7 @@ export declare function every(...layers: Hooks[]): Hooks;
|
|
|
52
52
|
* client gets a deterministic status code. Place the auth method whose
|
|
53
53
|
* `WWW-Authenticate` challenge you want clients to see first.
|
|
54
54
|
* - `afterHandle`, `onSend`, `onResponse`, and `onError` from every bundle
|
|
55
|
-
* still compose normally — `some()` only changes the
|
|
55
|
+
* still compose normally — `some()` only changes the auth-gate
|
|
56
56
|
* evaluation strategy.
|
|
57
57
|
*
|
|
58
58
|
* @example
|
|
@@ -63,8 +63,8 @@ export declare function every(...layers: Hooks[]): Hooks;
|
|
|
63
63
|
* ));
|
|
64
64
|
* ```
|
|
65
65
|
*
|
|
66
|
-
* @param layers Candidate hook bundles; the first
|
|
67
|
-
* @returns A merged {@link Hooks} bundle with
|
|
66
|
+
* @param layers Candidate hook bundles; the first auth gate that passes wins.
|
|
67
|
+
* @returns A merged {@link Hooks} bundle with an OR-style auth-gate strategy.
|
|
68
68
|
* @since 0.19.0
|
|
69
69
|
*/
|
|
70
70
|
export declare function some(...layers: Hooks[]): Hooks;
|
|
@@ -76,7 +76,7 @@ export declare function some(...layers: Hooks[]): Hooks;
|
|
|
76
76
|
*
|
|
77
77
|
* @since 0.19.0
|
|
78
78
|
*/
|
|
79
|
-
export type ExceptPredicate = string | string[] | ((ctx: BaseContext<any, any>) => boolean | Promise<boolean>);
|
|
79
|
+
export type ExceptPredicate = string | string[] | ((ctx: PreBodyContext<any> | BaseContext<any, any>) => boolean | Promise<boolean>);
|
|
80
80
|
/**
|
|
81
81
|
* Run a hook bundle on every request EXCEPT those matching `when`. The
|
|
82
82
|
* canonical use is "apply auth everywhere except the public endpoints":
|
|
@@ -89,15 +89,15 @@ export type ExceptPredicate = string | string[] | ((ctx: BaseContext<any, any>)
|
|
|
89
89
|
* ));
|
|
90
90
|
* ```
|
|
91
91
|
*
|
|
92
|
-
*
|
|
92
|
+
* The `preBody` and `beforeHandle` phases are gated — the surrounding
|
|
93
93
|
* `onRequest`/`afterHandle`/`onSend`/`onResponse` phases still run so
|
|
94
94
|
* shared concerns like request-id propagation are not accidentally
|
|
95
95
|
* exempted. Wrap each bundle with {@link except} individually when you
|
|
96
96
|
* need to gate other phases.
|
|
97
97
|
*
|
|
98
98
|
* @param when Paths or predicate ({@link ExceptPredicate}) that exempt a request.
|
|
99
|
-
* @param hooks The hook bundle whose `beforeHandle`
|
|
100
|
-
* @returns A {@link Hooks} bundle whose
|
|
99
|
+
* @param hooks The hook bundle whose `preBody` and `beforeHandle` gates are skipped on a match.
|
|
100
|
+
* @returns A {@link Hooks} bundle whose request gates are controlled by `when`.
|
|
101
101
|
* @throws Error at composition time if a string pattern does not start with `/`.
|
|
102
102
|
* @since 0.19.0
|
|
103
103
|
*/
|
package/dist/combine.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* @since 0.19.0
|
|
7
7
|
*/
|
|
8
|
+
import { _mergePreBodyWithEarlyRejections, EARLY_REJECTION_HOOK_MARKER } from "./middleware.js";
|
|
8
9
|
/**
|
|
9
10
|
* Run every supplied {@link Hooks} bundle in order, pipeline-style.
|
|
10
11
|
* Equivalent to passing the bundles to `app.use(...)` one after another,
|
|
@@ -12,7 +13,7 @@
|
|
|
12
13
|
* stack for the admin section"). All lifecycle phases compose:
|
|
13
14
|
*
|
|
14
15
|
* - `onRequest` / `onResponse` run in registration order.
|
|
15
|
-
* - `beforeHandle` / `onError` short-circuit on the first `Response`.
|
|
16
|
+
* - `preBody` / `beforeHandle` / `onError` short-circuit on the first `Response`.
|
|
16
17
|
* - `afterHandle` / `onSend` thread the value through every bundle.
|
|
17
18
|
*
|
|
18
19
|
* Symbol-keyed security markers (CORS / CSRF / session / secure-headers)
|
|
@@ -36,14 +37,14 @@ export function every(...layers) {
|
|
|
36
37
|
return mergeCombineHooks(layers);
|
|
37
38
|
}
|
|
38
39
|
/**
|
|
39
|
-
* Run the supplied bundles until one of them passes its
|
|
40
|
+
* Run the supplied bundles until one of them passes its auth gate
|
|
40
41
|
* check without throwing. Useful for "this route accepts a bearer token
|
|
41
42
|
* OR a signed cookie OR an API key" patterns where any single proof of
|
|
42
43
|
* identity is enough.
|
|
43
44
|
*
|
|
44
45
|
* Semantics:
|
|
45
46
|
*
|
|
46
|
-
* - The bundles' `beforeHandle` hooks are awaited in order. The first one
|
|
47
|
+
* - The bundles' `preBody` or `beforeHandle` hooks are awaited in order. The first one
|
|
47
48
|
* that resolves without throwing wins; its `ctx` mutations (headers,
|
|
48
49
|
* `ctx.state`, etc.) are preserved.
|
|
49
50
|
* - When a bundle returns a `Response`, that response is treated as a
|
|
@@ -53,7 +54,7 @@ export function every(...layers) {
|
|
|
53
54
|
* client gets a deterministic status code. Place the auth method whose
|
|
54
55
|
* `WWW-Authenticate` challenge you want clients to see first.
|
|
55
56
|
* - `afterHandle`, `onSend`, `onResponse`, and `onError` from every bundle
|
|
56
|
-
* still compose normally — `some()` only changes the
|
|
57
|
+
* still compose normally — `some()` only changes the auth-gate
|
|
57
58
|
* evaluation strategy.
|
|
58
59
|
*
|
|
59
60
|
* @example
|
|
@@ -64,45 +65,65 @@ export function every(...layers) {
|
|
|
64
65
|
* ));
|
|
65
66
|
* ```
|
|
66
67
|
*
|
|
67
|
-
* @param layers Candidate hook bundles; the first
|
|
68
|
-
* @returns A merged {@link Hooks} bundle with
|
|
68
|
+
* @param layers Candidate hook bundles; the first auth gate that passes wins.
|
|
69
|
+
* @returns A merged {@link Hooks} bundle with an OR-style auth-gate strategy.
|
|
69
70
|
* @since 0.19.0
|
|
70
71
|
*/
|
|
71
72
|
export function some(...layers) {
|
|
72
73
|
if (layers.length === 0)
|
|
73
74
|
return {};
|
|
74
|
-
const stripped = layers.map(({ beforeHandle: _b, ...rest }) => rest);
|
|
75
|
+
const stripped = layers.map(({ preBody: _p, beforeHandle: _b, ...rest }) => rest);
|
|
75
76
|
const base = mergeCombineHooks(stripped);
|
|
76
|
-
const
|
|
77
|
+
const preBodyCandidates = layers
|
|
78
|
+
.map((h) => h.preBody)
|
|
79
|
+
.filter((f) => typeof f === "function");
|
|
80
|
+
const beforeHandleCandidates = layers
|
|
77
81
|
.map((h) => h.beforeHandle)
|
|
78
82
|
.filter((f) => typeof f === "function");
|
|
83
|
+
const usePreBody = preBodyCandidates.length > 0 && beforeHandleCandidates.length === 0;
|
|
84
|
+
const candidates = usePreBody
|
|
85
|
+
? preBodyCandidates
|
|
86
|
+
: layers.flatMap((hooks) => {
|
|
87
|
+
if (hooks.preBody && hooks.beforeHandle) {
|
|
88
|
+
return [
|
|
89
|
+
async (ctx) => {
|
|
90
|
+
const early = await hooks.preBody(ctx);
|
|
91
|
+
return early instanceof Response ? early : hooks.beforeHandle(ctx);
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
const gate = hooks.preBody ?? hooks.beforeHandle;
|
|
96
|
+
return gate ? [gate] : [];
|
|
97
|
+
});
|
|
79
98
|
if (candidates.length === 0)
|
|
80
99
|
return base;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const r = await fn(ctx);
|
|
88
|
-
if (r instanceof Response) {
|
|
89
|
-
// Treat as a denial — try the next layer.
|
|
90
|
-
if (!firstFailure)
|
|
91
|
-
firstFailure = { kind: "response", res: r };
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
// Undefined = pass; bundle accepts the request.
|
|
95
|
-
return undefined;
|
|
96
|
-
}
|
|
97
|
-
catch (err) {
|
|
100
|
+
const runCandidates = async (ctx) => {
|
|
101
|
+
let firstFailure;
|
|
102
|
+
for (const fn of candidates) {
|
|
103
|
+
try {
|
|
104
|
+
const r = await fn(ctx);
|
|
105
|
+
if (r instanceof Response) {
|
|
98
106
|
if (!firstFailure)
|
|
99
|
-
firstFailure = { kind: "
|
|
107
|
+
firstFailure = { kind: "response", res: r };
|
|
108
|
+
continue;
|
|
100
109
|
}
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
if (!firstFailure)
|
|
114
|
+
firstFailure = { kind: "throw", err };
|
|
101
115
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
116
|
+
}
|
|
117
|
+
if (firstFailure?.kind === "response")
|
|
118
|
+
return firstFailure.res;
|
|
119
|
+
throw firstFailure.err;
|
|
120
|
+
};
|
|
121
|
+
if (usePreBody) {
|
|
122
|
+
return { ...base, preBody: runCandidates };
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
...base,
|
|
126
|
+
beforeHandle: runCandidates,
|
|
106
127
|
};
|
|
107
128
|
}
|
|
108
129
|
/**
|
|
@@ -117,31 +138,41 @@ export function some(...layers) {
|
|
|
117
138
|
* ));
|
|
118
139
|
* ```
|
|
119
140
|
*
|
|
120
|
-
*
|
|
141
|
+
* The `preBody` and `beforeHandle` phases are gated — the surrounding
|
|
121
142
|
* `onRequest`/`afterHandle`/`onSend`/`onResponse` phases still run so
|
|
122
143
|
* shared concerns like request-id propagation are not accidentally
|
|
123
144
|
* exempted. Wrap each bundle with {@link except} individually when you
|
|
124
145
|
* need to gate other phases.
|
|
125
146
|
*
|
|
126
147
|
* @param when Paths or predicate ({@link ExceptPredicate}) that exempt a request.
|
|
127
|
-
* @param hooks The hook bundle whose `beforeHandle`
|
|
128
|
-
* @returns A {@link Hooks} bundle whose
|
|
148
|
+
* @param hooks The hook bundle whose `preBody` and `beforeHandle` gates are skipped on a match.
|
|
149
|
+
* @returns A {@link Hooks} bundle whose request gates are controlled by `when`.
|
|
129
150
|
* @throws Error at composition time if a string pattern does not start with `/`.
|
|
130
151
|
* @since 0.19.0
|
|
131
152
|
*/
|
|
132
153
|
export function except(when, hooks) {
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
if (!original)
|
|
154
|
+
const earlyRejectionHooks = hooks[EARLY_REJECTION_HOOK_MARKER];
|
|
155
|
+
if (!hooks.preBody && !hooks.beforeHandle && !Array.isArray(earlyRejectionHooks))
|
|
136
156
|
return hooks;
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
157
|
+
const matches = compileExceptMatcher(when);
|
|
158
|
+
const wrapped = { ...hooks };
|
|
159
|
+
if (Array.isArray(earlyRejectionHooks)) {
|
|
160
|
+
wrapped[EARLY_REJECTION_HOOK_MARKER] =
|
|
161
|
+
earlyRejectionHooks.map((hook) => {
|
|
162
|
+
if (typeof hook !== "function")
|
|
163
|
+
return hook;
|
|
164
|
+
return async (ctx) => (await matches(ctx)) ? undefined : hook(ctx);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
if (hooks.preBody) {
|
|
168
|
+
const original = hooks.preBody;
|
|
169
|
+
wrapped.preBody = async (ctx) => ((await matches(ctx)) ? undefined : original(ctx));
|
|
170
|
+
}
|
|
171
|
+
if (hooks.beforeHandle) {
|
|
172
|
+
const original = hooks.beforeHandle;
|
|
173
|
+
wrapped.beforeHandle = async (ctx) => ((await matches(ctx)) ? undefined : original(ctx));
|
|
174
|
+
}
|
|
175
|
+
return wrapped;
|
|
145
176
|
}
|
|
146
177
|
function compileExceptMatcher(when) {
|
|
147
178
|
if (typeof when === "function") {
|
|
@@ -175,9 +206,7 @@ function compilePathPattern(pattern) {
|
|
|
175
206
|
return (path) => regex.test(path);
|
|
176
207
|
}
|
|
177
208
|
function mergeCombineHooks(layers) {
|
|
178
|
-
const pick = (key) => layers
|
|
179
|
-
.map((h) => h[key])
|
|
180
|
-
.filter((f) => typeof f === "function");
|
|
209
|
+
const pick = (key) => layers.map((h) => h[key]).filter((f) => typeof f === "function");
|
|
181
210
|
const merged = {};
|
|
182
211
|
const onRequest = pick("onRequest");
|
|
183
212
|
if (onRequest.length > 0) {
|
|
@@ -186,6 +215,9 @@ function mergeCombineHooks(layers) {
|
|
|
186
215
|
await fn(req);
|
|
187
216
|
};
|
|
188
217
|
}
|
|
218
|
+
const preBody = _mergePreBodyWithEarlyRejections(layers);
|
|
219
|
+
if (preBody !== undefined)
|
|
220
|
+
merged.preBody = preBody;
|
|
189
221
|
const beforeHandle = pick("beforeHandle");
|
|
190
222
|
if (beforeHandle.length > 0) {
|
|
191
223
|
merged.beforeHandle = async (ctx) => {
|
|
@@ -245,6 +277,17 @@ function mergeCombineHooks(layers) {
|
|
|
245
277
|
for (const hooks of layers) {
|
|
246
278
|
const record = hooks;
|
|
247
279
|
for (const key of Object.getOwnPropertySymbols(record)) {
|
|
280
|
+
if (key === EARLY_REJECTION_HOOK_MARKER && merged.preBody !== undefined)
|
|
281
|
+
continue;
|
|
282
|
+
if (key === EARLY_REJECTION_HOOK_MARKER) {
|
|
283
|
+
const existing = merged[key];
|
|
284
|
+
const incoming = record[key];
|
|
285
|
+
merged[key] = [
|
|
286
|
+
...(Array.isArray(existing) ? existing : []),
|
|
287
|
+
...(Array.isArray(incoming) ? incoming : []),
|
|
288
|
+
];
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
248
291
|
if (!(key in merged)) {
|
|
249
292
|
merged[key] = record[key];
|
|
250
293
|
}
|
package/dist/compression.d.ts
CHANGED
|
@@ -38,6 +38,15 @@ export interface CompressionOptions {
|
|
|
38
38
|
* below `0` or above `2 ** 31 - 1` are refused at construction.
|
|
39
39
|
*/
|
|
40
40
|
minimumSize?: number;
|
|
41
|
+
/**
|
|
42
|
+
* Maximum response body size (in bytes) the middleware will buffer for
|
|
43
|
+
* compression. Larger (or unknown-and-growing) bodies are left uncompressed
|
|
44
|
+
* so a large GET cannot force unbounded heap growth. Default: `1_048_576`
|
|
45
|
+
* (1 MiB). Must be a positive integer.
|
|
46
|
+
*
|
|
47
|
+
* @since 1.0.0
|
|
48
|
+
*/
|
|
49
|
+
maxCompressibleBytes?: number;
|
|
41
50
|
/**
|
|
42
51
|
* Allowed encodings, in caller-preferred order. Defaults to
|
|
43
52
|
* `["br", "gzip", "deflate"]` — the middleware will pick the
|