@nifrajs/middleware 2.8.1 → 2.9.0
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 +22 -1
- package/dist/_utils.d.ts +6 -1
- package/dist/_utils.d.ts.map +1 -1
- package/dist/_utils.js +39 -28
- package/dist/_utils.js.map +1 -1
- package/dist/cache-control.d.ts.map +1 -1
- package/dist/cache-control.js +31 -15
- package/dist/cache-control.js.map +1 -1
- package/dist/cache.d.ts +1 -1
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +2 -3
- package/dist/cache.js.map +1 -1
- package/dist/compression.d.ts +10 -10
- package/dist/compression.d.ts.map +1 -1
- package/dist/compression.js +191 -70
- package/dist/compression.js.map +1 -1
- package/dist/cors.d.ts.map +1 -1
- package/dist/cors.js +27 -16
- package/dist/cors.js.map +1 -1
- package/dist/etag.d.ts +4 -2
- package/dist/etag.d.ts.map +1 -1
- package/dist/etag.js +135 -63
- package/dist/etag.js.map +1 -1
- package/dist/idempotency.d.ts.map +1 -1
- package/dist/idempotency.js +10 -1
- package/dist/idempotency.js.map +1 -1
- package/dist/jwt.js +1 -1
- package/dist/jwt.js.map +1 -1
- package/dist/language.d.ts.map +1 -1
- package/dist/language.js +14 -15
- package/dist/language.js.map +1 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +34 -16
- package/dist/logger.js.map +1 -1
- package/dist/powered-by.d.ts.map +1 -1
- package/dist/powered-by.js +8 -5
- package/dist/powered-by.js.map +1 -1
- package/dist/pretty-json.d.ts +5 -2
- package/dist/pretty-json.d.ts.map +1 -1
- package/dist/pretty-json.js +85 -64
- package/dist/pretty-json.js.map +1 -1
- package/dist/rate-limit.d.ts.map +1 -1
- package/dist/rate-limit.js +76 -19
- package/dist/rate-limit.js.map +1 -1
- package/dist/request-id.js +1 -1
- package/dist/request-id.js.map +1 -1
- package/dist/security-headers.d.ts +8 -4
- package/dist/security-headers.d.ts.map +1 -1
- package/dist/security-headers.js +18 -16
- package/dist/security-headers.js.map +1 -1
- package/package.json +3 -3
- package/src/_utils.ts +39 -24
- package/src/cache-control.ts +27 -14
- package/src/cache.ts +2 -4
- package/src/compression.ts +202 -62
- package/src/cors.ts +23 -14
- package/src/etag.ts +141 -59
- package/src/idempotency.ts +11 -1
- package/src/jwt.ts +1 -1
- package/src/language.ts +12 -13
- package/src/logger.ts +24 -6
- package/src/powered-by.ts +8 -4
- package/src/pretty-json.ts +95 -70
- package/src/rate-limit.ts +86 -20
- package/src/request-id.ts +1 -1
- package/src/security-headers.ts +17 -15
package/dist/pretty-json.js
CHANGED
|
@@ -3,14 +3,15 @@ const JSON_TYPE = /^(?:application\/json|[^/]+\/[^;]+\+json)(?:\s*;|$)/i;
|
|
|
3
3
|
function isJson(contentType) {
|
|
4
4
|
return JSON_TYPE.test(contentType);
|
|
5
5
|
}
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
|
|
6
|
+
const concat = (chunks, total) => {
|
|
7
|
+
const out = new Uint8Array(total);
|
|
8
|
+
let offset = 0;
|
|
9
|
+
for (const chunk of chunks) {
|
|
10
|
+
out.set(chunk, offset);
|
|
11
|
+
offset += chunk.byteLength;
|
|
12
|
+
}
|
|
13
|
+
return out;
|
|
14
|
+
};
|
|
14
15
|
function replayFrom(res, chunks, reader) {
|
|
15
16
|
const stream = new ReadableStream({
|
|
16
17
|
start(controller) {
|
|
@@ -20,15 +21,12 @@ function replayFrom(res, chunks, reader) {
|
|
|
20
21
|
async pull(controller) {
|
|
21
22
|
try {
|
|
22
23
|
const { done, value } = await reader.read();
|
|
23
|
-
// No value and not done cannot happen for a byte stream, but it means the same thing here:
|
|
24
|
-
// there is nothing further to emit.
|
|
25
24
|
if (done || value === undefined)
|
|
26
25
|
controller.close();
|
|
27
26
|
else
|
|
28
27
|
controller.enqueue(value);
|
|
29
28
|
}
|
|
30
29
|
catch (error) {
|
|
31
|
-
// The upstream body failed; surface that to the client rather than truncating silently.
|
|
32
30
|
controller.error(error);
|
|
33
31
|
}
|
|
34
32
|
},
|
|
@@ -40,20 +38,6 @@ function replayFrom(res, chunks, reader) {
|
|
|
40
38
|
headers: res.headers,
|
|
41
39
|
});
|
|
42
40
|
}
|
|
43
|
-
/**
|
|
44
|
-
* Read up to `maxBytes` of a response, without disturbing it when the answer is "pass through".
|
|
45
|
-
*
|
|
46
|
-
* `Response.clone()` is the obvious way to peek and is a trap here. Cancelling the clone's reader once
|
|
47
|
-
* the cap is exceeded also stalls the ORIGINAL body in Bun, so an oversized streamed response hung the
|
|
48
|
-
* client instead of passing through - a hang, from a middleware whose entire job is cosmetic. (A clone
|
|
49
|
-
* read to completion is fine; only the cancel breaks it, which is why every buffered case passed and
|
|
50
|
-
* only a streamed one failed.) Abandoning the clone rather than cancelling it trades the hang for
|
|
51
|
-
* buffering the whole oversized body, which is the exact cost the cap exists to avoid.
|
|
52
|
-
*
|
|
53
|
-
* So the body is read directly and, when it proves too large, replayed: the bytes already pulled are
|
|
54
|
-
* re-emitted ahead of the rest of the same reader. No tee, nothing buffered past the cap, and the
|
|
55
|
-
* response the client receives is byte-for-byte the one the handler produced.
|
|
56
|
-
*/
|
|
57
41
|
async function peekText(res, maxBytes) {
|
|
58
42
|
const body = res.body;
|
|
59
43
|
if (body === null)
|
|
@@ -65,12 +49,14 @@ async function peekText(res, maxBytes) {
|
|
|
65
49
|
while (true) {
|
|
66
50
|
const { done, value } = await reader.read();
|
|
67
51
|
if (done) {
|
|
68
|
-
const decoder = new TextDecoder();
|
|
52
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
69
53
|
let text = "";
|
|
70
54
|
for (const chunk of chunks)
|
|
71
55
|
text += decoder.decode(chunk, { stream: true });
|
|
72
|
-
return { text: text + decoder.decode() };
|
|
56
|
+
return { text: text + decoder.decode(), bytes: concat(chunks, total) };
|
|
73
57
|
}
|
|
58
|
+
if (value === undefined)
|
|
59
|
+
return { response: replayFrom(res, chunks, reader) };
|
|
74
60
|
chunks.push(value);
|
|
75
61
|
total += value.byteLength;
|
|
76
62
|
if (total > maxBytes)
|
|
@@ -81,6 +67,9 @@ async function peekText(res, maxBytes) {
|
|
|
81
67
|
return { response: replayFrom(res, chunks, reader) };
|
|
82
68
|
}
|
|
83
69
|
}
|
|
70
|
+
function requestView(request) {
|
|
71
|
+
return { method: request.method, url: request.url, header: (name) => request.headers.get(name) };
|
|
72
|
+
}
|
|
84
73
|
/**
|
|
85
74
|
* Pretty-print JSON responses for debugging and developer-facing APIs. It only touches JSON content,
|
|
86
75
|
* skips encoded responses, caps inspection size, and leaves invalid JSON untouched.
|
|
@@ -91,8 +80,8 @@ export function prettyJson(options = {}) {
|
|
|
91
80
|
throw new Error("prettyJson: spaces must be an integer from 0 to 10");
|
|
92
81
|
}
|
|
93
82
|
const maxBytes = options.maxBytes ?? 1_000_000;
|
|
94
|
-
if (!Number.
|
|
95
|
-
throw new Error("prettyJson: maxBytes must be a non-negative integer");
|
|
83
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
|
|
84
|
+
throw new Error("prettyJson: maxBytes must be a non-negative safe integer");
|
|
96
85
|
}
|
|
97
86
|
const newline = options.newline !== false;
|
|
98
87
|
const query = options.query ?? false;
|
|
@@ -100,41 +89,73 @@ export function prettyJson(options = {}) {
|
|
|
100
89
|
throw new Error("prettyJson: query is empty");
|
|
101
90
|
const enabled = options.enabled ?? true;
|
|
102
91
|
const isEnabled = typeof enabled === "function" ? enabled : enabled ? () => true : () => false;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
92
|
+
const decoder = new TextDecoder();
|
|
93
|
+
const encoder = new TextEncoder();
|
|
94
|
+
return definePlugin("prettyJson", (app) => app.use({
|
|
95
|
+
onResponseBody(body, headers, req) {
|
|
96
|
+
if (!isEnabled(req))
|
|
97
|
+
return undefined;
|
|
98
|
+
if (query !== false && !new URL(req.url).searchParams.has(query))
|
|
99
|
+
return undefined;
|
|
100
|
+
if (headers.has("content-encoding"))
|
|
101
|
+
return undefined;
|
|
102
|
+
if (!isJson(headers.get("content-type") ?? ""))
|
|
103
|
+
return undefined;
|
|
104
|
+
const text = typeof body === "string" ? body : decoder.decode(body);
|
|
105
|
+
if ((typeof body === "string" ? encoder.encode(body).byteLength : body.byteLength) > maxBytes) {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
let parsed;
|
|
109
|
+
try {
|
|
110
|
+
parsed = JSON.parse(text);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
return `${JSON.stringify(parsed, null, spaces)}${newline ? "\n" : ""}`;
|
|
116
|
+
},
|
|
117
|
+
onResponseRaw(response, req) {
|
|
118
|
+
if (!isEnabled(requestView(req)))
|
|
119
|
+
return response;
|
|
120
|
+
if (query !== false && !new URL(req.url).searchParams.has(query))
|
|
121
|
+
return response;
|
|
122
|
+
if (response.body === null ||
|
|
123
|
+
response.status === 204 ||
|
|
124
|
+
response.status === 205 ||
|
|
125
|
+
response.status === 304 ||
|
|
126
|
+
response.headers.has("content-encoding"))
|
|
127
|
+
return response;
|
|
128
|
+
if (!isJson(response.headers.get("content-type") ?? ""))
|
|
129
|
+
return response;
|
|
130
|
+
const declared = response.headers.get("content-length");
|
|
131
|
+
if (declared !== null && /^(?:0|[1-9]\d*)$/.test(declared) && Number(declared) > maxBytes) {
|
|
132
|
+
return response;
|
|
133
|
+
}
|
|
134
|
+
return prettyRawResponse(response, maxBytes, spaces, newline);
|
|
135
|
+
},
|
|
138
136
|
}));
|
|
139
137
|
}
|
|
138
|
+
async function prettyRawResponse(response, maxBytes, spaces, newline) {
|
|
139
|
+
const peeked = await peekText(response, maxBytes);
|
|
140
|
+
if ("response" in peeked)
|
|
141
|
+
return peeked.response;
|
|
142
|
+
let parsed;
|
|
143
|
+
try {
|
|
144
|
+
parsed = JSON.parse(peeked.text);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return new Response(peeked.bytes, {
|
|
148
|
+
status: response.status,
|
|
149
|
+
statusText: response.statusText,
|
|
150
|
+
headers: response.headers,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
const headers = new Headers(response.headers);
|
|
154
|
+
headers.delete("content-length");
|
|
155
|
+
return new Response(`${JSON.stringify(parsed, null, spaces)}${newline ? "\n" : ""}`, {
|
|
156
|
+
status: response.status,
|
|
157
|
+
statusText: response.statusText,
|
|
158
|
+
headers,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
140
161
|
//# sourceMappingURL=pretty-json.js.map
|
package/dist/pretty-json.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pretty-json.js","sourceRoot":"","sources":["../src/pretty-json.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,
|
|
1
|
+
{"version":3,"file":"pretty-json.js","sourceRoot":"","sources":["../src/pretty-json.ts"],"names":[],"mappings":"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"}
|
package/dist/rate-limit.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,
|
|
1
|
+
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAA2C,MAAM,sBAAsB,CAAA;AAG/F,MAAM,WAAW,eAAe;IAC9B,+DAA+D;IAC/D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,+CAA+C;IAC/C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB;AAED;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAAA;CAC7D;AAED,MAAM,WAAW,kBAAkB;IACjC,mHAAmH;IACnH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,CAAA;IACpC;mFAC+E;IAC/E,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,2FAA2F;IAC3F,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAClC;AAOD;;;;;;;;GAQG;AACH,qBAAa,WAAY,YAAW,cAAc;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwD;IAChF,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAQ;IAChC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAQ;IACxC,OAAO,CAAC,SAAS,CAAI;IACrB,OAAO,CAAC,aAAa,CAAI;IAEzB;;yGAEqG;IACrG,IAAI,iBAAiB,IAAI,MAAM,CAE9B;gBAEW,OAAO,GAAE,kBAAuB;IAiB5C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAmB5D,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,cAAc;CAuBvB;AAED,MAAM,WAAW,gBAAgB;IAC/B,gFAAgF;IAChF,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAA;IAC9B,uCAAuC;IACvC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,sCAAsC;IACtC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;IAChC,iGAAiG;IACjG,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB;;;OAGG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAA;IACjC;;;;OAIG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,MAAM,CAAA;CACxC;AAkDD;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,UAAU,CAyG/D"}
|
package/dist/rate-limit.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NIFRA_ASSURANCE, withRouteAssurance } from "@nifrajs/core/assurance";
|
|
2
|
+
import { withHeaders, withNodeHeaders } from "./_utils.js";
|
|
2
3
|
/** Max entries scanned (oldest-first) per eviction looking for an expired victim before falling back
|
|
3
4
|
* to evicting the oldest-inserted. Bounds eviction to O(1) per insertion instead of a full O(n) sweep
|
|
4
5
|
* under a distinct-key flood - the abuse the key cap defends against. */
|
|
@@ -111,6 +112,24 @@ function defaultKey(req, trustedProxies, header, allowGlobalKey) {
|
|
|
111
112
|
// from it. A shared bucket is only safe when the app deliberately asked for a global throttle.
|
|
112
113
|
return allowGlobalKey ? "global" : null;
|
|
113
114
|
}
|
|
115
|
+
/** {@link defaultKey} against the allocation-light native request view - same logic, same order. */
|
|
116
|
+
function nativeKey(req, trustedProxies, header, allowGlobalKey) {
|
|
117
|
+
if (header !== undefined) {
|
|
118
|
+
const ip = req.header(header);
|
|
119
|
+
if (ip !== null && ip.trim() !== "")
|
|
120
|
+
return ip.trim();
|
|
121
|
+
}
|
|
122
|
+
if (trustedProxies > 0) {
|
|
123
|
+
const xff = req.header("x-forwarded-for");
|
|
124
|
+
if (xff !== null) {
|
|
125
|
+
const parts = xff.split(",");
|
|
126
|
+
const ip = parts[parts.length - trustedProxies]?.trim();
|
|
127
|
+
if (ip !== undefined && ip !== "")
|
|
128
|
+
return ip;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return allowGlobalKey ? "global" : null;
|
|
132
|
+
}
|
|
114
133
|
/**
|
|
115
134
|
* Rate limiting as a {@link Middleware}. Runs in `onRequest` (before routing, so it
|
|
116
135
|
* also covers 404s); over the limit → `429` + `Retry-After`. Every response carries
|
|
@@ -139,35 +158,73 @@ export function rateLimit(options) {
|
|
|
139
158
|
}
|
|
140
159
|
const keyOf = options.key ?? ((req) => defaultKey(req, trustedProxies, header, allowGlobalKey));
|
|
141
160
|
const quota = new WeakMap();
|
|
161
|
+
// State for the Node twins, keyed by the NodeRequestContext identity (the same object is passed
|
|
162
|
+
// to the request and response twins - the core identity contract that replaces `Request` keying).
|
|
163
|
+
const nativeQuota = new WeakMap();
|
|
164
|
+
const hitStore = async (key) => {
|
|
165
|
+
if (typeof key !== "string" || key.trim() === "") {
|
|
166
|
+
return Response.json({ ok: false, error: "rate_limit_key_unavailable" }, { status: 500 });
|
|
167
|
+
}
|
|
168
|
+
const { count, resetAt } = await store.hit(key, windowMs);
|
|
169
|
+
const resetSeconds = Math.max(0, Math.ceil((resetAt - Date.now()) / 1000));
|
|
170
|
+
const info = { remaining: Math.max(0, max - count), resetSeconds };
|
|
171
|
+
if (count > max) {
|
|
172
|
+
return {
|
|
173
|
+
info,
|
|
174
|
+
reject: new Response(JSON.stringify({ ok: false, error: "rate_limited" }), {
|
|
175
|
+
status: 429,
|
|
176
|
+
headers: { "content-type": "application/json", "retry-after": String(resetSeconds) },
|
|
177
|
+
}),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
return { info, reject: undefined };
|
|
181
|
+
};
|
|
142
182
|
const middleware = {
|
|
143
183
|
name: "rate-limit",
|
|
144
184
|
async onRequest(req) {
|
|
145
|
-
const
|
|
146
|
-
if (
|
|
147
|
-
return
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const resetSeconds = Math.max(0, Math.ceil((resetAt - Date.now()) / 1000));
|
|
151
|
-
quota.set(req, { remaining: Math.max(0, max - count), resetSeconds });
|
|
152
|
-
if (count > max) {
|
|
153
|
-
return new Response(JSON.stringify({ ok: false, error: "rate_limited" }), {
|
|
154
|
-
status: 429,
|
|
155
|
-
headers: { "content-type": "application/json", "retry-after": String(resetSeconds) },
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
return undefined;
|
|
185
|
+
const outcome = await hitStore(keyOf(req));
|
|
186
|
+
if (outcome instanceof Response)
|
|
187
|
+
return outcome;
|
|
188
|
+
quota.set(req, outcome.info);
|
|
189
|
+
return outcome.reject;
|
|
159
190
|
},
|
|
160
191
|
onResponse(res, req) {
|
|
161
192
|
const info = quota.get(req);
|
|
162
193
|
if (info === undefined)
|
|
163
194
|
return res;
|
|
164
195
|
quota.delete(req);
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
196
|
+
return withHeaders(res, (headers) => {
|
|
197
|
+
headers.set("RateLimit-Limit", String(max));
|
|
198
|
+
headers.set("RateLimit-Remaining", String(info.remaining));
|
|
199
|
+
headers.set("RateLimit-Reset", String(info.resetSeconds));
|
|
200
|
+
});
|
|
170
201
|
},
|
|
202
|
+
// Node twins exist only for the built-in header/XFF key derivation - a custom `key` callback
|
|
203
|
+
// takes a real `Request`, which is exactly the object the native lane avoids building.
|
|
204
|
+
...(options.key === undefined
|
|
205
|
+
? {
|
|
206
|
+
onNodeRequest: async (req) => {
|
|
207
|
+
const outcome = await hitStore(nativeKey(req, trustedProxies, header, allowGlobalKey));
|
|
208
|
+
if (outcome instanceof Response)
|
|
209
|
+
return outcome;
|
|
210
|
+
nativeQuota.set(req, outcome.info);
|
|
211
|
+
return outcome.reject;
|
|
212
|
+
},
|
|
213
|
+
onNodeResponse: (res, req) => {
|
|
214
|
+
const info = nativeQuota.get(req);
|
|
215
|
+
if (info === undefined)
|
|
216
|
+
return;
|
|
217
|
+
nativeQuota.delete(req);
|
|
218
|
+
withNodeHeaders(res, (headers) => {
|
|
219
|
+
// Lowercase on purpose: the Web path's `Headers` lowercases names on the wire, so
|
|
220
|
+
// this is byte-identical output.
|
|
221
|
+
headers["ratelimit-limit"] = String(max);
|
|
222
|
+
headers["ratelimit-remaining"] = String(info.remaining);
|
|
223
|
+
headers["ratelimit-reset"] = String(info.resetSeconds);
|
|
224
|
+
});
|
|
225
|
+
},
|
|
226
|
+
}
|
|
227
|
+
: {}),
|
|
171
228
|
};
|
|
172
229
|
return withRouteAssurance(middleware, {
|
|
173
230
|
id: NIFRA_ASSURANCE.RATE_LIMITED,
|
package/dist/rate-limit.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;
|
|
1
|
+
{"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"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"}
|
package/dist/request-id.js
CHANGED
|
@@ -13,7 +13,7 @@ export function requestId(options = {}) {
|
|
|
13
13
|
const header = options.header ?? "x-request-id";
|
|
14
14
|
const generate = options.generate ?? (() => crypto.randomUUID());
|
|
15
15
|
return definePlugin("requestId", (app) => app.derive((c) => {
|
|
16
|
-
const id = c.
|
|
16
|
+
const id = c.header(header) ?? generate();
|
|
17
17
|
c.set.headers[header] = id; // echo on the response (route handlers)
|
|
18
18
|
return { requestId: id };
|
|
19
19
|
}));
|
package/dist/request-id.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"request-id.js","sourceRoot":"","sources":["../src/request-id.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AASnD;;;;;;;;;GASG;AACH,MAAM,UAAU,SAAS,CAAC,UAA4B,EAAE;IACtD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,cAAc,CAAA;IAC/C,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC,CAAA;IAChE,OAAO,YAAY,CAAC,WAAW,EAAE,CAAC,GAAG,EAAE,EAAE,CACvC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE;QACf,MAAM,EAAE,GAAG,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"request-id.js","sourceRoot":"","sources":["../src/request-id.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AASnD;;;;;;;;;GASG;AACH,MAAM,UAAU,SAAS,CAAC,UAA4B,EAAE;IACtD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,cAAc,CAAA;IAC/C,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC,CAAA;IAChE,OAAO,YAAY,CAAC,WAAW,EAAE,CAAC,GAAG,EAAE,EAAE,CACvC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE;QACf,MAAM,EAAE,GAAG,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAA;QACzC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,CAAA,CAAC,wCAAwC;QACnE,OAAO,EAAE,SAAS,EAAE,EAAE,EAAE,CAAA;IAC1B,CAAC,CAAC,CACH,CAAA;AACH,CAAC"}
|
|
@@ -14,10 +14,14 @@ export interface SecurityHeadersOptions {
|
|
|
14
14
|
readonly referrerPolicy?: string;
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
* A safe-by-default set of response security headers
|
|
18
|
-
*
|
|
19
|
-
* `
|
|
20
|
-
*
|
|
17
|
+
* A safe-by-default set of response security headers, covering errors and 404s too:
|
|
18
|
+
* `X-Content-Type-Options: nosniff`, `X-Frame-Options`, and `Referrer-Policy` always;
|
|
19
|
+
* `Strict-Transport-Security` and `Content-Security-Policy` only when configured (both are
|
|
20
|
+
* environment-/app-specific).
|
|
21
|
+
*
|
|
22
|
+
* Every value is fixed at construction, so these are declared statically rather than written by a
|
|
23
|
+
* response hook: an app whose response middleware is only this keeps the fused native lanes. A route
|
|
24
|
+
* that sets one of these names itself keeps its own value.
|
|
21
25
|
*/
|
|
22
26
|
export declare function securityHeaders(options?: SecurityHeadersOptions): Middleware;
|
|
23
27
|
//# sourceMappingURL=security-headers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"security-headers.d.ts","sourceRoot":"","sources":["../src/security-headers.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;
|
|
1
|
+
{"version":3,"file":"security-headers.d.ts","sourceRoot":"","sources":["../src/security-headers.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAEtD,MAAM,WAAW,sBAAsB;IACrC,+FAA+F;IAC/F,QAAQ,CAAC,IAAI,CAAC,EAAE;QACd,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;QACvB,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,CAAA;QACpC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAC3B,CAAA;IACD,sEAAsE;IACtE,QAAQ,CAAC,qBAAqB,CAAC,EAAE,MAAM,CAAA;IACvC,2CAA2C;IAC3C,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,YAAY,CAAA;IAC7C,kDAAkD;IAClD,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CACjC;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,OAAO,GAAE,sBAA2B,GAAG,UAAU,CAgChF"}
|
package/dist/security-headers.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { NIFRA_ASSURANCE, withRouteAssurance } from "@nifrajs/core/assurance";
|
|
2
|
-
import { withHeaders } from "./_utils.js";
|
|
3
2
|
/**
|
|
4
|
-
* A safe-by-default set of response security headers
|
|
5
|
-
*
|
|
6
|
-
* `
|
|
7
|
-
*
|
|
3
|
+
* A safe-by-default set of response security headers, covering errors and 404s too:
|
|
4
|
+
* `X-Content-Type-Options: nosniff`, `X-Frame-Options`, and `Referrer-Policy` always;
|
|
5
|
+
* `Strict-Transport-Security` and `Content-Security-Policy` only when configured (both are
|
|
6
|
+
* environment-/app-specific).
|
|
7
|
+
*
|
|
8
|
+
* Every value is fixed at construction, so these are declared statically rather than written by a
|
|
9
|
+
* response hook: an app whose response middleware is only this keeps the fused native lanes. A route
|
|
10
|
+
* that sets one of these names itself keeps its own value.
|
|
8
11
|
*/
|
|
9
12
|
export function securityHeaders(options = {}) {
|
|
10
13
|
const frameOptions = options.frameOptions ?? "DENY";
|
|
@@ -19,19 +22,18 @@ export function securityHeaders(options = {}) {
|
|
|
19
22
|
parts.push("preload");
|
|
20
23
|
hstsValue = parts.join("; ");
|
|
21
24
|
}
|
|
25
|
+
const declared = {
|
|
26
|
+
"x-content-type-options": "nosniff",
|
|
27
|
+
"x-frame-options": frameOptions,
|
|
28
|
+
"referrer-policy": referrerPolicy,
|
|
29
|
+
};
|
|
30
|
+
if (hstsValue !== undefined)
|
|
31
|
+
declared["strict-transport-security"] = hstsValue;
|
|
32
|
+
if (csp !== undefined)
|
|
33
|
+
declared["content-security-policy"] = csp;
|
|
22
34
|
return withRouteAssurance({
|
|
23
35
|
name: "security-headers",
|
|
24
|
-
|
|
25
|
-
// (see withHeaders - the old always-clone was ~3% of a realistic request).
|
|
26
|
-
onResponse: (res) => withHeaders(res, (headers) => {
|
|
27
|
-
headers.set("X-Content-Type-Options", "nosniff");
|
|
28
|
-
headers.set("X-Frame-Options", frameOptions);
|
|
29
|
-
headers.set("Referrer-Policy", referrerPolicy);
|
|
30
|
-
if (hstsValue !== undefined)
|
|
31
|
-
headers.set("Strict-Transport-Security", hstsValue);
|
|
32
|
-
if (csp !== undefined)
|
|
33
|
-
headers.set("Content-Security-Policy", csp);
|
|
34
|
-
}),
|
|
36
|
+
responseHeaders: declared,
|
|
35
37
|
}, {
|
|
36
38
|
id: NIFRA_ASSURANCE.SECURITY_HEADERS,
|
|
37
39
|
source: "security-headers",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"security-headers.js","sourceRoot":"","sources":["../src/security-headers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;
|
|
1
|
+
{"version":3,"file":"security-headers.js","sourceRoot":"","sources":["../src/security-headers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAkB7E;;;;;;;;;GASG;AACH,MAAM,UAAU,eAAe,CAAC,UAAkC,EAAE;IAClE,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,MAAM,CAAA;IACnD,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,IAAI,aAAa,CAAA;IAC9D,MAAM,GAAG,GAAG,OAAO,CAAC,qBAAqB,CAAA;IAEzC,IAAI,SAA6B,CAAA;IACjC,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC/B,MAAM,KAAK,GAAG,CAAC,WAAW,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAA;QAChD,IAAI,OAAO,CAAC,IAAI,CAAC,iBAAiB;YAAE,KAAK,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAA;QACnE,IAAI,OAAO,CAAC,IAAI,CAAC,OAAO;YAAE,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;QAC/C,SAAS,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC9B,CAAC;IAED,MAAM,QAAQ,GAA2B;QACvC,wBAAwB,EAAE,SAAS;QACnC,iBAAiB,EAAE,YAAY;QAC/B,iBAAiB,EAAE,cAAc;KAClC,CAAA;IACD,IAAI,SAAS,KAAK,SAAS;QAAE,QAAQ,CAAC,2BAA2B,CAAC,GAAG,SAAS,CAAA;IAC9E,IAAI,GAAG,KAAK,SAAS;QAAE,QAAQ,CAAC,yBAAyB,CAAC,GAAG,GAAG,CAAA;IAEhE,OAAO,kBAAkB,CACvB;QACE,IAAI,EAAE,kBAAkB;QACxB,eAAe,EAAE,QAAQ;KAC1B,EACD;QACE,EAAE,EAAE,eAAe,CAAC,gBAAgB;QACpC,MAAM,EAAE,kBAAkB;QAC1B,KAAK,EAAE,QAAQ;KAChB,CACF,CAAA;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nifrajs/middleware",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.0",
|
|
4
4
|
"description": "Composable middleware for nifra - auth, CSRF, JWT/JWKS, IP restriction, CORS, body limits, response cache, timing, and operational helpers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -46,10 +46,10 @@
|
|
|
46
46
|
"access": "public"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@nifrajs/schema": "2.
|
|
49
|
+
"@nifrajs/schema": "2.9.0"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
|
-
"@nifrajs/core": "^2.
|
|
52
|
+
"@nifrajs/core": "^2.9.0"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@nifrajs/core": "workspace:*",
|
package/src/_utils.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export type MaybePromise<T> = T | Promise<T>
|
|
2
2
|
|
|
3
|
+
import type { NodeResponseContext } from "@nifrajs/core/server"
|
|
4
|
+
import { parseCookies as parseCoreCookies } from "@nifrajs/core/server"
|
|
5
|
+
|
|
3
6
|
export const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS", "TRACE"])
|
|
4
7
|
|
|
5
8
|
const TEXT = new TextEncoder()
|
|
@@ -15,28 +18,8 @@ export function jsonError(
|
|
|
15
18
|
)
|
|
16
19
|
}
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
// never a prototype-member shadow. Mirrors core's parseCookies + the query/form parsers.
|
|
21
|
-
const out: Record<string, string> = Object.create(null)
|
|
22
|
-
if (!header) return out
|
|
23
|
-
for (const part of header.split(";")) {
|
|
24
|
-
const eq = part.indexOf("=")
|
|
25
|
-
if (eq < 1) continue
|
|
26
|
-
const name = part.slice(0, eq).trim()
|
|
27
|
-
if (name === "") continue
|
|
28
|
-
let value = part.slice(eq + 1).trim()
|
|
29
|
-
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
|
|
30
|
-
value = value.slice(1, -1)
|
|
31
|
-
}
|
|
32
|
-
try {
|
|
33
|
-
out[name] = decodeURIComponent(value)
|
|
34
|
-
} catch {
|
|
35
|
-
out[name] = value
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return out
|
|
39
|
-
}
|
|
21
|
+
/** Shared with core so auth/CSRF middleware gets the allocation-light cookie scanner too. */
|
|
22
|
+
export const parseCookies = parseCoreCookies
|
|
40
23
|
|
|
41
24
|
export function quotedHeaderValue(value: string): string {
|
|
42
25
|
let out = ""
|
|
@@ -152,12 +135,44 @@ export async function verifyHmacSha256(
|
|
|
152
135
|
* - the first mutation throws before any partial change), so a mutation chain is safe to pass.
|
|
153
136
|
*/
|
|
154
137
|
export function withHeaders(res: Response, apply: (headers: Headers) => void): Response {
|
|
138
|
+
const responseHeaders = res.headers
|
|
139
|
+
const probe = "x-nifra-header-probe"
|
|
140
|
+
let previous: string | null = null
|
|
141
|
+
let mutable = true
|
|
155
142
|
try {
|
|
156
|
-
|
|
157
|
-
|
|
143
|
+
previous = responseHeaders.get(probe)
|
|
144
|
+
responseHeaders.set(probe, "1")
|
|
145
|
+
if (previous === null) responseHeaders.delete(probe)
|
|
146
|
+
else responseHeaders.set(probe, previous)
|
|
158
147
|
} catch {
|
|
148
|
+
mutable = false
|
|
149
|
+
try {
|
|
150
|
+
if (previous === null) responseHeaders.delete(probe)
|
|
151
|
+
else responseHeaders.set(probe, previous)
|
|
152
|
+
} catch {
|
|
153
|
+
// The guarded response rejected cleanup; the clone below is authoritative.
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (!mutable) {
|
|
159
157
|
const headers = new Headers(res.headers)
|
|
160
158
|
apply(headers)
|
|
161
159
|
return new Response(res.body, { status: res.status, statusText: res.statusText, headers })
|
|
162
160
|
}
|
|
161
|
+
apply(responseHeaders)
|
|
162
|
+
return res
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Mutate Node-direct response headers without constructing a Web Response. */
|
|
166
|
+
export function withNodeHeaders(
|
|
167
|
+
res: NodeResponseContext,
|
|
168
|
+
apply: (headers: Record<string, string | readonly string[]>) => void,
|
|
169
|
+
): void {
|
|
170
|
+
if (res.headers === undefined) {
|
|
171
|
+
res.headers = Object.create(null) as Record<string, string | readonly string[]>
|
|
172
|
+
} else if (Object.getPrototypeOf(res.headers) !== null) {
|
|
173
|
+
const safe = Object.create(null) as Record<string, string | readonly string[]>
|
|
174
|
+
Object.assign(safe, res.headers)
|
|
175
|
+
res.headers = safe
|
|
176
|
+
}
|
|
177
|
+
apply(res.headers)
|
|
163
178
|
}
|