@zudojs/http 1.2.0 → 1.3.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 (69) hide show
  1. package/dist/httpAdapter/http.adapters.d.ts +24 -3
  2. package/dist/httpAdapter/http.adapters.js +20 -11
  3. package/dist/httpAdapter/node/httpNode.request.js +1 -1
  4. package/dist/httpAgent/http.agent.d.ts +22 -3
  5. package/dist/httpAgent/http.agent.js +52 -14
  6. package/dist/httpCacheControl/httpCacheControl.freshness.d.ts +7 -1
  7. package/dist/httpCacheControl/httpCacheControl.freshness.js +30 -3
  8. package/dist/httpKeepAlive/httpKeepAlive.core.js +14 -2
  9. package/dist/httpMiddleware/builtin/logging/httpMiddleware.logging.d.ts +18 -0
  10. package/dist/httpMiddleware/builtin/logging/httpMiddleware.logging.js +17 -1
  11. package/dist/httpMiddleware/builtin/security/httpMiddleware.security.d.ts +11 -0
  12. package/dist/httpMiddleware/builtin/security/httpMiddleware.security.js +25 -12
  13. package/dist/httpNegotiation/httpNegotiation.core.d.ts +13 -0
  14. package/dist/httpNegotiation/httpNegotiation.core.js +60 -8
  15. package/dist/httpProxy/http.proxy.d.ts +9 -0
  16. package/dist/httpProxy/http.proxy.js +35 -3
  17. package/dist/httpQuery/index.d.ts +11 -2
  18. package/dist/httpQuery/index.js +11 -2
  19. package/dist/httpQuery/queryParse/index.d.ts +10 -0
  20. package/dist/httpQuery/queryParse/index.js +10 -0
  21. package/dist/httpQuery/queryParse/queryParse.flat.d.ts +13 -0
  22. package/dist/httpQuery/queryParse/queryParse.flat.js +44 -0
  23. package/dist/httpQuery/queryParse/queryParse.nested.d.ts +25 -0
  24. package/dist/httpQuery/queryParse/queryParse.nested.js +112 -0
  25. package/dist/httpQuery/queryParse/queryParse.tokenizer.d.ts +37 -0
  26. package/dist/httpQuery/queryParse/queryParse.tokenizer.js +95 -0
  27. package/dist/httpQuery/queryRequest/index.d.ts +9 -0
  28. package/dist/httpQuery/queryRequest/index.js +9 -0
  29. package/dist/httpQuery/queryRequest/query.request.d.ts +43 -0
  30. package/dist/httpQuery/queryRequest/query.request.js +96 -0
  31. package/dist/httpQuery/querySerialize/index.d.ts +10 -0
  32. package/dist/httpQuery/querySerialize/index.js +10 -0
  33. package/dist/httpQuery/querySerialize/query.util.d.ts +21 -0
  34. package/dist/httpQuery/querySerialize/query.util.js +67 -0
  35. package/dist/httpQuery/querySerialize/querySerialize.core.d.ts +12 -0
  36. package/dist/httpQuery/querySerialize/querySerialize.core.js +97 -0
  37. package/dist/httpQuery/queryTypes/index.d.ts +11 -0
  38. package/dist/httpQuery/queryTypes/index.js +9 -0
  39. package/dist/httpQuery/queryTypes/query.container.d.ts +16 -0
  40. package/dist/httpQuery/queryTypes/query.container.js +51 -0
  41. package/dist/httpQuery/queryTypes/query.limit.d.ts +25 -0
  42. package/dist/httpQuery/queryTypes/query.limit.js +32 -0
  43. package/dist/httpQuery/queryTypes/query.type.d.ts +62 -0
  44. package/dist/httpQuery/queryTypes/query.type.js +2 -0
  45. package/dist/httpRedirect/http.redirect.d.ts +6 -0
  46. package/dist/httpRedirect/http.redirect.js +53 -2
  47. package/dist/httpRequest/http.request.d.ts +61 -2
  48. package/dist/httpRequest/http.request.js +86 -35
  49. package/dist/httpRequest/httpRequest.context.js +11 -18
  50. package/dist/httpRequest/target/httpRequest.target.d.ts +2 -2
  51. package/dist/httpRequest/target/httpRequest.target.js +23 -5
  52. package/dist/httpRouter/core/factory/httpRoute.factory.base.d.ts +18 -1
  53. package/dist/httpRouter/core/factory/httpRoute.factory.base.js +49 -6
  54. package/dist/httpRouter/core/factory/httpRoute.factory.js +3 -3
  55. package/dist/httpRouter/core/register/httpRouter.register.js +16 -21
  56. package/dist/httpRouter/core/types/httpRouter.type.d.ts +6 -0
  57. package/dist/httpRouter/core/util/httpRoute.util.d.ts +47 -0
  58. package/dist/httpRouter/core/util/httpRoute.util.js +85 -4
  59. package/dist/httpRouter/dispatch/httpRoute.dispatcher.d.ts +11 -0
  60. package/dist/httpRouter/dispatch/httpRoute.dispatcher.js +5 -3
  61. package/dist/httpRouter/matching/httpRoute.matcher.core.js +51 -6
  62. package/dist/httpRouter/matching/httpRoute.matcher.d.ts +0 -1
  63. package/dist/httpRouter/matching/httpRoute.matcher.js +64 -26
  64. package/dist/httpRouter/pattern/httpRoute.pattern.parse.d.ts +16 -0
  65. package/dist/httpRouter/pattern/httpRoute.pattern.parse.js +40 -11
  66. package/dist/httpRouter/pattern/index.d.ts +1 -1
  67. package/dist/httpRouter/pattern/index.js +1 -1
  68. package/dist/httpSecurity/httpSecurity.validator.js +16 -7
  69. package/package.json +5 -5
@@ -1,5 +1,6 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import type { HTTPContext, HTTPHandler, HTTPMiddleware, HTTPRequest, HTTPResponse, HTTPRouteMatch, HTTPState } from "../httpTypes/http.types.js";
3
+ import type { TrustProxy } from "../httpTrustProxy/httpTrustProxy.type.js";
3
4
  export interface HTTPAdapter<State extends HTTPState = HTTPState> {
4
5
  readonly name: string;
5
6
  createRequest(request: IncomingMessage): HTTPRequest;
@@ -9,9 +10,21 @@ export interface HTTPAdapter<State extends HTTPState = HTTPState> {
9
10
  readonly signal?: AbortSignal;
10
11
  }): HTTPContext<State>;
11
12
  }
13
+ /**
14
+ * Options every Node request built by this module accepts.
15
+ *
16
+ * `trustProxy` defaults to `false`, so `X-Forwarded-For` and
17
+ * `X-Forwarded-Proto` are ignored and the socket peer decides `request.ip`,
18
+ * `request.protocol` and `request.secure`.
19
+ */
20
+ export interface NodeRequestTrustOptions {
21
+ readonly trustProxy?: TrustProxy;
22
+ }
12
23
  export declare class NodeHTTPAdapter<State extends HTTPState = HTTPState> implements HTTPAdapter<State> {
13
24
  readonly name = "node";
14
- createRequest(request: IncomingMessage): HTTPRequest;
25
+ private readonly trustProxy;
26
+ constructor(options?: NodeRequestTrustOptions);
27
+ createRequest(request: IncomingMessage, options?: NodeRequestTrustOptions): HTTPRequest;
15
28
  createResponse(response: ServerResponse): HTTPResponse;
16
29
  createContext(request: HTTPRequest, response: HTTPResponse, options?: {
17
30
  readonly state?: State;
@@ -20,12 +33,19 @@ export declare class NodeHTTPAdapter<State extends HTTPState = HTTPState> implem
20
33
  }
21
34
  export interface HTTPAdapterOptions<State extends HTTPState = HTTPState> {
22
35
  readonly adapter?: HTTPAdapter<State>;
36
+ /**
37
+ * Which socket peers may speak for a client through `X-Forwarded-*`.
38
+ * Defaults to `false` — forwarded headers are ignored.
39
+ */
40
+ readonly trustProxy?: TrustProxy;
23
41
  }
24
- export declare function createHTTPAdapter<State extends HTTPState = HTTPState>(): NodeHTTPAdapter<State>;
42
+ export declare function createHTTPAdapter<State extends HTTPState = HTTPState>(options?: NodeRequestTrustOptions): NodeHTTPAdapter<State>;
25
43
  export interface RequestAdapter {
26
44
  toHTTPRequest(request: IncomingMessage): HTTPRequest;
27
45
  }
28
46
  export declare class NodeRequestAdapter implements RequestAdapter {
47
+ private readonly trustProxy;
48
+ constructor(options?: NodeRequestTrustOptions);
29
49
  toHTTPRequest(request: IncomingMessage): HTTPRequest;
30
50
  }
31
51
  export interface ResponseAdapter {
@@ -61,11 +81,12 @@ export declare class DefaultHTTPMiddlewareAdapter<State extends HTTPState = HTTP
61
81
  export interface HTTPRouteAdapter<State extends HTTPState = HTTPState> {
62
82
  match(method: string, path: string): HTTPRouteMatch<State> | undefined;
63
83
  }
64
- export declare function adaptNodeRequest(request: IncomingMessage): HTTPRequest;
84
+ export declare function adaptNodeRequest(request: IncomingMessage, options?: NodeRequestTrustOptions): HTTPRequest;
65
85
  export declare function adaptNodeResponse(response: ServerResponse): HTTPResponse;
66
86
  export declare function adaptNodeContext<State extends HTTPState = HTTPState>(request: IncomingMessage, response: ServerResponse, options?: {
67
87
  readonly state?: State;
68
88
  readonly signal?: AbortSignal;
89
+ readonly trustProxy?: TrustProxy;
69
90
  }): HTTPContext<State>;
70
91
  export declare function isHTTPAdapter(value: unknown): value is HTTPAdapter;
71
92
  //# sourceMappingURL=http.adapters.d.ts.map
@@ -2,13 +2,16 @@ import { createHTTPContext } from "../httpContext/http.context.js";
2
2
  import { createHTTPRequest } from "../httpRequest/http.request.js";
3
3
  import { createHTTPResponse } from "../httpResponse/http.response.js";
4
4
  import { createDefaultContextLogger } from "./httpAdapter.logger.js";
5
- /* -------------------------------------------------------------------------- */
6
- /* Node Adapter */
7
- /* -------------------------------------------------------------------------- */
8
5
  export class NodeHTTPAdapter {
9
6
  name = "node";
10
- createRequest(request) {
11
- return createHTTPRequest(request);
7
+ trustProxy;
8
+ constructor(options = {}) {
9
+ this.trustProxy = options.trustProxy ?? false;
10
+ }
11
+ createRequest(request, options = {}) {
12
+ return createHTTPRequest(request, {
13
+ trustProxy: options.trustProxy ?? this.trustProxy,
14
+ });
12
15
  }
13
16
  createResponse(response) {
14
17
  return createHTTPResponse(response);
@@ -26,12 +29,16 @@ export class NodeHTTPAdapter {
26
29
  /* -------------------------------------------------------------------------- */
27
30
  /* Adapter Factory */
28
31
  /* -------------------------------------------------------------------------- */
29
- export function createHTTPAdapter() {
30
- return new NodeHTTPAdapter();
32
+ export function createHTTPAdapter(options = {}) {
33
+ return new NodeHTTPAdapter(options);
31
34
  }
32
35
  export class NodeRequestAdapter {
36
+ trustProxy;
37
+ constructor(options = {}) {
38
+ this.trustProxy = options.trustProxy ?? false;
39
+ }
33
40
  toHTTPRequest(request) {
34
- return createHTTPRequest(request);
41
+ return createHTTPRequest(request, { trustProxy: this.trustProxy });
35
42
  }
36
43
  }
37
44
  export class NodeResponseAdapter {
@@ -76,15 +83,17 @@ export class DefaultHTTPMiddlewareAdapter {
76
83
  /* -------------------------------------------------------------------------- */
77
84
  /* Request Conversion Helpers */
78
85
  /* -------------------------------------------------------------------------- */
79
- export function adaptNodeRequest(request) {
80
- return createHTTPRequest(request);
86
+ export function adaptNodeRequest(request, options = {}) {
87
+ return createHTTPRequest(request, {
88
+ trustProxy: options.trustProxy ?? false,
89
+ });
81
90
  }
82
91
  export function adaptNodeResponse(response) {
83
92
  return createHTTPResponse(response);
84
93
  }
85
94
  export function adaptNodeContext(request, response, options = {}) {
86
95
  return createHTTPContext({
87
- request: adaptNodeRequest(request),
96
+ request: adaptNodeRequest(request, { trustProxy: options.trustProxy }),
88
97
  response: adaptNodeResponse(response),
89
98
  state: options.state ?? {},
90
99
  signal: options.signal,
@@ -6,7 +6,7 @@
6
6
  import { HttpRequestContext, createRequestContext, } from "../../httpRequest/httpRequest.context.js";
7
7
  import { getClientIp, isTrustedProxy, } from "../../httpTrustProxy/httpTrustProxy.core.js";
8
8
  import { removePort, extractPort } from "./httpNode.server.js";
9
- import { parseQueryString } from "../../httpQuery/http.query.js";
9
+ import { parseQueryString } from "../../httpQuery/index.js";
10
10
  import { findRequestTargetViolation } from "../../httpRequest/target/httpRequest.target.js";
11
11
  /* -------------------------------------------------------------------------- */
12
12
  /* Proxy Trust */
@@ -46,9 +46,28 @@ export interface AgentRegistryKey {
46
46
  readonly name?: string;
47
47
  }
48
48
  export declare function getOrCreateAgent(key: string | AgentRegistryKey, options?: HTTPAgentConfig | HTTPSAgentConfig): HTTPAgentInstance;
49
- export declare function getAgent(key: string | AgentRegistryKey): HTTPAgentInstance | undefined;
50
- export declare function hasAgent(key: string | AgentRegistryKey): boolean;
51
- export declare function removeAgent(key: string | AgentRegistryKey, destroy?: boolean): boolean;
49
+ /**
50
+ * Looks up a registered agent.
51
+ *
52
+ * `options` must be the same TLS-relevant options `getOrCreateAgent` was
53
+ * given, because they are part of the key; omitting them looks up the agent
54
+ * created with no options.
55
+ */
56
+ export declare function getAgent(key: string | AgentRegistryKey, options?: HTTPAgentConfig | HTTPSAgentConfig): HTTPAgentInstance | undefined;
57
+ /**
58
+ * Whether an agent is registered for this key and TLS option set.
59
+ */
60
+ export declare function hasAgent(key: string | AgentRegistryKey, options?: HTTPAgentConfig | HTTPSAgentConfig): boolean;
61
+ /**
62
+ * Removes registered agents for a key.
63
+ *
64
+ * With `options` the single matching agent is removed; without them every
65
+ * agent registered for that host is removed, whatever TLS options it was
66
+ * created with, so a per-host teardown releases all of its sockets.
67
+ *
68
+ * @returns Whether anything was removed.
69
+ */
70
+ export declare function removeAgent(key: string | AgentRegistryKey, destroy?: boolean, options?: HTTPAgentConfig | HTTPSAgentConfig): boolean;
52
71
  export declare function clearAgents(destroy?: boolean): void;
53
72
  export declare function getRegisteredAgentKeys(): string[];
54
73
  export declare function getDefaultHTTPAgent(): HTTPAgent;
@@ -113,7 +113,7 @@ export function getOrCreateAgent(key, options = {}) {
113
113
  * passing `rejectUnauthorized: false` would have disabled certificate
114
114
  * verification for a caller that had pinned a CA, with no way to detect it.
115
115
  */
116
- const cacheKey = `${registryKey}|${tlsFingerprint(options)}`;
116
+ const cacheKey = agentCacheKey(registryKey, options);
117
117
  const existing = agentRegistry.get(cacheKey);
118
118
  if (existing) {
119
119
  return existing;
@@ -127,6 +127,18 @@ export function getOrCreateAgent(key, options = {}) {
127
127
  agentRegistry.set(cacheKey, agent);
128
128
  return agent;
129
129
  }
130
+ /**
131
+ * Builds the registry key an agent is stored under.
132
+ *
133
+ * Every read and write goes through this one helper. `getOrCreateAgent` used
134
+ * to append the TLS fingerprint while `getAgent`, `hasAgent` and
135
+ * `removeAgent` looked up the bare registry key, so every lookup missed and
136
+ * the documented per-host teardown silently leaked the agent and its
137
+ * keep-alive sockets.
138
+ */
139
+ function agentCacheKey(registryKey, options) {
140
+ return `${registryKey}|${tlsFingerprint(options)}`;
141
+ }
130
142
  /**
131
143
  * Builds a stable fingerprint of the TLS-relevant fields of an agent config.
132
144
  */
@@ -156,23 +168,49 @@ function describeCredential(value) {
156
168
  }
157
169
  return "opaque";
158
170
  }
159
- export function getAgent(key) {
160
- return agentRegistry.get(normalizeRegistryKey(key));
171
+ /**
172
+ * Looks up a registered agent.
173
+ *
174
+ * `options` must be the same TLS-relevant options `getOrCreateAgent` was
175
+ * given, because they are part of the key; omitting them looks up the agent
176
+ * created with no options.
177
+ */
178
+ export function getAgent(key, options = {}) {
179
+ return agentRegistry.get(agentCacheKey(normalizeRegistryKey(key), options));
161
180
  }
162
- export function hasAgent(key) {
163
- return agentRegistry.has(normalizeRegistryKey(key));
181
+ /**
182
+ * Whether an agent is registered for this key and TLS option set.
183
+ */
184
+ export function hasAgent(key, options = {}) {
185
+ return agentRegistry.has(agentCacheKey(normalizeRegistryKey(key), options));
164
186
  }
165
- export function removeAgent(key, destroy = true) {
187
+ /**
188
+ * Removes registered agents for a key.
189
+ *
190
+ * With `options` the single matching agent is removed; without them every
191
+ * agent registered for that host is removed, whatever TLS options it was
192
+ * created with, so a per-host teardown releases all of its sockets.
193
+ *
194
+ * @returns Whether anything was removed.
195
+ */
196
+ export function removeAgent(key, destroy = true, options) {
166
197
  const registryKey = normalizeRegistryKey(key);
167
- const agent = agentRegistry.get(registryKey);
168
- if (!agent) {
169
- return false;
170
- }
171
- agentRegistry.delete(registryKey);
172
- if (destroy) {
173
- agent.destroy();
198
+ const cacheKeys = options === undefined
199
+ ? Array.from(agentRegistry.keys()).filter((candidate) => candidate.startsWith(`${registryKey}|`))
200
+ : [agentCacheKey(registryKey, options)];
201
+ let removed = false;
202
+ for (const cacheKey of cacheKeys) {
203
+ const agent = agentRegistry.get(cacheKey);
204
+ if (!agent) {
205
+ continue;
206
+ }
207
+ agentRegistry.delete(cacheKey);
208
+ if (destroy) {
209
+ agent.destroy();
210
+ }
211
+ removed = true;
174
212
  }
175
- return true;
213
+ return removed;
176
214
  }
177
215
  export function clearAgents(destroy = true) {
178
216
  if (destroy) {
@@ -12,11 +12,17 @@ import type { CacheFreshness } from "./core/httpCacheControl.type.js";
12
12
  * the origin marked must-revalidate-before-reuse must never be reported
13
13
  * fresh.
14
14
  *
15
+ * The response's current age follows RFC 9111 section 4.2.3: the `Age`
16
+ * header plus the time elapsed since the response's `Date`. Reading `Age`
17
+ * alone — as this used to — left every response without that header aged
18
+ * `0` forever, so `isFresh()` answered `true` for a response of any age.
19
+ *
15
20
  * @param responseHeaders - The cached response's headers.
16
21
  * @param responseDate - The response's `Date`, if already parsed.
22
+ * @param now - The current time, defaulting to `Date.now()`.
17
23
  * @returns The freshness calculation.
18
24
  */
19
- export declare function calculateFreshness(responseHeaders: Readonly<Record<string, string>>, responseDate?: Date): CacheFreshness;
25
+ export declare function calculateFreshness(responseHeaders: Readonly<Record<string, string>>, responseDate?: Date, now?: Date): CacheFreshness;
20
26
  /**
21
27
  * Determines if a cached response is still fresh.
22
28
  */
@@ -12,24 +12,37 @@ import { parseCacheControl } from "./core/httpCacheControl.parse.js";
12
12
  * the origin marked must-revalidate-before-reuse must never be reported
13
13
  * fresh.
14
14
  *
15
+ * The response's current age follows RFC 9111 section 4.2.3: the `Age`
16
+ * header plus the time elapsed since the response's `Date`. Reading `Age`
17
+ * alone — as this used to — left every response without that header aged
18
+ * `0` forever, so `isFresh()` answered `true` for a response of any age.
19
+ *
15
20
  * @param responseHeaders - The cached response's headers.
16
21
  * @param responseDate - The response's `Date`, if already parsed.
22
+ * @param now - The current time, defaulting to `Date.now()`.
17
23
  * @returns The freshness calculation.
18
24
  */
19
- export function calculateFreshness(responseHeaders, responseDate) {
25
+ export function calculateFreshness(responseHeaders, responseDate, now = new Date()) {
20
26
  const cacheControl = responseHeaders["cache-control"];
21
27
  const directives = parseCacheControl(cacheControl);
22
- const date = responseDate ?? new Date(responseHeaders["date"] ?? Date.now());
28
+ const date = responseDate ?? parseDateHeader(responseHeaders["date"], now);
23
29
  /*
24
30
  * RFC 9111 section 5.1: Age is a non-negative delta-seconds. A negative or
25
31
  * suffixed value would otherwise inflate freshness without bound, which
26
32
  * pins a poisoned response in cache far past the origin's TTL.
27
33
  */
28
34
  const rawAge = (responseHeaders["age"] ?? "").trim();
29
- const age = /^\d+$/.test(rawAge) ? Number(rawAge) : 0;
35
+ const ageHeader = /^\d+$/.test(rawAge) ? Number(rawAge) : 0;
36
+ const residentSeconds = Math.max(0, Math.floor((now.getTime() - date.getTime()) / 1_000));
37
+ const age = ageHeader + residentSeconds;
30
38
  const expiresHeader = responseHeaders["expires"];
31
39
  const expires = expiresHeader ? parseExpires(expiresHeader) : undefined;
32
40
  const effectiveMaxAge = resolveMaxAge(directives, expires, date);
41
+ /*
42
+ * With an `Expires`-derived lifetime this reduces to `expires - now`,
43
+ * because the lifetime is measured from `Date` and the age is measured to
44
+ * `now`.
45
+ */
33
46
  const remaining = Math.max(0, effectiveMaxAge - age);
34
47
  const stale = remaining <= 0 ||
35
48
  directives.noCache === true ||
@@ -43,6 +56,20 @@ export function calculateFreshness(responseHeaders, responseDate) {
43
56
  remaining,
44
57
  };
45
58
  }
59
+ /**
60
+ * Parses a `Date` header, falling back to the current time.
61
+ *
62
+ * @param value - The raw `Date` value.
63
+ * @param now - The fallback instant.
64
+ * @returns The parsed date.
65
+ */
66
+ function parseDateHeader(value, now) {
67
+ if (value === undefined) {
68
+ return now;
69
+ }
70
+ const parsed = new Date(value);
71
+ return Number.isNaN(parsed.getTime()) ? now : parsed;
72
+ }
46
73
  /**
47
74
  * Parses an `Expires` header, treating an unparseable value as expired.
48
75
  *
@@ -7,6 +7,7 @@
7
7
  * Transport-specific socket management belongs to the server/client adapter
8
8
  * layer. This module only handles HTTP-level semantics.
9
9
  */
10
+ import { assertSafeHeaderValue, escapeHeaderQuotedString, } from "../httpHeaders/security/index.js";
10
11
  /* -------------------------------------------------------------------------- */
11
12
  /* Constants */
12
13
  /* -------------------------------------------------------------------------- */
@@ -121,7 +122,9 @@ export function formatKeepAliveHeader(parameters = {}) {
121
122
  parts.push(`${key}=${quoteIfNeeded(value)}`);
122
123
  }
123
124
  }
124
- return parts.join(", ");
125
+ const header = parts.join(", ");
126
+ assertSafeHeaderValue(header);
127
+ return header;
125
128
  }
126
129
  /* -------------------------------------------------------------------------- */
127
130
  /* Connection Header */
@@ -348,10 +351,19 @@ function unquote(value) {
348
351
  }
349
352
  return trimmed;
350
353
  }
354
+ /**
355
+ * Emits a Keep-Alive parameter value.
356
+ *
357
+ * Quoting does not neutralise a CR or LF, so the control character used to
358
+ * survive into the field value. `escapeHeaderQuotedString` rejects it, the
359
+ * same helper every other quoted-parameter emitter in this package uses.
360
+ *
361
+ * @throws {TypeError} If the value contains a forbidden control character.
362
+ */
351
363
  function quoteIfNeeded(value) {
352
364
  if (/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(value)) {
353
365
  return value;
354
366
  }
355
- return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
367
+ return `"${escapeHeaderQuotedString(value)}"`;
356
368
  }
357
369
  //# sourceMappingURL=httpKeepAlive.core.js.map
@@ -11,6 +11,24 @@ export interface RequestLogger {
11
11
  export interface LoggingMiddlewareOptions {
12
12
  readonly logger?: RequestLogger;
13
13
  readonly includeHeaders?: boolean;
14
+ /**
15
+ * Extra header names whose value must be replaced with `[REDACTED]`, on top
16
+ * of the credential-bearing names `@zudojs/logger` already recognises
17
+ * (`authorization`, `proxy-authorization`, `cookie`, `set-cookie`, …).
18
+ */
19
+ readonly redactHeaders?: readonly string[];
14
20
  }
21
+ /**
22
+ * Creates the request/response logging middleware.
23
+ *
24
+ * With `includeHeaders` the header record is redacted before it reaches the
25
+ * logger, using the same matcher `@zudojs/logger` applies to log metadata.
26
+ * It used to be copied verbatim, so a bearer token and the whole session
27
+ * cookie landed in the log store on every request.
28
+ *
29
+ * @param options - Logger, header inclusion and extra redacted names.
30
+ * @returns A middleware that logs the start, completion and failure of a
31
+ * request.
32
+ */
15
33
  export declare function createLoggingMiddleware(options?: LoggingMiddlewareOptions): HttpMiddleware;
16
34
  //# sourceMappingURL=httpMiddleware.logging.d.ts.map
@@ -3,8 +3,24 @@
3
3
  *
4
4
  * @module httpMiddleware/builtin/logging
5
5
  */
6
+ import { createSecretMatcher, redactLogValue } from "@zudojs/logger";
6
7
  import { getRequestMethod, getRequestUrl, getRequestHeaders, getResponseStatus, } from "../helpers/index.js";
8
+ /**
9
+ * Creates the request/response logging middleware.
10
+ *
11
+ * With `includeHeaders` the header record is redacted before it reaches the
12
+ * logger, using the same matcher `@zudojs/logger` applies to log metadata.
13
+ * It used to be copied verbatim, so a bearer token and the whole session
14
+ * cookie landed in the log store on every request.
15
+ *
16
+ * @param options - Logger, header inclusion and extra redacted names.
17
+ * @returns A middleware that logs the start, completion and failure of a
18
+ * request.
19
+ */
7
20
  export function createLoggingMiddleware(options = {}) {
21
+ const isSecret = createSecretMatcher({
22
+ keys: options.redactHeaders ? [...options.redactHeaders] : undefined,
23
+ });
8
24
  return async (context, next) => {
9
25
  const startedAt = Date.now();
10
26
  const request = context.request;
@@ -14,7 +30,7 @@ export function createLoggingMiddleware(options = {}) {
14
30
  url: getRequestUrl(request),
15
31
  ...(options.includeHeaders
16
32
  ? {
17
- headers: getRequestHeaders(request),
33
+ headers: redactLogValue(getRequestHeaders(request), isSecret),
18
34
  }
19
35
  : {}),
20
36
  });
@@ -17,5 +17,16 @@ export interface SecurityMiddlewareOptions {
17
17
  */
18
18
  readonly useDefaults?: boolean;
19
19
  }
20
+ /**
21
+ * Creates the response-hardening middleware.
22
+ *
23
+ * Called with no options it emits the package's default security header set
24
+ * ({@link createDefaultSecurityHeaderOptions}); the options below layer over
25
+ * that set, and `useDefaults: false` drops it entirely.
26
+ *
27
+ * @param options - Explicit header values, layered over the baseline.
28
+ * @returns A middleware that adds the headers to the downstream response.
29
+ * @throws {TypeError} If a configured value contains a control character.
30
+ */
20
31
  export declare function createSecurityMiddleware(options?: SecurityMiddlewareOptions): HttpMiddleware;
21
32
  //# sourceMappingURL=httpMiddleware.security.d.ts.map
@@ -4,19 +4,34 @@
4
4
  * @module httpMiddleware/builtin/security
5
5
  */
6
6
  import { isValidHeaderFieldValue } from "../../../httpHeaders/security/index.js";
7
+ import { createSecurityHeaders } from "../../../httpSecurityHeaders/httpSecurityHeader.factory.js";
8
+ import { createDefaultSecurityHeaderOptions } from "../../../httpSecurityHeaders/httpSecurityHeader.recommended.js";
7
9
  import { withResponseHeaders } from "../helpers/index.js";
8
10
  /**
9
- * Headers applied when the caller does not override them.
11
+ * The headers applied when the caller does not override them.
10
12
  *
11
- * `createSecurityMiddleware()` previously defaulted every option to
12
- * `undefined` and therefore set no headers at all — its presence in a
13
- * codebase read as evidence that the control existed while shipping nothing.
13
+ * This is the package's declared safe baseline — the very set
14
+ * `createDefaultSecurityHeaderOptions` was written for and, until now, was
15
+ * never called for. The middleware used to re-derive its own three-entry
16
+ * list, so `pipeline.use(createSecurityMiddleware())` emitted no
17
+ * `Content-Security-Policy`, no `Strict-Transport-Security`, no
18
+ * `Permissions-Policy` and none of the cross-origin isolation headers while
19
+ * reading as evidence that the control was in force.
20
+ */
21
+ function defaultSecurityHeaders() {
22
+ return createSecurityHeaders(createDefaultSecurityHeaderOptions());
23
+ }
24
+ /**
25
+ * Creates the response-hardening middleware.
26
+ *
27
+ * Called with no options it emits the package's default security header set
28
+ * ({@link createDefaultSecurityHeaderOptions}); the options below layer over
29
+ * that set, and `useDefaults: false` drops it entirely.
30
+ *
31
+ * @param options - Explicit header values, layered over the baseline.
32
+ * @returns A middleware that adds the headers to the downstream response.
33
+ * @throws {TypeError} If a configured value contains a control character.
14
34
  */
15
- const DEFAULT_SECURITY_HEADERS = Object.freeze({
16
- "x-content-type-options": "nosniff",
17
- "x-frame-options": "DENY",
18
- "referrer-policy": "strict-origin-when-cross-origin",
19
- });
20
35
  export function createSecurityMiddleware(options = {}) {
21
36
  const configured = {
22
37
  "strict-transport-security": options.strictTransportSecurity,
@@ -26,9 +41,7 @@ export function createSecurityMiddleware(options = {}) {
26
41
  "content-security-policy": options.contentSecurityPolicy,
27
42
  "referrer-policy": options.referrerPolicy,
28
43
  };
29
- const resolved = {
30
- ...(options.useDefaults === false ? {} : DEFAULT_SECURITY_HEADERS),
31
- };
44
+ const resolved = options.useDefaults === false ? {} : defaultSecurityHeaders();
32
45
  for (const [name, value] of Object.entries(configured)) {
33
46
  if (value === undefined) {
34
47
  continue;
@@ -99,6 +99,19 @@ export declare function negotiateCharset(header: string | undefined | null, avai
99
99
  * @returns The selected alternative, or `undefined` if none is acceptable.
100
100
  */
101
101
  export declare function negotiate<T>(preferences: readonly NegotiationPreference[], available: readonly T[], matcher: (accepted: string, available: T) => boolean): T | undefined;
102
+ /**
103
+ * Returns the weight a preference list assigns to one value.
104
+ *
105
+ * The **most specific** match wins, and only then the highest weight, per
106
+ * RFC 9110 section 12.4.2. Ranking by weight first let `*;q=1` override an
107
+ * explicit `gzip;q=0`, so a coding the client had refused came back with
108
+ * full quality.
109
+ *
110
+ * @param preferences - The parsed preferences.
111
+ * @param value - The alternative to weigh.
112
+ * @param matcher - Matches a preference value against an alternative.
113
+ * @returns The quality in `[0, 1]`, or `0` when nothing matches.
114
+ */
102
115
  export declare function getPreferenceQuality<T>(preferences: readonly NegotiationPreference[], value: T, matcher: (accepted: string, available: T) => boolean): number;
103
116
  export declare function normalizeMediaType(value: string): string;
104
117
  export declare function splitMediaType(value: string): [string, string] | undefined;
@@ -214,7 +214,36 @@ export function negotiateEncoding(header, available) {
214
214
  if (preferences.length === 0) {
215
215
  return available[0];
216
216
  }
217
- return negotiate(preferences, available, matchesEncoding);
217
+ const selected = negotiate(preferences, available, matchesEncoding);
218
+ if (selected !== undefined) {
219
+ return selected;
220
+ }
221
+ /*
222
+ * RFC 9110 section 12.5.3: a representation with no content coding is
223
+ * acceptable unless the field explicitly excludes it with `identity;q=0`
224
+ * or a `*;q=0` that no identity entry overrides. Returning `undefined`
225
+ * here made `Accept-Encoding: zstd` look like "nothing is acceptable", so
226
+ * a caller answered 406 for a request it could have served uncompressed.
227
+ */
228
+ const identity = available.find((value) => isIdentityEncoding(value));
229
+ if (identity === undefined || isIdentityRejected(preferences)) {
230
+ return undefined;
231
+ }
232
+ return identity;
233
+ }
234
+ /**
235
+ * Reports whether an `Accept-Encoding` field rejects the identity coding.
236
+ *
237
+ * @param preferences - The parsed preferences.
238
+ * @returns `true` when identity must not be served.
239
+ */
240
+ function isIdentityRejected(preferences) {
241
+ const explicit = preferences.find((preference) => isIdentityEncoding(preference.value));
242
+ if (explicit) {
243
+ return !isAcceptableQuality(explicit.quality);
244
+ }
245
+ const wildcard = preferences.find((preference) => isWildcardEncoding(preference.value));
246
+ return wildcard !== undefined && !isAcceptableQuality(wildcard.quality);
218
247
  }
219
248
  export function getEncodingQuality(header, encoding) {
220
249
  const preferences = parseAcceptEncoding(header);
@@ -319,20 +348,43 @@ function isExcluded(candidate, selected, rejections, matcher) {
319
348
  return rejections.some((rejection) => rejection.specificity >= selected.specificity &&
320
349
  matcher(rejection.value, candidate));
321
350
  }
351
+ /**
352
+ * Returns the weight a preference list assigns to one value.
353
+ *
354
+ * The **most specific** match wins, and only then the highest weight, per
355
+ * RFC 9110 section 12.4.2. Ranking by weight first let `*;q=1` override an
356
+ * explicit `gzip;q=0`, so a coding the client had refused came back with
357
+ * full quality.
358
+ *
359
+ * @param preferences - The parsed preferences.
360
+ * @param value - The alternative to weigh.
361
+ * @param matcher - Matches a preference value against an alternative.
362
+ * @returns The quality in `[0, 1]`, or `0` when nothing matches.
363
+ */
322
364
  export function getPreferenceQuality(preferences, value, matcher) {
323
365
  let best;
324
366
  for (const preference of preferences) {
325
- if (matcher(preference.value, value)) {
326
- if (!best ||
327
- preference.quality > best.quality ||
328
- (preference.quality === best.quality &&
329
- preference.specificity > best.specificity)) {
330
- best = preference;
331
- }
367
+ if (!matcher(preference.value, value)) {
368
+ continue;
369
+ }
370
+ if (!best || isMoreRelevant(preference, best)) {
371
+ best = preference;
332
372
  }
333
373
  }
334
374
  return best?.quality ?? 0;
335
375
  }
376
+ /**
377
+ * Orders two matching preferences: specificity, then weight, then position.
378
+ */
379
+ function isMoreRelevant(candidate, best) {
380
+ if (candidate.specificity !== best.specificity) {
381
+ return candidate.specificity > best.specificity;
382
+ }
383
+ if (candidate.quality !== best.quality) {
384
+ return candidate.quality > best.quality;
385
+ }
386
+ return false;
387
+ }
336
388
  /* -------------------------------------------------------------------------- */
337
389
  /* Media Type Helpers */
338
390
  /* -------------------------------------------------------------------------- */
@@ -167,6 +167,15 @@ export declare function applyProxyHeaders(headers: readonly HTTPHeader[], additi
167
167
  * with `httpTrustProxy.getClientIp` rather than copying the raw header.
168
168
  */
169
169
  export declare function setForwardedHeaders(headers: readonly HTTPHeader[], target: ProxyTarget, client?: ProxyClientContext): HTTPHeader[];
170
+ /**
171
+ * Builds an RFC 7239 `Forwarded` field value.
172
+ *
173
+ * Every parameter is escaped as a `quoted-string` when it is not a bare
174
+ * token, and the finished value is checked the same way `setForwardedHeaders`
175
+ * checks the values it writes.
176
+ *
177
+ * @throws {TypeError} If any parameter contains a control character.
178
+ */
170
179
  export declare function createForwardedHeader(address: ForwardedAddress): string;
171
180
  export declare function parseForwardedHeader(value: string | undefined | null): ForwardedAddress[];
172
181
  /**