@daloyjs/core 1.0.0-rc.4 → 1.0.0-rc.6
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 +34 -22
- package/dist/adapters/bun.d.ts +20 -2
- package/dist/adapters/bun.js +42 -7
- 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 +117 -46
- package/dist/app.d.ts +30 -4
- package/dist/app.js +187 -45
- package/dist/auto-ban.js +1 -3
- package/dist/bot-guard.js +30 -3
- package/dist/cli.js +9 -6
- package/dist/client.d.ts +36 -7
- package/dist/client.js +7 -0
- package/dist/compression.d.ts +9 -0
- package/dist/compression.js +72 -1
- package/dist/config.js +1 -3
- package/dist/conn-info.d.ts +5 -2
- package/dist/conn-info.js +5 -2
- package/dist/errors.d.ts +12 -3
- package/dist/errors.js +14 -8
- package/dist/etag.js +12 -2
- package/dist/fetch-guard.d.ts +27 -19
- package/dist/fetch-guard.js +50 -8
- package/dist/geo-block.js +4 -9
- package/dist/hashing.js +1 -1
- package/dist/http-signatures.d.ts +4 -1
- package/dist/http-signatures.js +16 -9
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/ip-reputation.js +1 -1
- package/dist/ip-restriction.js +3 -12
- package/dist/jwt.js +12 -14
- package/dist/logger.d.ts +45 -0
- package/dist/logger.js +135 -0
- package/dist/mcp.js +10 -9
- package/dist/middleware.js +33 -3
- package/dist/mtls.js +6 -1
- package/dist/multipart.js +9 -12
- package/dist/openapi.d.ts +1 -1
- package/dist/openapi.js +2 -2
- package/dist/rate-limit-redis.d.ts +4 -4
- package/dist/response-cache.d.ts +179 -21
- package/dist/response-cache.js +338 -29
- package/dist/router.d.ts +2 -2
- package/dist/router.js +24 -9
- package/dist/safe-redirect.d.ts +5 -1
- package/dist/safe-redirect.js +27 -3
- package/dist/sbom.cdx.json +9 -9
- package/dist/sbom.spdx.json +5 -5
- package/dist/security-schemes.js +1 -2
- package/dist/security.d.ts +41 -0
- package/dist/security.js +131 -15
- package/dist/session.d.ts +13 -2
- package/dist/session.js +111 -17
- package/dist/subdomains.js +1 -4
- package/dist/tenancy.d.ts +40 -0
- package/dist/tenancy.js +54 -3
- package/dist/time-claims.js +3 -1
- package/dist/waf.js +124 -32
- package/dist/webhook-delivery.js +19 -3
- package/dist/websocket.d.ts +8 -0
- package/dist/websocket.js +19 -4
- package/package.json +6 -5
package/dist/response-cache.d.ts
CHANGED
|
@@ -32,6 +32,31 @@
|
|
|
32
32
|
* the rate-limit store, with an in-memory {@link MemoryResponseCacheStore}
|
|
33
33
|
* default; supply a shared backend (e.g. Redis) for multi-instance fleets.
|
|
34
34
|
*
|
|
35
|
+
* ## Cross-principal isolation (CWE-524)
|
|
36
|
+
*
|
|
37
|
+
* A shared response cache is only as safe as its key. Anything that varies the
|
|
38
|
+
* response but not the key becomes a cross-principal disclosure: the next caller
|
|
39
|
+
* of the same URL receives the previous caller's private body. This module is
|
|
40
|
+
* fail-closed on every principal dimension the framework can see:
|
|
41
|
+
*
|
|
42
|
+
* - **Authority.** The key is built from the *effective request URI* (scheme +
|
|
43
|
+
* authority + path + query) per RFC 9111 §4, so one process serving several
|
|
44
|
+
* hostnames (vanity domains, subdomain-per-customer) never shares an entry
|
|
45
|
+
* across them.
|
|
46
|
+
* - **Credentials.** Requests carrying `Authorization` **or** `Cookie` bypass
|
|
47
|
+
* the shared cache entirely unless the caller is identified (see
|
|
48
|
+
* {@link ResponseCacheOptions.principal}) or the header is explicitly declared
|
|
49
|
+
* shareable (see {@link ResponseCacheOptions.cacheAuthenticatedRequests}).
|
|
50
|
+
* - **Tenant.** When `tenancy()` has resolved a tenant for the request, that
|
|
51
|
+
* tenant is folded into the key automatically — no `keyGenerator` wiring
|
|
52
|
+
* required, and it applies to a custom `keyGenerator` too.
|
|
53
|
+
* - **Declared variants.** A response's own `Vary` header is honoured as a
|
|
54
|
+
* secondary key (RFC 9111 §4.1): an entry is replayed only to a request whose
|
|
55
|
+
* values for those fields match the ones it was stored with. `cors()` emits
|
|
56
|
+
* `Vary: Origin` and `compression()` emits `Vary: Accept-Encoding`, so
|
|
57
|
+
* without this one caller's `Access-Control-Allow-Origin` — or their gzipped
|
|
58
|
+
* body — would be served to the next. `Vary: *` is never stored.
|
|
59
|
+
*
|
|
35
60
|
* This module is dependency-free and uses only Web Standard
|
|
36
61
|
* `Request`/`Response` + `Headers`, so it runs unchanged on Node, Bun, Deno,
|
|
37
62
|
* Cloudflare Workers, and Vercel.
|
|
@@ -40,6 +65,15 @@
|
|
|
40
65
|
* @since 0.37.0
|
|
41
66
|
*/
|
|
42
67
|
import type { BaseContext, Hooks } from "./types.js";
|
|
68
|
+
/**
|
|
69
|
+
* Marker stamped on the `Hooks` object returned by {@link responseCache}, so the
|
|
70
|
+
* `App` boot guard can detect a cache mounted *ahead of* `tenancy()` — an order
|
|
71
|
+
* in which the tenant is not yet in `ctx.state` when the cache key is built, and
|
|
72
|
+
* automatic tenant partitioning therefore cannot protect the entry.
|
|
73
|
+
*
|
|
74
|
+
* @since 1.0.0
|
|
75
|
+
*/
|
|
76
|
+
export declare const RESPONSE_CACHE_HOOK_MARKER: unique symbol;
|
|
43
77
|
/**
|
|
44
78
|
* Test-only helper that clears the process-wide shared stores used by
|
|
45
79
|
* `responseCache({ groupId })`. Not part of the documented public API.
|
|
@@ -64,6 +98,22 @@ export interface CachedResponse {
|
|
|
64
98
|
freshUntil: number;
|
|
65
99
|
/** End of the stale-while-revalidate window as ms since epoch. */
|
|
66
100
|
staleUntil: number;
|
|
101
|
+
/**
|
|
102
|
+
* Lower-cased request-header names from the stored response's `Vary` header —
|
|
103
|
+
* the secondary cache key per RFC 9111 §4.1. Absent when the response
|
|
104
|
+
* declared no `Vary`.
|
|
105
|
+
*
|
|
106
|
+
* @since 1.0.0
|
|
107
|
+
*/
|
|
108
|
+
vary?: string[];
|
|
109
|
+
/**
|
|
110
|
+
* The values {@link vary}'s fields had on the request that produced this
|
|
111
|
+
* entry, length-prefix encoded. A stored entry is only reusable for a request
|
|
112
|
+
* whose values re-encode identically.
|
|
113
|
+
*
|
|
114
|
+
* @since 1.0.0
|
|
115
|
+
*/
|
|
116
|
+
varyKey?: string;
|
|
67
117
|
}
|
|
68
118
|
/**
|
|
69
119
|
* Pluggable persistence backend for {@link responseCache}. All methods may be
|
|
@@ -118,13 +168,62 @@ export interface ResponseCacheOptions {
|
|
|
118
168
|
* Request header names whose values partition the cache (e.g.
|
|
119
169
|
* `["accept-language"]`). Their values are folded into the cache key.
|
|
120
170
|
* Default: none.
|
|
171
|
+
*
|
|
172
|
+
* @remarks This is the *proactive* dimension list, applied to every request
|
|
173
|
+
* before the handler runs. It is independent of — and additive to — the
|
|
174
|
+
* `Vary` header a response declares for itself, which the cache always
|
|
175
|
+
* honours as a secondary key (see {@link responseCache}).
|
|
121
176
|
*/
|
|
122
177
|
varyHeaders?: string[];
|
|
123
178
|
/**
|
|
124
|
-
*
|
|
179
|
+
* Extra response headers to drop before an entry is stored, on top of the
|
|
180
|
+
* built-in hop-by-hop / per-request set (`Age`, `Connection`,
|
|
181
|
+
* `Transfer-Encoding`, `X-Request-Id`, …).
|
|
182
|
+
*
|
|
183
|
+
* Supply the name of a custom correlation or tracing header so it is not
|
|
184
|
+
* frozen into the entry and replayed to every later caller — for example
|
|
185
|
+
* `requestId({ header: "x-correlation-id" })` pairs with
|
|
186
|
+
* `excludeHeaders: ["x-correlation-id"]`.
|
|
187
|
+
*
|
|
188
|
+
* @since 1.0.0
|
|
189
|
+
*/
|
|
190
|
+
excludeHeaders?: readonly string[];
|
|
191
|
+
/**
|
|
192
|
+
* Derive the cache key **body** from the request. Default: method + the
|
|
193
|
+
* effective request URI (scheme + authority + path + query) +
|
|
125
194
|
* {@link varyHeaders} values. Return `null` to skip caching for this request.
|
|
195
|
+
*
|
|
196
|
+
* The resolved tenant and {@link principal} partition is applied *around*
|
|
197
|
+
* whatever this returns, so a custom generator does not have to (and should
|
|
198
|
+
* not bother to) fold them in itself — it cannot accidentally widen the
|
|
199
|
+
* partition below what the framework knows about the caller.
|
|
126
200
|
*/
|
|
127
201
|
keyGenerator?: (ctx: BaseContext<any, any>) => string | null;
|
|
202
|
+
/**
|
|
203
|
+
* Identify the caller, so responses to credentialed requests can be cached
|
|
204
|
+
* *per principal* instead of bypassing the cache.
|
|
205
|
+
*
|
|
206
|
+
* Return a stable id for the calling principal (user id, tenant id, API-key
|
|
207
|
+
* fingerprint — never the raw credential), or `null` / `undefined` when the
|
|
208
|
+
* request is anonymous. The returned id is folded into the cache key.
|
|
209
|
+
*
|
|
210
|
+
* This is what makes cookie-authenticated caching safe: a session cookie
|
|
211
|
+
* identifies a user that the cache key would otherwise ignore, so without a
|
|
212
|
+
* `principal` such a request is not cached at all.
|
|
213
|
+
*
|
|
214
|
+
* ```ts
|
|
215
|
+
* responseCache({
|
|
216
|
+
* ttlSeconds: 30,
|
|
217
|
+
* principal: (ctx) => ctx.state.session?.get<string>("userId") ?? null,
|
|
218
|
+
* });
|
|
219
|
+
* ```
|
|
220
|
+
*
|
|
221
|
+
* @remarks Returning `null` for a request that *does* carry credentials is
|
|
222
|
+
* treated as "cannot identify this caller", and the request bypasses the cache
|
|
223
|
+
* rather than sharing an anonymous entry.
|
|
224
|
+
* @since 1.0.0
|
|
225
|
+
*/
|
|
226
|
+
principal?: (ctx: BaseContext<any, any>) => string | null | undefined;
|
|
128
227
|
/**
|
|
129
228
|
* Maximum response body size (bytes) the middleware will buffer and store.
|
|
130
229
|
* Larger responses pass through uncached. Default: `1048576` (1 MiB).
|
|
@@ -142,33 +241,79 @@ export interface ResponseCacheOptions {
|
|
|
142
241
|
*/
|
|
143
242
|
groupId?: string;
|
|
144
243
|
/**
|
|
145
|
-
* Whether to cache responses to requests that carry an
|
|
146
|
-
* header. Default: `false
|
|
244
|
+
* Whether to cache responses to requests that carry credentials — an
|
|
245
|
+
* `Authorization` header or a `Cookie` header. Default: `false` for both.
|
|
246
|
+
*
|
|
247
|
+
* A shared response cache keyed on the request URI does not include the
|
|
248
|
+
* credential, so caching a credentialed response would serve one user's
|
|
249
|
+
* private data to the next caller of the same URL (CWE-524 — cross-principal
|
|
250
|
+
* cached-response disclosure). Per RFC 9111 §3.5 a shared cache MUST NOT
|
|
251
|
+
* reuse a response to an `Authorization`-bearing request unless explicitly
|
|
252
|
+
* permitted; `Cookie` is treated the same way because a session cookie is the
|
|
253
|
+
* single most common way a response is made private.
|
|
147
254
|
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* (CWE-524 — cross-tenant cached-response disclosure). For that reason, and
|
|
152
|
-
* per RFC 9111 §3.5 (a shared cache MUST NOT reuse a response to an
|
|
153
|
-
* `Authorization`-bearing request unless explicitly permitted), such
|
|
154
|
-
* requests bypass the cache entirely by default.
|
|
255
|
+
* Pass a boolean to set both, or an object to control them independently —
|
|
256
|
+
* useful when a public endpoint receives unrelated analytics cookies but must
|
|
257
|
+
* never cache bearer-authenticated responses:
|
|
155
258
|
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
* {@link keyGenerator} so distinct callers cannot collide.
|
|
259
|
+
* ```ts
|
|
260
|
+
* responseCache({ cacheAuthenticatedRequests: { cookie: true } });
|
|
261
|
+
* ```
|
|
160
262
|
*
|
|
161
|
-
*
|
|
263
|
+
* Enable a dimension only when the response is genuinely shareable across
|
|
264
|
+
* principals (e.g. public reference data behind a bearer gate). Otherwise
|
|
265
|
+
* prefer {@link principal}, which keeps caching *and* keeps callers apart.
|
|
266
|
+
*
|
|
267
|
+
* @remarks Declaring the credential header in {@link varyHeaders} also counts
|
|
268
|
+
* as handling it, since its value then partitions the key.
|
|
269
|
+
* @since 0.40.0 — extended to `Cookie` and per-header control in 1.0.0.
|
|
162
270
|
*/
|
|
163
|
-
cacheAuthenticatedRequests?: boolean
|
|
271
|
+
cacheAuthenticatedRequests?: boolean | {
|
|
272
|
+
authorization?: boolean;
|
|
273
|
+
cookie?: boolean;
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
/** Options for {@link MemoryResponseCacheStore}. */
|
|
277
|
+
export interface MemoryResponseCacheStoreOptions {
|
|
278
|
+
/**
|
|
279
|
+
* Hard ceiling on retained entries. Default: `10_000`. Once reached, the
|
|
280
|
+
* oldest-inserted entries are evicted — expired ones first.
|
|
281
|
+
*/
|
|
282
|
+
maxEntries?: number;
|
|
283
|
+
/**
|
|
284
|
+
* Hard ceiling on retained body bytes. Default: `64 * 1024 * 1024` (64 MiB).
|
|
285
|
+
*
|
|
286
|
+
* An entry count alone does not bound memory: with the module's default
|
|
287
|
+
* `maxBodyBytes` of 1 MiB, ten thousand entries is ten gigabytes. This is the
|
|
288
|
+
* limit that actually caps the store's footprint.
|
|
289
|
+
*/
|
|
290
|
+
maxBytes?: number;
|
|
164
291
|
}
|
|
165
292
|
/**
|
|
166
293
|
* In-memory {@link ResponseCacheStore}. Suitable for tests and single-process
|
|
167
|
-
* deployments.
|
|
168
|
-
*
|
|
294
|
+
* deployments.
|
|
295
|
+
*
|
|
296
|
+
* Expired entries are dropped on access. The map is bounded on **both** entry
|
|
297
|
+
* count and retained body bytes ({@link MemoryResponseCacheStoreOptions}):
|
|
298
|
+
* pruning expired entries alone cannot bound it, because every entry in a burst
|
|
299
|
+
* of requests for distinct URLs is unexpired for the whole TTL. An attacker
|
|
300
|
+
* rotating a query string would otherwise grow the map without limit until the
|
|
301
|
+
* process runs out of memory.
|
|
302
|
+
*
|
|
303
|
+
* Eviction is FIFO over insertion order (expired entries first), which `Map`
|
|
304
|
+
* gives in O(1) per eviction.
|
|
169
305
|
*/
|
|
170
306
|
export declare class MemoryResponseCacheStore implements ResponseCacheStore {
|
|
171
307
|
private readonly map;
|
|
308
|
+
private readonly maxEntries;
|
|
309
|
+
private readonly maxBytes;
|
|
310
|
+
/** Running sum of `entry.body.length` over `map`, kept in step with writes. */
|
|
311
|
+
private bytes;
|
|
312
|
+
/**
|
|
313
|
+
* @param opts - Capacity limits; see {@link MemoryResponseCacheStoreOptions}.
|
|
314
|
+
* @throws TypeError if either limit is not a positive integer.
|
|
315
|
+
*/
|
|
316
|
+
constructor(opts?: MemoryResponseCacheStoreOptions);
|
|
172
317
|
/** @inheritDoc */
|
|
173
318
|
get(key: string): CachedResponse | null;
|
|
174
319
|
/**
|
|
@@ -179,7 +324,13 @@ export declare class MemoryResponseCacheStore implements ResponseCacheStore {
|
|
|
179
324
|
set(key: string, entry: CachedResponse, _ttlMs?: number): void;
|
|
180
325
|
/** @inheritDoc */
|
|
181
326
|
delete(key: string): void;
|
|
182
|
-
|
|
327
|
+
/** Remove one entry, keeping the byte counter in step. */
|
|
328
|
+
private drop;
|
|
329
|
+
/**
|
|
330
|
+
* Bring the map back under both limits: expired entries first, then
|
|
331
|
+
* oldest-inserted, since `Map` iterates in insertion order.
|
|
332
|
+
*/
|
|
333
|
+
private evict;
|
|
183
334
|
/** Test helper. Remove every entry. */
|
|
184
335
|
clear(): void;
|
|
185
336
|
/** Test helper. Number of stored entries (including expired). */
|
|
@@ -201,10 +352,17 @@ export declare class MemoryResponseCacheStore implements ResponseCacheStore {
|
|
|
201
352
|
*
|
|
202
353
|
* Request `Cache-Control: no-store` bypasses the cache entirely; `no-cache`
|
|
203
354
|
* bypasses the read but still refreshes the stored entry. Responses marked
|
|
204
|
-
* `no-store` / `private` / `no-cache`, carrying `Set-Cookie
|
|
205
|
-
* {@link ResponseCacheOptions.cacheableStatus}, or larger than
|
|
355
|
+
* `no-store` / `private` / `no-cache`, carrying `Set-Cookie` or `Vary: *`,
|
|
356
|
+
* failing {@link ResponseCacheOptions.cacheableStatus}, or larger than
|
|
206
357
|
* {@link ResponseCacheOptions.maxBodyBytes} are never cached.
|
|
207
358
|
*
|
|
359
|
+
* A response that declares `Vary` is stored as a **variant**: the request's
|
|
360
|
+
* values for those fields are recorded alongside it, and the entry is replayed
|
|
361
|
+
* only to a request whose values match. A mismatch is a miss, so the handler
|
|
362
|
+
* runs and the entry is re-stored for that variant. This applies to `Vary`
|
|
363
|
+
* written by any middleware in the chain — notably `cors()` (`Origin`) and
|
|
364
|
+
* `compression()` (`Accept-Encoding`) — with no configuration.
|
|
365
|
+
*
|
|
208
366
|
* @example
|
|
209
367
|
* ```ts
|
|
210
368
|
* import { App, responseCache } from "@daloyjs/core";
|