@daloyjs/core 1.0.0-rc.7 → 1.0.0-rc.9
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/dist/adapters/node.js +76 -14
- package/dist/app.d.ts +25 -0
- package/dist/app.js +84 -0
- package/dist/auto-ban.d.ts +49 -5
- package/dist/auto-ban.js +99 -24
- package/dist/bot-guard.d.ts +2 -2
- package/dist/bot-guard.js +6 -1
- package/dist/geo-block.d.ts +3 -3
- package/dist/geo-block.js +7 -1
- package/dist/idempotency.d.ts +69 -2
- package/dist/idempotency.js +151 -7
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/ip-reputation.d.ts +2 -2
- package/dist/ip-reputation.js +6 -1
- package/dist/ip-restriction.d.ts +3 -3
- package/dist/ip-restriction.js +7 -2
- package/dist/mcp.d.ts +4 -12
- package/dist/safe-redirect.js +19 -0
- package/dist/sbom.cdx.json +9 -9
- package/dist/sbom.spdx.json +5 -5
- package/dist/types.d.ts +31 -0
- package/dist/waf.js +38 -2
- package/dist/websocket.d.ts +48 -5
- package/dist/websocket.js +57 -5
- package/package.json +6 -4
package/dist/idempotency.d.ts
CHANGED
|
@@ -164,17 +164,71 @@ export interface IdempotencyOptions {
|
|
|
164
164
|
* unauthenticated idempotent writes). Returning a stable per-user id is
|
|
165
165
|
* preferable to the raw credential when tokens rotate between retries.
|
|
166
166
|
*
|
|
167
|
+
* **Supply this whenever `Authorization` is not per-user.** The default assumes
|
|
168
|
+
* that header names one caller. If it is shared — a per-tenant API key, a
|
|
169
|
+
* service token, a gateway credential — with end users distinguished some other
|
|
170
|
+
* way (a session cookie, a subject claim your app reads), then the default
|
|
171
|
+
* partitions per *tenant* and every user inside one tenant shares a namespace.
|
|
172
|
+
* A caller who knows another's `Idempotency-Key` can then replay their stored
|
|
173
|
+
* response. This is not detectable from the header alone, which is why it is
|
|
174
|
+
* your call rather than a framework guard: a resolvable-but-coarse scope looks
|
|
175
|
+
* identical to a correctly per-user one. The cookie-only case *is* guarded —
|
|
176
|
+
* see {@link allowUnscopedCallers}.
|
|
177
|
+
*
|
|
178
|
+
* Independently of scoping, a replay never re-issues `Set-Cookie`, so a coarse
|
|
179
|
+
* namespace cannot escalate from disclosing a response body into handing over a
|
|
180
|
+
* live session.
|
|
181
|
+
*
|
|
167
182
|
* @since 0.40.0
|
|
168
183
|
*/
|
|
169
184
|
scope?: (ctx: BaseContext<any, any>) => string | undefined | Promise<string | undefined>;
|
|
185
|
+
/**
|
|
186
|
+
* Accept callers the default {@link scope} resolver cannot identify, letting
|
|
187
|
+
* them share one idempotency namespace. Default `false`.
|
|
188
|
+
*
|
|
189
|
+
* The guard this disables exists because a cookie-authenticated request
|
|
190
|
+
* carries no `Authorization` header, so the default resolver returns
|
|
191
|
+
* `undefined` and the namespace collapses. The retry fingerprint (method +
|
|
192
|
+
* path + body) is then all that separates two users — and two users
|
|
193
|
+
* submitting the same payload fingerprint identically, so one replays the
|
|
194
|
+
* other's stored response (CWE-524). Rather than share the namespace
|
|
195
|
+
* silently, a cookie-bearing request with no resolvable scope throws with an
|
|
196
|
+
* actionable message.
|
|
197
|
+
*
|
|
198
|
+
* Set this to `true` only when unscoped callers are genuinely interchangeable
|
|
199
|
+
* — a public, unauthenticated idempotent write where no response body is
|
|
200
|
+
* caller-specific. Supplying {@link scope} is almost always the right answer
|
|
201
|
+
* instead. A custom `scope` bypasses the guard entirely, including when it
|
|
202
|
+
* returns `undefined`, because an explicit resolver owns its own posture.
|
|
203
|
+
*
|
|
204
|
+
* @since 1.0.0-rc.8
|
|
205
|
+
*/
|
|
206
|
+
allowUnscopedCallers?: boolean;
|
|
170
207
|
}
|
|
171
208
|
/**
|
|
172
209
|
* In-memory {@link IdempotencyStore}. Suitable for tests and single-process
|
|
173
|
-
* deployments. Expired records are dropped on access
|
|
174
|
-
*
|
|
210
|
+
* deployments. Expired records are dropped on access.
|
|
211
|
+
*
|
|
212
|
+
* Growth is bounded by {@link maxEntries}: an expiry sweep runs first, and if the
|
|
213
|
+
* store is still at the cap the oldest surviving record is evicted. The sweep
|
|
214
|
+
* alone is not a bound — it only drops records that have *expired*, so a stream
|
|
215
|
+
* of unique keys inside the TTL grew the map linearly no matter how often it ran,
|
|
216
|
+
* with each entry pinning a stored response body.
|
|
217
|
+
*
|
|
218
|
+
* Evicting a live record can only cost exactly-once semantics for a retry that
|
|
219
|
+
* arrives after the eviction — it re-executes rather than replaying. That is the
|
|
220
|
+
* right trade against unbounded memory, but it is a reason to supply a shared
|
|
221
|
+
* (e.g. Redis) store for any deployment where the key volume approaches the cap.
|
|
175
222
|
*/
|
|
176
223
|
export declare class MemoryIdempotencyStore implements IdempotencyStore {
|
|
177
224
|
private readonly map;
|
|
225
|
+
private readonly maxEntries;
|
|
226
|
+
/**
|
|
227
|
+
* @param maxEntries - Maximum live records retained. Must be a positive
|
|
228
|
+
* integer. Default {@link DEFAULT_MAX_IDEMPOTENCY_ENTRIES} (10 000).
|
|
229
|
+
* @throws Error when `maxEntries` is not a positive integer.
|
|
230
|
+
*/
|
|
231
|
+
constructor(maxEntries?: number);
|
|
178
232
|
/**
|
|
179
233
|
* @inheritDoc
|
|
180
234
|
* `_ttlMs` is part of the {@link IdempotencyStore} contract but unused here:
|
|
@@ -192,6 +246,19 @@ export declare class MemoryIdempotencyStore implements IdempotencyStore {
|
|
|
192
246
|
/** Test helper. Number of stored records (including expired). */
|
|
193
247
|
size(): number;
|
|
194
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* Internal Symbol stamped on the hooks {@link idempotency} returns, so `App`'s
|
|
251
|
+
* boot guards can see where a stored-response replay sits in a route's effective
|
|
252
|
+
* hook chain.
|
|
253
|
+
*
|
|
254
|
+
* A replay is returned from `beforeHandle` and ends the hook chain, which means
|
|
255
|
+
* anything enforcing from the *same* phase but mounted later never runs. That is
|
|
256
|
+
* how a `rateLimit()` behind a replay stops counting. The marker lets the boot
|
|
257
|
+
* path refuse that order rather than leave the limiter silently infinite.
|
|
258
|
+
*
|
|
259
|
+
* @internal
|
|
260
|
+
*/
|
|
261
|
+
export declare const IDEMPOTENCY_HOOK_MARKER: unique symbol;
|
|
195
262
|
/**
|
|
196
263
|
* Idempotency-key middleware. Mount it ahead of the routes that need
|
|
197
264
|
* exactly-once semantics under retries (typically the payment / write
|
package/dist/idempotency.js
CHANGED
|
@@ -55,13 +55,44 @@ export function _resetSharedIdempotencyStoresForTests() {
|
|
|
55
55
|
SHARED_IDEMPOTENCY_STORES.clear();
|
|
56
56
|
}
|
|
57
57
|
// ---------- Default store ----------
|
|
58
|
+
/**
|
|
59
|
+
* Default cap on live records held by {@link MemoryIdempotencyStore}.
|
|
60
|
+
*
|
|
61
|
+
* Each record can hold a base64 response body up to
|
|
62
|
+
* {@link IdempotencyOptions.maxResponseBytes} (1 MiB by default), so the cap is
|
|
63
|
+
* what actually bounds this store's footprint. Matches the size at which the
|
|
64
|
+
* store already attempted an expiry sweep.
|
|
65
|
+
*/
|
|
66
|
+
const DEFAULT_MAX_IDEMPOTENCY_ENTRIES = 10_000;
|
|
58
67
|
/**
|
|
59
68
|
* In-memory {@link IdempotencyStore}. Suitable for tests and single-process
|
|
60
|
-
* deployments. Expired records are dropped on access
|
|
61
|
-
*
|
|
69
|
+
* deployments. Expired records are dropped on access.
|
|
70
|
+
*
|
|
71
|
+
* Growth is bounded by {@link maxEntries}: an expiry sweep runs first, and if the
|
|
72
|
+
* store is still at the cap the oldest surviving record is evicted. The sweep
|
|
73
|
+
* alone is not a bound — it only drops records that have *expired*, so a stream
|
|
74
|
+
* of unique keys inside the TTL grew the map linearly no matter how often it ran,
|
|
75
|
+
* with each entry pinning a stored response body.
|
|
76
|
+
*
|
|
77
|
+
* Evicting a live record can only cost exactly-once semantics for a retry that
|
|
78
|
+
* arrives after the eviction — it re-executes rather than replaying. That is the
|
|
79
|
+
* right trade against unbounded memory, but it is a reason to supply a shared
|
|
80
|
+
* (e.g. Redis) store for any deployment where the key volume approaches the cap.
|
|
62
81
|
*/
|
|
63
82
|
export class MemoryIdempotencyStore {
|
|
64
83
|
map = new Map();
|
|
84
|
+
maxEntries;
|
|
85
|
+
/**
|
|
86
|
+
* @param maxEntries - Maximum live records retained. Must be a positive
|
|
87
|
+
* integer. Default {@link DEFAULT_MAX_IDEMPOTENCY_ENTRIES} (10 000).
|
|
88
|
+
* @throws Error when `maxEntries` is not a positive integer.
|
|
89
|
+
*/
|
|
90
|
+
constructor(maxEntries = DEFAULT_MAX_IDEMPOTENCY_ENTRIES) {
|
|
91
|
+
if (!Number.isInteger(maxEntries) || maxEntries < 1) {
|
|
92
|
+
throw new Error(`MemoryIdempotencyStore: maxEntries must be a positive integer; got ${String(maxEntries)}.`);
|
|
93
|
+
}
|
|
94
|
+
this.maxEntries = maxEntries;
|
|
95
|
+
}
|
|
65
96
|
/**
|
|
66
97
|
* @inheritDoc
|
|
67
98
|
* `_ttlMs` is part of the {@link IdempotencyStore} contract but unused here:
|
|
@@ -71,9 +102,19 @@ export class MemoryIdempotencyStore {
|
|
|
71
102
|
const existing = this.read(key);
|
|
72
103
|
if (existing)
|
|
73
104
|
return existing;
|
|
74
|
-
this.map.
|
|
75
|
-
if (this.map.size > 10_000)
|
|
105
|
+
if (this.map.size >= this.maxEntries) {
|
|
76
106
|
this.prune();
|
|
107
|
+
// Still full: every record is live, so drop the oldest. `Map` iterates in
|
|
108
|
+
// insertion order, and `complete()` overwrites in place rather than
|
|
109
|
+
// re-inserting, so the first key is the least recently reserved.
|
|
110
|
+
while (this.map.size >= this.maxEntries) {
|
|
111
|
+
const oldest = this.map.keys().next();
|
|
112
|
+
if (oldest.done)
|
|
113
|
+
break;
|
|
114
|
+
this.map.delete(oldest.value);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
this.map.set(key, record);
|
|
77
118
|
return null;
|
|
78
119
|
}
|
|
79
120
|
/** @inheritDoc */
|
|
@@ -185,25 +226,87 @@ function validateKey(key, headerName, maxLen) {
|
|
|
185
226
|
throw new BadRequestError(`${headerName} header contains invalid characters.`);
|
|
186
227
|
}
|
|
187
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* Response headers that must never be stored for replay.
|
|
231
|
+
*
|
|
232
|
+
* `set-cookie` is the security-critical entry. A stored response was replayed
|
|
233
|
+
* with every header the original produced, so a `Set-Cookie` issued to the first
|
|
234
|
+
* caller was re-issued to whoever replayed the record. Combined with any
|
|
235
|
+
* coarse-scoped namespace (a shared tenant `Authorization`, or an explicit
|
|
236
|
+
* `allowUnscopedCallers: true`) that promotes a body disclosure into handing over
|
|
237
|
+
* a live session — account takeover rather than data leakage. Even for a
|
|
238
|
+
* correctly scoped, genuinely same-caller retry it is wrong: the replay would
|
|
239
|
+
* resurrect a cookie the handler set once, silently rolling back a session
|
|
240
|
+
* rotation performed at login or on a privilege change.
|
|
241
|
+
*
|
|
242
|
+
* Unlike `responseCache()`, which refuses to store a `Set-Cookie` response at
|
|
243
|
+
* all, idempotency strips and stores: declining to store would release the
|
|
244
|
+
* reservation and let a retry re-execute the handler, which for a payment or
|
|
245
|
+
* order endpoint is the double-charge this middleware exists to prevent.
|
|
246
|
+
* Stripping keeps exactly-once semantics and drops only the header that must not
|
|
247
|
+
* be replayed.
|
|
248
|
+
*
|
|
249
|
+
* The remainder are hop-by-hop or per-request fields (RFC 9110 §7.6.1) that
|
|
250
|
+
* describe the one connection or the one request that populated the record;
|
|
251
|
+
* replaying `x-request-id` in particular hands every later caller a correlation
|
|
252
|
+
* id belonging to someone else's request.
|
|
253
|
+
*/
|
|
254
|
+
const NEVER_REPLAYED_HEADERS = new Set([
|
|
255
|
+
"set-cookie",
|
|
256
|
+
"set-cookie2",
|
|
257
|
+
"age",
|
|
258
|
+
"connection",
|
|
259
|
+
"keep-alive",
|
|
260
|
+
"proxy-authenticate",
|
|
261
|
+
"proxy-authorization",
|
|
262
|
+
"te",
|
|
263
|
+
"trailer",
|
|
264
|
+
"transfer-encoding",
|
|
265
|
+
"upgrade",
|
|
266
|
+
"x-request-id",
|
|
267
|
+
]);
|
|
188
268
|
async function captureResponse(res, maxBytes) {
|
|
189
269
|
const buf = new Uint8Array(await res.clone().arrayBuffer());
|
|
190
270
|
if (buf.byteLength > maxBytes)
|
|
191
271
|
return null;
|
|
192
272
|
const headers = [];
|
|
193
273
|
res.headers.forEach((value, name) => {
|
|
194
|
-
|
|
274
|
+
// Filter on capture, not on replay, so a credential never reaches the store
|
|
275
|
+
// in the first place — a shared (Redis) store would otherwise persist one
|
|
276
|
+
// caller's session cookie for the whole TTL.
|
|
277
|
+
if (!NEVER_REPLAYED_HEADERS.has(name.toLowerCase()))
|
|
278
|
+
headers.push([name, value]);
|
|
195
279
|
});
|
|
196
280
|
return { status: res.status, headers, body: buf.byteLength ? bytesToBase64(buf) : "" };
|
|
197
281
|
}
|
|
198
282
|
function buildReplayResponse(stored, replayHeaderName) {
|
|
199
283
|
const headers = new Headers();
|
|
200
|
-
for (const [name, value] of stored.headers)
|
|
284
|
+
for (const [name, value] of stored.headers) {
|
|
285
|
+
// Filtered on capture already; re-checked here so a record written by an
|
|
286
|
+
// older build — or by any other writer sharing the same Redis store — cannot
|
|
287
|
+
// replay a credential either.
|
|
288
|
+
if (NEVER_REPLAYED_HEADERS.has(name.toLowerCase()))
|
|
289
|
+
continue;
|
|
201
290
|
headers.set(name, value);
|
|
291
|
+
}
|
|
202
292
|
headers.set(replayHeaderName, "true");
|
|
203
293
|
const body = stored.body ? base64ToBytes(stored.body) : null;
|
|
204
294
|
return markSchemaValidatedResponse(new Response(body, { status: stored.status, headers }));
|
|
205
295
|
}
|
|
206
296
|
// ---------- Middleware ----------
|
|
297
|
+
/**
|
|
298
|
+
* Internal Symbol stamped on the hooks {@link idempotency} returns, so `App`'s
|
|
299
|
+
* boot guards can see where a stored-response replay sits in a route's effective
|
|
300
|
+
* hook chain.
|
|
301
|
+
*
|
|
302
|
+
* A replay is returned from `beforeHandle` and ends the hook chain, which means
|
|
303
|
+
* anything enforcing from the *same* phase but mounted later never runs. That is
|
|
304
|
+
* how a `rateLimit()` behind a replay stops counting. The marker lets the boot
|
|
305
|
+
* path refuse that order rather than leave the limiter silently infinite.
|
|
306
|
+
*
|
|
307
|
+
* @internal
|
|
308
|
+
*/
|
|
309
|
+
export const IDEMPOTENCY_HOOK_MARKER = Symbol.for("daloyjs.idempotency.hook");
|
|
207
310
|
/**
|
|
208
311
|
* Idempotency-key middleware. Mount it ahead of the routes that need
|
|
209
312
|
* exactly-once semantics under retries (typically the payment / write
|
|
@@ -252,6 +355,7 @@ export function idempotency(opts = {}) {
|
|
|
252
355
|
const replayHeaderName = (opts.replayHeaderName ?? "idempotency-replayed").toLowerCase();
|
|
253
356
|
const methods = new Set((opts.methods ?? ["POST", "PUT", "PATCH", "DELETE"]).map((m) => m.toUpperCase()));
|
|
254
357
|
const requireKey = opts.requireKey === true;
|
|
358
|
+
const allowUnscopedCallers = opts.allowUnscopedCallers === true;
|
|
255
359
|
const cacheableStatus = opts.cacheableStatus ?? ((status) => status < 500);
|
|
256
360
|
const ttlMs = ttlSeconds * 1_000;
|
|
257
361
|
let store;
|
|
@@ -270,7 +374,7 @@ export function idempotency(opts = {}) {
|
|
|
270
374
|
store = new MemoryIdempotencyStore();
|
|
271
375
|
}
|
|
272
376
|
const keyPrefix = opts.groupId ? `${opts.groupId}:` : "";
|
|
273
|
-
|
|
377
|
+
const hooks = {
|
|
274
378
|
async beforeHandle(ctx) {
|
|
275
379
|
const method = ctx.request.method.toUpperCase();
|
|
276
380
|
if (!methods.has(method))
|
|
@@ -291,6 +395,44 @@ export function idempotency(opts = {}) {
|
|
|
291
395
|
const scopeRaw = opts.scope
|
|
292
396
|
? await opts.scope(ctx)
|
|
293
397
|
: (ctx.request.headers.get("authorization") ?? undefined);
|
|
398
|
+
// A credentialed request the default resolver cannot see is the dangerous
|
|
399
|
+
// case: cookie-session auth sends no `Authorization`, so `scopeRaw` is
|
|
400
|
+
// undefined, the namespace collapses to the shared one, and the retry
|
|
401
|
+
// fingerprint (method + path + body) becomes the only thing separating two
|
|
402
|
+
// users. Two users legitimately submit the same payload fingerprint
|
|
403
|
+
// identically — so client B replays client A's stored response, which is
|
|
404
|
+
// exactly the CWE-524 disclosure `scope` exists to prevent.
|
|
405
|
+
//
|
|
406
|
+
// Fail loudly rather than share the namespace. This mirrors
|
|
407
|
+
// `responseCache()`, which treats `Cookie` as a credential alongside
|
|
408
|
+
// `Authorization` for the same reason. Only fires when a cookie is present
|
|
409
|
+
// *and* nothing resolved, so the documented bearer-token path is untouched
|
|
410
|
+
// and a genuinely anonymous caller still shares the unscoped namespace.
|
|
411
|
+
//
|
|
412
|
+
// Deliberately NOT widened to "any cookie-bearing request". A resolvable
|
|
413
|
+
// scope can still be too coarse — a per-tenant API key with
|
|
414
|
+
// cookie-identified end users partitions per tenant while every user inside
|
|
415
|
+
// one shares a namespace — but that is indistinguishable from the far more
|
|
416
|
+
// common shape of a *per-user* bearer token arriving alongside incidental
|
|
417
|
+
// browser cookies (analytics, consent, CSRF), where the default is already
|
|
418
|
+
// correct. Keying the guard on the cookie's presence rejects that setup with
|
|
419
|
+
// a 500, so the check stays where the default is provably useless rather
|
|
420
|
+
// than merely possibly coarse. Callers whose `Authorization` is shared
|
|
421
|
+
// across users must pass `scope` — see the TSDoc on
|
|
422
|
+
// {@link IdempotencyOptions.scope}. Independently of scoping, `Set-Cookie`
|
|
423
|
+
// is never stored or replayed (see {@link NEVER_REPLAYED_HEADERS}), so a
|
|
424
|
+
// coarse namespace cannot escalate into handing over a live session.
|
|
425
|
+
if (!opts.scope &&
|
|
426
|
+
scopeRaw === undefined &&
|
|
427
|
+
!allowUnscopedCallers &&
|
|
428
|
+
ctx.request.headers.has("cookie")) {
|
|
429
|
+
throw new Error("idempotency(): cannot determine the calling principal for a cookie-bearing request. " +
|
|
430
|
+
"The default scope reads the Authorization header, which this request does not carry, " +
|
|
431
|
+
"so every cookie-authenticated caller would share one idempotency namespace and could " +
|
|
432
|
+
"replay another caller's stored response (CWE-524). Pass " +
|
|
433
|
+
"`scope: (ctx) => ctx.state.session?.id` (or another stable per-caller id), or set " +
|
|
434
|
+
"`allowUnscopedCallers: true` if these callers are genuinely interchangeable.");
|
|
435
|
+
}
|
|
294
436
|
const scopeTag = scopeRaw ? `${await sha256Hex(scopeRaw)}:` : "";
|
|
295
437
|
const storeKey = `${keyPrefix}${scopeTag}${key}`;
|
|
296
438
|
const now = Date.now();
|
|
@@ -354,6 +496,8 @@ export function idempotency(opts = {}) {
|
|
|
354
496
|
return undefined;
|
|
355
497
|
},
|
|
356
498
|
};
|
|
499
|
+
hooks[IDEMPOTENCY_HOOK_MARKER] = true;
|
|
500
|
+
return hooks;
|
|
357
501
|
}
|
|
358
502
|
function isPromiseLike(value) {
|
|
359
503
|
return (value !== null &&
|
package/dist/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export { subdomains, PSL_SNAPSHOT_DATE, PSL_PUBLIC_SUFFIXES, MAX_SNAPSHOT_AGE_DA
|
|
|
12
12
|
export type { SubdomainsOptions, SubdomainsResult } from "./subdomains.js";
|
|
13
13
|
export { defineDependency, DEPENDENCY_MARKER } from "./dependency.js";
|
|
14
14
|
export type { DependencyHooks, DependencyOptions } from "./dependency.js";
|
|
15
|
-
export type { RouteDefinition, HttpMethod, PathString, RequestSchemas, ResponsesMap, ResponseSpec, AuthSpec, Hooks, BaseContext, PreBodyContext, AppState, AuthScheme, AuthContext, HandlerReturn, InferRequest, ParamsOf, PathParams, CallbackDefinition, CallbackMap, CallbackOperation, RouteExample, RouteMeta, } from "./types.js";
|
|
15
|
+
export type { RouteDefinition, HttpMethod, PathString, RequestSchemas, ResponsesMap, ResponseSpec, AuthSpec, Hooks, BaseContext, PreBodyContext, IdentityGateContext, AppState, AuthScheme, AuthContext, HandlerReturn, InferRequest, ParamsOf, PathParams, CallbackDefinition, CallbackMap, CallbackOperation, RouteExample, RouteMeta, } from "./types.js";
|
|
16
16
|
export { HttpError, BadRequestError, ValidationError, NotFoundError, ConflictError, UnauthorizedError, ForbiddenError, MethodNotAllowedError, PayloadTooLargeError, RequestHeaderFieldsTooLargeError, UnsupportedMediaTypeError, TooManyRequestsError, RequestTimeoutError, InternalError, MessageLeakError, httpError, SAFE_CUSTOM_ERROR_RESPONSE_HEADERS, checkCustomErrorResponseHeaders, } from "./errors.js";
|
|
17
17
|
export type { ProblemDetails, ProblemRenderOptions, HttpErrorOptions } from "./errors.js";
|
|
18
18
|
export type { StandardSchemaV1 } from "./schema.js";
|
|
@@ -101,5 +101,5 @@ export { otelTracing, TRACING_SPAN_KIND_SERVER, TRACING_SPAN_STATUS_UNSET, TRACI
|
|
|
101
101
|
export type { OtelTracingOptions, TracingAttributes, TracingAttributeValue, TracingSpan, TracingStartSpanOptions, TracingTracer, } from "./tracing.js";
|
|
102
102
|
export { tenancy, tenantScope, tenantFromSubdomain, tenantFromHeader, tenantFromPathPrefix, tenantFromClaim, defaultTenantNormalize, TENANCY_HOOK_MARKER, TENANCY_RESOLVED_MARKER, TENANT_UNRESOLVED, } from "./tenancy.js";
|
|
103
103
|
export type { TenancyOptions, TenantResolver, TenantScopeOptions, SubdomainTenantOptions, PathPrefixTenantOptions, ClaimTenantOptions, UnresolvedStatus, InvalidStatus, } from "./tenancy.js";
|
|
104
|
-
export { defineWebSocket, WebSocketRegistry, WebSocketProtocolError, WebSocketPayloadTooLargeError, WS_GUID, WS_READY_STATE, WS_OPCODE, WS_CLOSE_CODE, WS_MAX_CONTROL_PAYLOAD, DEFAULT_WS_BACKPRESSURE_LIMIT, DEFAULT_WS_MAX_PAYLOAD_LENGTH, DEFAULT_WS_IDLE_TIMEOUT_SECONDS, computeAcceptKey, parseSubprotocols, validateSelectedSubprotocol, validateUpgrade, checkWebSocketOrigin, parseFrame, encodeFrame, encodeClosePayload, decodeClosePayload, encodeSendPayload, normalizeWebSocketOptions, wsRateLimit, FrameSink, FRAME_INCOMPLETE, } from "./websocket.js";
|
|
104
|
+
export { defineWebSocket, WebSocketRegistry, WebSocketProtocolError, WebSocketPayloadTooLargeError, WS_GUID, WS_READY_STATE, WS_OPCODE, WS_CLOSE_CODE, WS_MAX_CONTROL_PAYLOAD, DEFAULT_WS_BACKPRESSURE_LIMIT, DEFAULT_WS_MAX_PAYLOAD_LENGTH, DEFAULT_WS_IDLE_TIMEOUT_SECONDS, computeAcceptKey, parseSubprotocols, validateSelectedSubprotocol, validateUpgrade, checkWebSocketOrigin, parseFrame, encodeFrame, encodeClosePayload, decodeClosePayload, isValidWireCloseCode, encodeSendPayload, normalizeWebSocketOptions, wsRateLimit, FrameSink, FRAME_INCOMPLETE, } from "./websocket.js";
|
|
105
105
|
export type { WebSocketConnection, WebSocketContext, WebSocketHandler, WebSocketMeta, WebSocketRouteEntry, NormalizedWebSocketOptions, WebSocketBeforeUpgrade, HandshakeResult, ParsedFrame, MessageEvent as WebSocketMessageEvent, FrameSinkEvents, } from "./websocket.js";
|
package/dist/index.js
CHANGED
|
@@ -51,4 +51,4 @@ export { MetricsRegistry, Counter, Gauge, Histogram, httpMetrics, DEFAULT_DURATI
|
|
|
51
51
|
export { fileField, multipartObject, isFileFieldSchema, isMultipartObjectSchema, } from "./multipart.js";
|
|
52
52
|
export { otelTracing, TRACING_SPAN_KIND_SERVER, TRACING_SPAN_STATUS_UNSET, TRACING_SPAN_STATUS_OK, TRACING_SPAN_STATUS_ERROR, } from "./tracing.js";
|
|
53
53
|
export { tenancy, tenantScope, tenantFromSubdomain, tenantFromHeader, tenantFromPathPrefix, tenantFromClaim, defaultTenantNormalize, TENANCY_HOOK_MARKER, TENANCY_RESOLVED_MARKER, TENANT_UNRESOLVED, } from "./tenancy.js";
|
|
54
|
-
export { defineWebSocket, WebSocketRegistry, WebSocketProtocolError, WebSocketPayloadTooLargeError, WS_GUID, WS_READY_STATE, WS_OPCODE, WS_CLOSE_CODE, WS_MAX_CONTROL_PAYLOAD, DEFAULT_WS_BACKPRESSURE_LIMIT, DEFAULT_WS_MAX_PAYLOAD_LENGTH, DEFAULT_WS_IDLE_TIMEOUT_SECONDS, computeAcceptKey, parseSubprotocols, validateSelectedSubprotocol, validateUpgrade, checkWebSocketOrigin, parseFrame, encodeFrame, encodeClosePayload, decodeClosePayload, encodeSendPayload, normalizeWebSocketOptions, wsRateLimit, FrameSink, FRAME_INCOMPLETE, } from "./websocket.js";
|
|
54
|
+
export { defineWebSocket, WebSocketRegistry, WebSocketProtocolError, WebSocketPayloadTooLargeError, WS_GUID, WS_READY_STATE, WS_OPCODE, WS_CLOSE_CODE, WS_MAX_CONTROL_PAYLOAD, DEFAULT_WS_BACKPRESSURE_LIMIT, DEFAULT_WS_MAX_PAYLOAD_LENGTH, DEFAULT_WS_IDLE_TIMEOUT_SECONDS, computeAcceptKey, parseSubprotocols, validateSelectedSubprotocol, validateUpgrade, checkWebSocketOrigin, parseFrame, encodeFrame, encodeClosePayload, decodeClosePayload, isValidWireCloseCode, encodeSendPayload, normalizeWebSocketOptions, wsRateLimit, FrameSink, FRAME_INCOMPLETE, } from "./websocket.js";
|
package/dist/ip-reputation.d.ts
CHANGED
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
* @module
|
|
45
45
|
* @since 0.37.0
|
|
46
46
|
*/
|
|
47
|
-
import type {
|
|
47
|
+
import type { Hooks, IdentityGateContext } from "./types.js";
|
|
48
48
|
/**
|
|
49
49
|
* A pluggable source of abusive IP / CIDR entries. Implementations return the
|
|
50
50
|
* raw list each refresh; parsing, validation, and de-duplication are handled by
|
|
@@ -105,7 +105,7 @@ export interface IpReputationOptions {
|
|
|
105
105
|
* Custom client-IP resolver. Overrides {@link IpReputationOptions.trustProxyHeaders}.
|
|
106
106
|
* Defaults to failing open (no IP → not blocked).
|
|
107
107
|
*/
|
|
108
|
-
resolveIp?: (ctx:
|
|
108
|
+
resolveIp?: (ctx: IdentityGateContext) => string | undefined;
|
|
109
109
|
/**
|
|
110
110
|
* Trust `X-Forwarded-For` / `X-Real-IP` in the default IP resolver. Only
|
|
111
111
|
* enable behind a trusted proxy that overwrites these headers.
|
package/dist/ip-reputation.js
CHANGED
|
@@ -222,7 +222,12 @@ export function ipReputation(opts) {
|
|
|
222
222
|
const ready = opts.loadOnStart === false ? Promise.resolve() : refresh();
|
|
223
223
|
return {
|
|
224
224
|
hooks: {
|
|
225
|
-
beforeHandle
|
|
225
|
+
// `preBody`, not `beforeHandle`: a denylist gate that short-circuits from
|
|
226
|
+
// `beforeHandle` is preempted by any earlier `beforeHandle` middleware that
|
|
227
|
+
// returns a Response first — a `responseCache()` HIT mounted above it would
|
|
228
|
+
// serve a denylisted address the cached body. `preBody` always runs first,
|
|
229
|
+
// so the feed holds regardless of mount order.
|
|
230
|
+
preBody(ctx) {
|
|
226
231
|
const ip = resolveIp(ctx);
|
|
227
232
|
if (!ip)
|
|
228
233
|
return undefined; // fail-open on unresolved IP
|
package/dist/ip-restriction.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @since 0.19.0
|
|
9
9
|
*/
|
|
10
|
-
import type {
|
|
10
|
+
import type { Hooks, IdentityGateContext } from "./types.js";
|
|
11
11
|
/**
|
|
12
12
|
* Options for {@link ipRestriction}. At least one of `allow` or `deny` must
|
|
13
13
|
* be provided; supplying both runs deny-first then allow-otherwise (deny
|
|
@@ -36,7 +36,7 @@ export interface IpRestrictionOptions {
|
|
|
36
36
|
* Provide a function to read adapter connection metadata or a trusted
|
|
37
37
|
* custom header (e.g. a CDN-specific identifier).
|
|
38
38
|
*/
|
|
39
|
-
resolveIp?: (ctx:
|
|
39
|
+
resolveIp?: (ctx: IdentityGateContext) => string | undefined;
|
|
40
40
|
/**
|
|
41
41
|
* Read `X-Forwarded-For` / `X-Real-IP` in the default resolver. Defaults
|
|
42
42
|
* to `false` because those headers are client-spoofable unless every
|
|
@@ -101,7 +101,7 @@ export interface IpMatcher {
|
|
|
101
101
|
*
|
|
102
102
|
* @param opts Allow/deny lists plus IP-resolution options; see
|
|
103
103
|
* {@link IpRestrictionOptions}. Deny matches always win over allow.
|
|
104
|
-
* @returns A {@link Hooks} object whose `
|
|
104
|
+
* @returns A {@link Hooks} object whose `preBody` hook enforces the lists,
|
|
105
105
|
* failing closed (403) when the client IP cannot be resolved or parsed.
|
|
106
106
|
* @throws Error at setup time when neither `allow` nor `deny` is provided,
|
|
107
107
|
* or when a pattern is not a valid IP/CIDR.
|
package/dist/ip-restriction.js
CHANGED
|
@@ -29,7 +29,7 @@ import { resolveForwardedClientIp, resolveForwardedTrust } from "./conn-info.js"
|
|
|
29
29
|
*
|
|
30
30
|
* @param opts Allow/deny lists plus IP-resolution options; see
|
|
31
31
|
* {@link IpRestrictionOptions}. Deny matches always win over allow.
|
|
32
|
-
* @returns A {@link Hooks} object whose `
|
|
32
|
+
* @returns A {@link Hooks} object whose `preBody` hook enforces the lists,
|
|
33
33
|
* failing closed (403) when the client IP cannot be resolved or parsed.
|
|
34
34
|
* @throws Error at setup time when neither `allow` nor `deny` is provided,
|
|
35
35
|
* or when a pattern is not a valid IP/CIDR.
|
|
@@ -45,7 +45,12 @@ export function ipRestriction(opts) {
|
|
|
45
45
|
const resolveIp = opts.resolveIp ?? (hops !== undefined ? forwardedIpResolver(hops) : noIpResolver);
|
|
46
46
|
const message = opts.message ?? "IP address not permitted";
|
|
47
47
|
return {
|
|
48
|
-
beforeHandle
|
|
48
|
+
// `preBody`, not `beforeHandle`: a gate that returns a Response from
|
|
49
|
+
// `beforeHandle` can be preempted by any earlier `beforeHandle` middleware
|
|
50
|
+
// that short-circuits first — a `responseCache()` HIT mounted above it would
|
|
51
|
+
// serve a deny-listed address the cached body. `preBody` always runs first,
|
|
52
|
+
// so the allow/deny lists hold regardless of mount order.
|
|
53
|
+
preBody(ctx) {
|
|
49
54
|
const raw = resolveIp(ctx);
|
|
50
55
|
if (!raw)
|
|
51
56
|
throw new ForbiddenError(message);
|
package/dist/mcp.d.ts
CHANGED
|
@@ -35,32 +35,24 @@ export declare const MCP_PROTOCOL_VERSIONS: readonly string[];
|
|
|
35
35
|
*
|
|
36
36
|
* @since 1.0.0
|
|
37
37
|
*/
|
|
38
|
-
export declare const MCP_META_KEYS:
|
|
39
|
-
/** Protocol version for this request. Required on every modern request. */
|
|
38
|
+
export declare const MCP_META_KEYS: {
|
|
40
39
|
readonly protocolVersion: "io.modelcontextprotocol/protocolVersion";
|
|
41
|
-
/** Self-reported client name/version. Advisory only; never a security input. */
|
|
42
40
|
readonly clientInfo: "io.modelcontextprotocol/clientInfo";
|
|
43
|
-
/** Client capabilities relevant to this request. Required on every modern request. */
|
|
44
41
|
readonly clientCapabilities: "io.modelcontextprotocol/clientCapabilities";
|
|
45
|
-
/** Minimum log level the server should emit for this request. */
|
|
46
42
|
readonly logLevel: "io.modelcontextprotocol/logLevel";
|
|
47
|
-
/** Self-reported server name/version, returned in each modern result's `_meta`. */
|
|
48
43
|
readonly serverInfo: "io.modelcontextprotocol/serverInfo";
|
|
49
|
-
}
|
|
44
|
+
};
|
|
50
45
|
/**
|
|
51
46
|
* JSON-RPC error codes defined by the MCP specification in its reserved
|
|
52
47
|
* `-32020`..`-32099` sub-range.
|
|
53
48
|
*
|
|
54
49
|
* @since 1.0.0
|
|
55
50
|
*/
|
|
56
|
-
export declare const MCP_ERROR_CODES:
|
|
57
|
-
/** HTTP headers disagree with the request body, or a required header is missing. */
|
|
51
|
+
export declare const MCP_ERROR_CODES: {
|
|
58
52
|
readonly headerMismatch: -32020;
|
|
59
|
-
/** The request needs a client capability the client did not declare. */
|
|
60
53
|
readonly missingRequiredClientCapability: -32021;
|
|
61
|
-
/** The requested protocol version is not implemented by this server. */
|
|
62
54
|
readonly unsupportedProtocolVersion: -32022;
|
|
63
|
-
}
|
|
55
|
+
};
|
|
64
56
|
/**
|
|
65
57
|
* Default maximum accepted JSON-RPC request body for a DaloyJS MCP endpoint.
|
|
66
58
|
* The cap is intentionally small because MCP calls should carry parameters,
|
package/dist/safe-redirect.js
CHANGED
|
@@ -69,6 +69,17 @@ const ALLOWED_REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
|
69
69
|
// response-splitting via the `Location` header.
|
|
70
70
|
// eslint-disable-next-line no-control-regex
|
|
71
71
|
const CONTROL_CHAR_RE = /[\u0000-\u001f\u007f-\u009f]/;
|
|
72
|
+
// Reject *percent-encoded* C0 controls and DEL (`%00`-`%1F`, `%7F`) that arrive
|
|
73
|
+
// as literal text in the target (e.g. a still-encoded query value passed
|
|
74
|
+
// straight through). `CONTROL_CHAR_RE` only sees decoded characters, so an
|
|
75
|
+
// encoded tab would otherwise be written verbatim into the `Location` header.
|
|
76
|
+
// Per WHATWG URL that stays same-origin, but legacy WebKit stacks strip
|
|
77
|
+
// decoded tabs/newlines and can re-interpret `/%09/host` as protocol-relative
|
|
78
|
+
// — the trick behind historical Safari open-redirect CVEs. The range is
|
|
79
|
+
// deliberately narrow: UTF-8 continuation bytes live in `%80`-`%BF`, so
|
|
80
|
+
// legitimate percent-encoded non-ASCII paths (e.g. `/s%C3%A9arch`) are
|
|
81
|
+
// unaffected.
|
|
82
|
+
const ENCODED_CONTROL_CHAR_RE = /%(?:0[0-9a-f]|1[0-9a-f]|7f)/i;
|
|
72
83
|
// Any code point above U+00FF (outside Latin-1). Such characters cannot be
|
|
73
84
|
// written to a `Location` header — which is serialized as an ISO-8859-1
|
|
74
85
|
// ByteString, so `Headers.set` throws a raw `TypeError` — and they cover the
|
|
@@ -91,6 +102,14 @@ function classify(target, allowedPaths, allowedOrigins) {
|
|
|
91
102
|
if (CONTROL_CHAR_RE.test(target)) {
|
|
92
103
|
return { ok: false, reason: "invalid-control-characters" };
|
|
93
104
|
}
|
|
105
|
+
// Encoded control characters (`%09`, `%00`, …) arriving as literal text:
|
|
106
|
+
// spec-compliant browsers keep `/%09/host` same-origin, but legacy WebKit
|
|
107
|
+
// strips decoded tabs/newlines and can fold it into an origin-escaping
|
|
108
|
+
// protocol-relative URL. Refuse rather than rely on every user agent
|
|
109
|
+
// parsing it the WHATWG way.
|
|
110
|
+
if (ENCODED_CONTROL_CHAR_RE.test(target)) {
|
|
111
|
+
return { ok: false, reason: "invalid-control-characters" };
|
|
112
|
+
}
|
|
94
113
|
// Protocol-relative (`//evil.com`) is the classic open-redirect bypass.
|
|
95
114
|
if (target.startsWith("//"))
|
|
96
115
|
return { ok: false, reason: "protocol-relative" };
|
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:85b1efae-7e9c-5b6d-8081-af5e65efa14a",
|
|
5
5
|
"version": 1,
|
|
6
6
|
"metadata": {
|
|
7
|
-
"timestamp": "2026-
|
|
7
|
+
"timestamp": "2026-08-01T11:35:20.075Z",
|
|
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.9"
|
|
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.9",
|
|
23
23
|
"name": "@daloyjs/core",
|
|
24
|
-
"version": "1.0.0-rc.
|
|
24
|
+
"version": "1.0.0-rc.9",
|
|
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.9",
|
|
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.9",
|
|
50
50
|
"name": "@daloyjs/core",
|
|
51
|
-
"version": "1.0.0-rc.
|
|
51
|
+
"version": "1.0.0-rc.9",
|
|
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.9",
|
|
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.9",
|
|
6
|
+
"documentNamespace": "https://github.com/daloyjs/daloy/sbom/@daloyjs/core-1.0.0-rc.9-85b1efae-7e9c-5b6d-8081-af5e65efa14a",
|
|
7
7
|
"creationInfo": {
|
|
8
|
-
"created": "2026-
|
|
8
|
+
"created": "2026-08-01T11:35:20.075Z",
|
|
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.9",
|
|
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.9"
|
|
31
31
|
}
|
|
32
32
|
]
|
|
33
33
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -305,6 +305,37 @@ export interface PreBodyContext<P extends string = string> {
|
|
|
305
305
|
headers: Headers;
|
|
306
306
|
};
|
|
307
307
|
}
|
|
308
|
+
/**
|
|
309
|
+
* Context handed to the caller-supplied resolvers of the network-identity
|
|
310
|
+
* access-control gates — `geoBlock`, `ipRestriction`, `botGuard`, `autoBan` and
|
|
311
|
+
* `ipReputation`.
|
|
312
|
+
*
|
|
313
|
+
* Those gates enforce from {@link Hooks.preBody} so a `responseCache()` hit
|
|
314
|
+
* cannot preempt them (see SECURITY.md, "Hook phase decides what a
|
|
315
|
+
* short-circuiting middleware can preempt"). Their callbacks therefore run
|
|
316
|
+
* before body I/O and before *any* `beforeHandle` middleware, which means:
|
|
317
|
+
*
|
|
318
|
+
* - `body` is always `undefined` — nothing has been parsed yet.
|
|
319
|
+
* - `state` holds only what `onRequest` / an earlier `preBody` layer put there.
|
|
320
|
+
* In particular it does **not** hold anything `session()` or another
|
|
321
|
+
* `beforeHandle` layer resolves.
|
|
322
|
+
*
|
|
323
|
+
* The alias exists so that is a compile error instead of a runtime surprise.
|
|
324
|
+
* Typing these callbacks on the full {@link BaseContext} — whose `body` widens
|
|
325
|
+
* to `any` — let `(ctx) => ctx.body.email` type-check and then silently evaluate
|
|
326
|
+
* to `undefined` at run time. The consequence differed per gate and two of the
|
|
327
|
+
* five failed *silently*: `ipReputation` fails open on an unresolved IP, and
|
|
328
|
+
* `autoBan` stopped recording strikes altogether because it never got an
|
|
329
|
+
* identity to attribute them to.
|
|
330
|
+
*
|
|
331
|
+
* Resolve identity from `request` (headers, URL), `params`, `query`, or state a
|
|
332
|
+
* `preBody` layer set. If a value genuinely requires the parsed body, it cannot
|
|
333
|
+
* be a `preBody` gate input — see {@link "./auto-ban.js".AutoBanOptions.keyGenerator},
|
|
334
|
+
* which falls back to a later phase for exactly that case.
|
|
335
|
+
*
|
|
336
|
+
* @since 1.0.0-rc.8
|
|
337
|
+
*/
|
|
338
|
+
export type IdentityGateContext = PreBodyContext<any>;
|
|
308
339
|
/**
|
|
309
340
|
* Lifecycle hooks fired around request handling. Hooks compose pipeline-style
|
|
310
341
|
* — the global hooks (`AppOptions.hooks`) run first, then group hooks added
|