@nifrajs/middleware 2.8.2 → 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.
Files changed (59) hide show
  1. package/README.md +22 -1
  2. package/dist/_utils.d.ts +6 -1
  3. package/dist/_utils.d.ts.map +1 -1
  4. package/dist/_utils.js +39 -28
  5. package/dist/_utils.js.map +1 -1
  6. package/dist/cache-control.d.ts.map +1 -1
  7. package/dist/cache-control.js +31 -15
  8. package/dist/cache-control.js.map +1 -1
  9. package/dist/cache.d.ts +1 -1
  10. package/dist/cache.d.ts.map +1 -1
  11. package/dist/cache.js +2 -3
  12. package/dist/cache.js.map +1 -1
  13. package/dist/compression.d.ts +10 -10
  14. package/dist/compression.d.ts.map +1 -1
  15. package/dist/compression.js +191 -70
  16. package/dist/compression.js.map +1 -1
  17. package/dist/cors.d.ts.map +1 -1
  18. package/dist/cors.js +27 -16
  19. package/dist/cors.js.map +1 -1
  20. package/dist/etag.d.ts +4 -2
  21. package/dist/etag.d.ts.map +1 -1
  22. package/dist/etag.js +135 -63
  23. package/dist/etag.js.map +1 -1
  24. package/dist/language.d.ts.map +1 -1
  25. package/dist/language.js +14 -15
  26. package/dist/language.js.map +1 -1
  27. package/dist/logger.d.ts.map +1 -1
  28. package/dist/logger.js +34 -16
  29. package/dist/logger.js.map +1 -1
  30. package/dist/powered-by.d.ts.map +1 -1
  31. package/dist/powered-by.js +8 -5
  32. package/dist/powered-by.js.map +1 -1
  33. package/dist/pretty-json.d.ts +5 -2
  34. package/dist/pretty-json.d.ts.map +1 -1
  35. package/dist/pretty-json.js +85 -64
  36. package/dist/pretty-json.js.map +1 -1
  37. package/dist/rate-limit.d.ts.map +1 -1
  38. package/dist/rate-limit.js +76 -19
  39. package/dist/rate-limit.js.map +1 -1
  40. package/dist/request-id.js +1 -1
  41. package/dist/request-id.js.map +1 -1
  42. package/dist/security-headers.d.ts +8 -4
  43. package/dist/security-headers.d.ts.map +1 -1
  44. package/dist/security-headers.js +18 -16
  45. package/dist/security-headers.js.map +1 -1
  46. package/package.json +3 -3
  47. package/src/_utils.ts +39 -24
  48. package/src/cache-control.ts +27 -14
  49. package/src/cache.ts +2 -4
  50. package/src/compression.ts +202 -62
  51. package/src/cors.ts +23 -14
  52. package/src/etag.ts +141 -59
  53. package/src/language.ts +12 -13
  54. package/src/logger.ts +24 -6
  55. package/src/powered-by.ts +8 -4
  56. package/src/pretty-json.ts +95 -70
  57. package/src/rate-limit.ts +86 -20
  58. package/src/request-id.ts +1 -1
  59. package/src/security-headers.ts +17 -15
@@ -2,6 +2,33 @@ import { definePlugin } from "@nifrajs/core/server";
2
2
  // Text-like payloads benefit from gzip; binary media (images/video/archives) is already compressed.
3
3
  const COMPRESSIBLE = /^(?:text\/|application\/(?:json|[\w.-]+\+json|javascript|xml|[\w.-]+\+xml|wasm|x-ndjson)|image\/svg\+xml)/i;
4
4
  const defaultCompressible = (contentType) => COMPRESSIBLE.test(contentType);
5
+ /** Honor explicit q=0 exclusions; a client that says `gzip;q=0` must not receive gzip. */
6
+ function acceptsGzip(value) {
7
+ if (value === null)
8
+ return false;
9
+ let wildcard;
10
+ let gzip;
11
+ for (const item of value.split(",")) {
12
+ const parts = item.trim().split(";");
13
+ const coding = parts[0]?.trim().toLowerCase();
14
+ if (coding !== "gzip" && coding !== "*")
15
+ continue;
16
+ let quality = 1;
17
+ for (let i = 1; i < parts.length; i++) {
18
+ const [name, raw] = parts[i].trim().split("=", 2);
19
+ if (name?.toLowerCase() === "q") {
20
+ const parsed = Number(raw);
21
+ quality = Number.isFinite(parsed) && parsed >= 0 && parsed <= 1 ? parsed : 0;
22
+ }
23
+ }
24
+ if (coding === "gzip")
25
+ gzip = quality;
26
+ else
27
+ wildcard = quality;
28
+ }
29
+ return (gzip ?? wildcard ?? 0) > 0;
30
+ }
31
+ const GZIP_ENCODER = new TextEncoder();
5
32
  const concat = (chunks, total) => {
6
33
  const out = new Uint8Array(total);
7
34
  let offset = 0;
@@ -14,92 +41,186 @@ const concat = (chunks, total) => {
14
41
  const gzipHeaders = (source) => {
15
42
  const headers = new Headers(source);
16
43
  headers.set("content-encoding", "gzip");
17
- headers.delete("content-length"); // compressed length is unknown up front → chunked
44
+ headers.delete("content-length");
45
+ addGzipVary(headers);
46
+ return headers;
47
+ };
48
+ function addGzipVary(headers) {
18
49
  const vary = headers.get("vary");
19
- if (vary === null)
50
+ if (vary === null) {
20
51
  headers.set("vary", "Accept-Encoding");
21
- else if (!vary.toLowerCase().split(",").includes("accept-encoding"))
52
+ }
53
+ else if (!vary
54
+ .toLowerCase()
55
+ .split(",")
56
+ .some((part) => part.trim() === "accept-encoding")) {
22
57
  headers.set("vary", `${vary}, Accept-Encoding`);
23
- return headers;
24
- };
25
- /**
26
- * Transparently **gzip** responses when the client sends `Accept-Encoding: gzip` and the body is a
27
- * compressible type larger than `threshold`. Uses the Web-standard `CompressionStream` (streaming, no
28
- * full-body buffering), so it runs on every nifra runtime including the edge. gzip is the one encoding
29
- * `CompressionStream` guarantees everywhere; brotli isn't part of the standard, so it's not offered.
30
- *
31
- * The body is **peeked** up to `threshold` bytes to enforce the size floor: a response that ends below
32
- * the threshold is sent uncompressed (gzip's ~20-byte overhead would enlarge it), and only larger
33
- * bodies are compressed - the buffered prefix is replayed and the remainder streamed. (Runtimes rarely
34
- * set `Content-Length` on a constructed `Response`, so the header alone can't gate this.)
35
- *
36
- * Skips: clients that don't accept gzip, already-encoded responses, bodyless responses (204/304/HEAD),
37
- * `Range` responses (206 / `Content-Range`), `Cache-Control: no-transform`, and non-compressible types.
38
- * Adds `Vary: Accept-Encoding`.
39
- *
40
- * ```ts
41
- * app.use(compression())
42
- * ```
43
- */
44
- export function compression(options = {}) {
45
- const threshold = options.threshold ?? 1024;
46
- const isCompressible = options.compressible ?? defaultCompressible;
47
- return definePlugin("compression", (app) => app.onResponse(async (res, req) => {
48
- const body = res.body;
49
- if (body === null)
50
- return res; // 204/304/HEAD - nothing to compress
51
- if (!(req.headers.get("accept-encoding") ?? "").toLowerCase().includes("gzip"))
52
- return res;
53
- if (res.headers.has("content-encoding"))
54
- return res; // already encoded
55
- if (res.status === 206 || res.headers.has("content-range"))
56
- return res; // ranges pass through
57
- if ((res.headers.get("cache-control") ?? "").includes("no-transform"))
58
- return res;
59
- if (!isCompressible(res.headers.get("content-type") ?? ""))
60
- return res;
61
- const declared = res.headers.get("content-length");
62
- if (declared !== null && Number(declared) < threshold)
63
- return res; // fast path when length is known
64
- // Peek up to `threshold` bytes to decide whether compressing is worth it.
65
- const reader = body.getReader();
66
- const buffered = [];
67
- let total = 0;
58
+ }
59
+ }
60
+ function hasNoTransform(value) {
61
+ return (value
62
+ ?.split(",")
63
+ .some((directive) => directive.trim().split(";", 1)[0]?.trim().toLowerCase() === "no-transform") ?? false);
64
+ }
65
+ /** Gzip a resident buffer through the Web-standard `CompressionStream` (available everywhere). */
66
+ async function gzipBytes(bytes) {
67
+ // No explicit generic: the chunk type infers from `enqueue`, which keeps the stream assignable
68
+ // to `CompressionStream`'s typing across the Bun/DOM lib variants.
69
+ const source = new ReadableStream({
70
+ start(controller) {
71
+ controller.enqueue(bytes);
72
+ controller.close();
73
+ },
74
+ });
75
+ const compressed = await new Response(source.pipeThrough(new CompressionStream("gzip"))).arrayBuffer();
76
+ return new Uint8Array(compressed);
77
+ }
78
+ /** Stream-safe fallback for handler-returned/proxied Responses. Peek only enough to enforce the
79
+ * threshold, then replay the prefix through CompressionStream without buffering the remainder. */
80
+ function compressRawResponse(response, req, threshold, isCompressible) {
81
+ const body = response.body;
82
+ if (body === null)
83
+ return response;
84
+ if (response.status === 204 || response.status === 205 || response.status === 304) {
85
+ return response;
86
+ }
87
+ if (!acceptsGzip(req.headers.get("accept-encoding")))
88
+ return response;
89
+ if (response.headers.has("content-encoding"))
90
+ return response;
91
+ if (response.status === 206 || response.headers.has("content-range"))
92
+ return response;
93
+ if (hasNoTransform(response.headers.get("cache-control")))
94
+ return response;
95
+ if (!isCompressible(response.headers.get("content-type") ?? ""))
96
+ return response;
97
+ const declared = response.headers.get("content-length");
98
+ if (declared !== null && Number(declared) < threshold)
99
+ return response;
100
+ return compressRawBody(response, body, threshold);
101
+ }
102
+ async function compressRawBody(response, body, threshold) {
103
+ const reader = body.getReader();
104
+ const buffered = [];
105
+ let total = 0;
106
+ try {
68
107
  while (total < threshold) {
69
108
  const { done, value } = await reader.read();
70
109
  if (done) {
71
- // Body ended below the threshold - too small to be worth gzipping. Emit as-is.
72
110
  return new Response(concat(buffered, total), {
73
- status: res.status,
74
- statusText: res.statusText,
75
- headers: res.headers,
111
+ status: response.status,
112
+ statusText: response.statusText,
113
+ headers: response.headers,
76
114
  });
77
115
  }
116
+ if (value === undefined)
117
+ return replayRawResponse(response, buffered, reader);
78
118
  buffered.push(value);
79
119
  total += value.byteLength;
80
120
  }
81
- // Over the threshold: replay the buffered prefix, then stream the rest - all through gzip.
82
- // (No explicit generic: it lets the chunk type infer from the non-shared reader so the stream
83
- // matches `CompressionStream`'s `Uint8Array` typing.)
84
- const source = new ReadableStream({
85
- start(controller) {
86
- for (const chunk of buffered)
87
- controller.enqueue(chunk);
88
- },
89
- async pull(controller) {
121
+ }
122
+ catch {
123
+ return replayRawResponse(response, buffered, reader);
124
+ }
125
+ const source = new ReadableStream({
126
+ start(controller) {
127
+ for (const chunk of buffered)
128
+ controller.enqueue(chunk);
129
+ },
130
+ async pull(controller) {
131
+ try {
90
132
  const { done, value } = await reader.read();
91
- if (done)
133
+ if (done || value === undefined)
92
134
  controller.close();
93
135
  else
94
136
  controller.enqueue(value);
95
- },
96
- cancel: (reason) => reader.cancel(reason),
97
- });
98
- return new Response(source.pipeThrough(new CompressionStream("gzip")), {
99
- status: res.status,
100
- statusText: res.statusText,
101
- headers: gzipHeaders(res.headers),
102
- });
137
+ }
138
+ catch (error) {
139
+ controller.error(error);
140
+ }
141
+ },
142
+ cancel: (reason) => reader.cancel(reason),
143
+ });
144
+ return new Response(source.pipeThrough(new CompressionStream("gzip")), {
145
+ status: response.status,
146
+ statusText: response.statusText,
147
+ headers: gzipHeaders(response.headers),
148
+ });
149
+ }
150
+ function replayRawResponse(response, chunks, reader) {
151
+ const stream = new ReadableStream({
152
+ start(controller) {
153
+ for (const chunk of chunks)
154
+ controller.enqueue(chunk);
155
+ },
156
+ async pull(controller) {
157
+ try {
158
+ const { done, value } = await reader.read();
159
+ if (done || value === undefined)
160
+ controller.close();
161
+ else
162
+ controller.enqueue(value);
163
+ }
164
+ catch (error) {
165
+ controller.error(error);
166
+ }
167
+ },
168
+ cancel: (reason) => reader.cancel(reason),
169
+ });
170
+ return new Response(stream, {
171
+ status: response.status,
172
+ statusText: response.statusText,
173
+ headers: response.headers,
174
+ });
175
+ }
176
+ /**
177
+ * Transparently **gzip** responses when the client sends `Accept-Encoding: gzip` and the body is a
178
+ * compressible type larger than `threshold`. Uses the Web-standard `CompressionStream`, so it runs
179
+ * on every nifra runtime including the edge; gzip is the one encoding `CompressionStream`
180
+ * guarantees everywhere, so brotli isn't offered.
181
+ *
182
+ * Built on the portable `onResponseBody` tier for framework-serialized bytes, with a stream-safe raw
183
+ * fallback for handler-returned/proxied Responses. The common JSON path stays on Node's direct
184
+ * writer; raw streams are compressed incrementally without being fully buffered.
185
+ *
186
+ * Skips: clients that don't accept gzip, already-encoded responses, bodyless responses,
187
+ * `Range` responses (206 / `Content-Range`), `Cache-Control: no-transform`, non-compressible
188
+ * types, and bodies below `threshold` (gzip's ~20-byte overhead would enlarge them). Adds
189
+ * `Vary: Accept-Encoding`.
190
+ *
191
+ * ```ts
192
+ * app.use(compression())
193
+ * ```
194
+ */
195
+ export function compression(options = {}) {
196
+ const threshold = options.threshold ?? 1024;
197
+ if (!Number.isSafeInteger(threshold) || threshold < 0) {
198
+ throw new Error("compression: threshold must be a non-negative safe integer");
199
+ }
200
+ const isCompressible = options.compressible ?? defaultCompressible;
201
+ return definePlugin("compression", (app) => app.use({
202
+ onResponseBody(body, headers, req, status) {
203
+ if (!acceptsGzip(req.header("accept-encoding")))
204
+ return undefined;
205
+ if (headers.has("content-encoding"))
206
+ return undefined;
207
+ if (status === 206 || headers.has("content-range"))
208
+ return undefined;
209
+ if (hasNoTransform(headers.get("cache-control")))
210
+ return undefined;
211
+ if (!isCompressible(headers.get("content-type") ?? ""))
212
+ return undefined;
213
+ const bytes = typeof body === "string" ? GZIP_ENCODER.encode(body) : body;
214
+ if (bytes.byteLength < threshold)
215
+ return undefined;
216
+ return gzipBytes(bytes).then((compressed) => {
217
+ headers.set("content-encoding", "gzip");
218
+ headers.delete("content-length");
219
+ addGzipVary(headers);
220
+ return compressed;
221
+ });
222
+ },
223
+ onResponseRaw: (response, req) => compressRawResponse(response, req, threshold, isCompressible),
103
224
  }));
104
225
  }
105
226
  //# sourceMappingURL=compression.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"compression.js","sourceRoot":"","sources":["../src/compression.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"compression.js","sourceRoot":"","sources":["../src/compression.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"cors.d.ts","sourceRoot":"","sources":["../src/cors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAGtD,MAAM,WAAW,WAAW;IAC1B,wFAAwF;IACxF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,CAAA;IAChF,8EAA8E;IAC9E,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACxC,iGAAiG;IACjG,QAAQ,CAAC,cAAc,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IAC/C,+CAA+C;IAC/C,QAAQ,CAAC,cAAc,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IAC/C,2GAA2G;IAC3G,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAA;IAC9B,4CAA4C;IAC5C,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CACzB;AAwBD;;;;;;;GAOG;AACH,wBAAgB,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,UAAU,CAyC1D"}
1
+ {"version":3,"file":"cors.d.ts","sourceRoot":"","sources":["../src/cors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAEtD,MAAM,WAAW,WAAW;IAC1B,wFAAwF;IACxF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,CAAA;IAChF,8EAA8E;IAC9E,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACxC,iGAAiG;IACjG,QAAQ,CAAC,cAAc,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IAC/C,+CAA+C;IAC/C,QAAQ,CAAC,cAAc,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IAC/C,2GAA2G;IAC3G,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAA;IAC9B,4CAA4C;IAC5C,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CACzB;AAwBD;;;;;;;GAOG;AACH,wBAAgB,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,UAAU,CAmD1D"}
package/dist/cors.js CHANGED
@@ -1,4 +1,3 @@
1
- import { withHeaders } from "./_utils.js";
2
1
  const DEFAULT_METHODS = [
3
2
  "GET",
4
3
  "HEAD",
@@ -51,24 +50,36 @@ export function cors(options = {}) {
51
50
  headers.set("Access-Control-Allow-Headers", requested);
52
51
  if (maxAge !== undefined)
53
52
  headers.set("Access-Control-Max-Age", String(maxAge));
54
- // Allow-Origin / Allow-Credentials are added by onResponse (runs on every response).
53
+ // Allow-Origin / Allow-Credentials are added by onResponseHeaders (runs on every response).
55
54
  return new Response(null, { status: 204, headers });
56
55
  },
57
- onResponse(res, req) {
58
- const allowOrigin = resolveAllowOrigin(origin, req.headers.get("origin"));
56
+ onNodeRequest(req) {
57
+ const isPreflight = req.method === "OPTIONS" && req.header("access-control-request-method") !== null;
58
+ if (!isPreflight)
59
+ return undefined;
60
+ const headers = {
61
+ "Access-Control-Allow-Methods": methods,
62
+ };
63
+ const requested = allowedHeaders ?? req.header("access-control-request-headers");
64
+ if (requested)
65
+ headers["Access-Control-Allow-Headers"] = requested;
66
+ if (maxAge !== undefined)
67
+ headers["Access-Control-Max-Age"] = String(maxAge);
68
+ return new Response(null, { status: 204, headers });
69
+ },
70
+ onResponseHeaders(headers, req) {
71
+ const allowOrigin = resolveAllowOrigin(origin, req.header("origin"));
59
72
  if (allowOrigin === null)
60
- return res;
61
- // Mutate in place on the common (mutable-headers) response; clone only for an immutable one.
62
- return withHeaders(res, (headers) => {
63
- headers.set("Access-Control-Allow-Origin", allowOrigin);
64
- if (allowOrigin !== "*")
65
- headers.append("Vary", "Origin");
66
- if (credentials)
67
- headers.set("Access-Control-Allow-Credentials", "true");
68
- if (exposedHeaders !== undefined) {
69
- headers.set("Access-Control-Expose-Headers", exposedHeaders);
70
- }
71
- });
73
+ return;
74
+ headers.set("access-control-allow-origin", allowOrigin);
75
+ if (allowOrigin !== "*") {
76
+ const vary = headers.get("vary");
77
+ headers.set("vary", vary === null || vary === "" ? "Origin" : `${vary}, Origin`);
78
+ }
79
+ if (credentials)
80
+ headers.set("access-control-allow-credentials", "true");
81
+ if (exposedHeaders !== undefined)
82
+ headers.set("access-control-expose-headers", exposedHeaders);
72
83
  },
73
84
  };
74
85
  }
package/dist/cors.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cors.js","sourceRoot":"","sources":["../src/cors.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cors.js","sourceRoot":"","sources":["../src/cors.ts"],"names":[],"mappings":"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"}
package/dist/etag.d.ts CHANGED
@@ -7,8 +7,10 @@ export interface ETagOptions {
7
7
  /**
8
8
  * A {@link definePlugin} plugin that adds a content-hash `ETag` to `GET` `200` responses and returns
9
9
  * **`304 Not Modified`** when the client's `If-None-Match` matches - saving bandwidth on unchanged
10
- * responses. It reads and rebuilds small bodies only; larger responses pass through unchanged.
11
- * Idempotent.
10
+ * responses. Built on the portable `onResponseBody` tier: the hook receives the final
11
+ * framework-serialized bytes on every runtime with nothing drained, so the middleware stays on
12
+ * Node's direct writer. Handler-returned raw `Response`s (streams, proxied fetches) use a capped,
13
+ * replaying fallback so oversized bodies remain intact. Idempotent.
12
14
  */
13
15
  export declare function etag(options?: ETagOptions): import("@nifrajs/core/server").NifraPlugin<import("@nifrajs/core/server").AnyServer, import("@nifrajs/core/server").AnyServer>;
14
16
  //# sourceMappingURL=etag.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"etag.d.ts","sourceRoot":"","sources":["../src/etag.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,WAAW;IAC1B,0GAA0G;IAC1G,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAA;IACvB,oGAAoG;IACpG,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED;;;;;GAKG;AACH,wBAAgB,IAAI,CAAC,OAAO,GAAE,WAAgB,kIA0B7C"}
1
+ {"version":3,"file":"etag.d.ts","sourceRoot":"","sources":["../src/etag.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,WAAW;IAC1B,0GAA0G;IAC1G,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAA;IACvB,oGAAoG;IACpG,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAC3B;AA4ED;;;;;;;GAOG;AACH,wBAAgB,IAAI,CAAC,OAAO,GAAE,WAAgB,kIAwC7C"}
package/dist/etag.js CHANGED
@@ -8,40 +8,150 @@ function fnv1a(bytes) {
8
8
  }
9
9
  return (h >>> 0).toString(16);
10
10
  }
11
+ const ETAG_ENCODER = new TextEncoder();
12
+ async function sha256Tag(bytes) {
13
+ const input = new Uint8Array(bytes.byteLength);
14
+ input.set(bytes);
15
+ const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", input));
16
+ let hex = "";
17
+ for (const byte of digest)
18
+ hex += byte.toString(16).padStart(2, "0");
19
+ return hex;
20
+ }
21
+ const concat = (chunks, total) => {
22
+ const out = new Uint8Array(total);
23
+ let offset = 0;
24
+ for (const chunk of chunks) {
25
+ out.set(chunk, offset);
26
+ offset += chunk.byteLength;
27
+ }
28
+ return out;
29
+ };
30
+ /** Read a raw response without losing a streamed body when the cap is exceeded or the upstream
31
+ * fails. The already-read prefix is replayed through the same reader instead of cancelling a clone,
32
+ * which can stall the original body on some runtimes. */
33
+ async function readBytesCapped(res, maxBytes) {
34
+ const body = res.body;
35
+ if (body === null)
36
+ return { bytes: new Uint8Array() };
37
+ const reader = body.getReader();
38
+ const chunks = [];
39
+ let total = 0;
40
+ try {
41
+ for (;;) {
42
+ const { done, value } = await reader.read();
43
+ if (done)
44
+ return { bytes: concat(chunks, total) };
45
+ if (value === undefined)
46
+ return { response: replayFrom(res, chunks, reader) };
47
+ chunks.push(value);
48
+ total += value.byteLength;
49
+ if (total > maxBytes)
50
+ return { response: replayFrom(res, chunks, reader) };
51
+ }
52
+ }
53
+ catch {
54
+ return { response: replayFrom(res, chunks, reader) };
55
+ }
56
+ }
57
+ function replayFrom(res, chunks, reader) {
58
+ const stream = new ReadableStream({
59
+ start(controller) {
60
+ for (const chunk of chunks)
61
+ controller.enqueue(chunk);
62
+ },
63
+ async pull(controller) {
64
+ try {
65
+ const { done, value } = await reader.read();
66
+ if (done || value === undefined)
67
+ controller.close();
68
+ else
69
+ controller.enqueue(value);
70
+ }
71
+ catch (error) {
72
+ controller.error(error);
73
+ }
74
+ },
75
+ cancel: (reason) => reader.cancel(reason),
76
+ });
77
+ return new Response(stream, {
78
+ status: res.status,
79
+ statusText: res.statusText,
80
+ headers: res.headers,
81
+ });
82
+ }
11
83
  /**
12
84
  * A {@link definePlugin} plugin that adds a content-hash `ETag` to `GET` `200` responses and returns
13
85
  * **`304 Not Modified`** when the client's `If-None-Match` matches - saving bandwidth on unchanged
14
- * responses. It reads and rebuilds small bodies only; larger responses pass through unchanged.
15
- * Idempotent.
86
+ * responses. Built on the portable `onResponseBody` tier: the hook receives the final
87
+ * framework-serialized bytes on every runtime with nothing drained, so the middleware stays on
88
+ * Node's direct writer. Handler-returned raw `Response`s (streams, proxied fetches) use a capped,
89
+ * replaying fallback so oversized bodies remain intact. Idempotent.
16
90
  */
17
91
  export function etag(options = {}) {
18
- const prefix = (options.weak ?? true) ? "W/" : "";
92
+ const weak = options.weak ?? true;
93
+ const prefix = weak ? "W/" : "";
19
94
  const maxBytes = options.maxBytes ?? 1_000_000;
20
- if (!Number.isInteger(maxBytes) || maxBytes < 0) {
21
- throw new Error("etag: maxBytes must be a non-negative integer");
95
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
96
+ throw new Error("etag: maxBytes must be a non-negative safe integer");
22
97
  }
23
- return definePlugin("etag", (app) => app.onResponse(async (res, req) => {
24
- if (req.method !== "GET" || res.status !== 200 || res.body === null)
25
- return res;
26
- const declared = parseLength(res.headers.get("content-length"));
27
- if (declared !== undefined && declared > maxBytes)
28
- return res;
29
- const body = await readBytesCapped(res, maxBytes);
30
- if (body === null)
31
- return res;
32
- const tag = `${prefix}"${fnv1a(body)}"`;
33
- const headers = new Headers(res.headers);
34
- headers.set("ETag", tag);
35
- if (matchesIfNoneMatch(req.headers.get("if-none-match"), tag)) {
36
- // A 304 carries no body - drop the body-describing headers so strict intermediaries don't see a
37
- // null body with a non-zero Content-Length.
38
- headers.delete("content-length");
39
- headers.delete("content-type");
40
- return new Response(null, { status: 304, headers });
41
- }
42
- return new Response(body, { status: res.status, statusText: res.statusText, headers });
98
+ return definePlugin("etag", (app) => app.use({
99
+ onResponseBody(body, headers, req, status) {
100
+ if (req.method !== "GET" || status !== 200)
101
+ return undefined;
102
+ const bytes = typeof body === "string" ? ETAG_ENCODER.encode(body) : body;
103
+ if (bytes.byteLength > maxBytes)
104
+ return undefined;
105
+ const finish = (fingerprint) => {
106
+ const tag = prefix + String.fromCharCode(34) + fingerprint + String.fromCharCode(34);
107
+ headers.set("etag", tag);
108
+ if (matchesIfNoneMatch(req.header("if-none-match"), tag)) {
109
+ // A 304 carries no body - drop the body-describing headers so strict intermediaries
110
+ // do not see a null body with a non-zero Content-Length.
111
+ headers.delete("content-length");
112
+ headers.delete("content-type");
113
+ return { body: null, status: 304 };
114
+ }
115
+ return undefined;
116
+ };
117
+ if (weak)
118
+ return finish(fnv1a(bytes));
119
+ return sha256Tag(bytes).then(finish);
120
+ },
121
+ onResponseRaw(response, req) {
122
+ if (req.method !== "GET" || response.status !== 200 || response.body === null) {
123
+ return response;
124
+ }
125
+ const declared = response.headers.get("content-length");
126
+ if (declared !== null && /^(?:0|[1-9]\d*)$/.test(declared) && Number(declared) > maxBytes) {
127
+ return response;
128
+ }
129
+ return etagRawResponse(response, req, weak, prefix, maxBytes);
130
+ },
43
131
  }));
44
132
  }
133
+ async function etagRawResponse(response, req, weak, prefix, maxBytes) {
134
+ const read = await readBytesCapped(response, maxBytes);
135
+ if ("response" in read)
136
+ return read.response;
137
+ const fingerprint = weak ? fnv1a(read.bytes) : await sha256Tag(read.bytes);
138
+ const tag = prefix + String.fromCharCode(34) + fingerprint + String.fromCharCode(34);
139
+ const headers = new Headers(response.headers);
140
+ headers.set("etag", tag);
141
+ if (matchesIfNoneMatch(req.headers.get("if-none-match"), tag)) {
142
+ headers.delete("content-length");
143
+ headers.delete("content-type");
144
+ return new Response(null, { status: 304, headers });
145
+ }
146
+ // The body was consumed and rebuilt from bytes. Re-derive framing at the serving adapter
147
+ // instead of trusting a stale upstream declaration.
148
+ headers.delete("content-length");
149
+ return new Response(read.bytes, {
150
+ status: response.status,
151
+ statusText: response.statusText,
152
+ headers,
153
+ });
154
+ }
45
155
  function matchesIfNoneMatch(value, tag) {
46
156
  if (value === null)
47
157
  return false;
@@ -58,42 +168,4 @@ function matchesIfNoneMatch(value, tag) {
58
168
  function weakComparableTag(tag) {
59
169
  return tag.startsWith("W/") ? tag.slice(2) : tag;
60
170
  }
61
- function parseLength(value) {
62
- if (value === null)
63
- return undefined;
64
- if (!/^(?:0|[1-9]\d*)$/.test(value))
65
- return undefined;
66
- return Number(value);
67
- }
68
- async function readBytesCapped(res, maxBytes) {
69
- const body = res.clone().body;
70
- if (body === null)
71
- return new Uint8Array();
72
- const reader = body.getReader();
73
- const chunks = [];
74
- let total = 0;
75
- try {
76
- for (;;) {
77
- const { done, value } = await reader.read();
78
- if (done) {
79
- const out = new Uint8Array(total);
80
- let offset = 0;
81
- for (const chunk of chunks) {
82
- out.set(chunk, offset);
83
- offset += chunk.byteLength;
84
- }
85
- return out;
86
- }
87
- total += value.byteLength;
88
- if (total > maxBytes) {
89
- await reader.cancel();
90
- return null;
91
- }
92
- chunks.push(value);
93
- }
94
- }
95
- catch {
96
- return null;
97
- }
98
- }
99
171
  //# sourceMappingURL=etag.js.map