@blamejs/core 0.8.90 → 0.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/index.js CHANGED
@@ -145,6 +145,9 @@ var compliance = Object.assign({}, require("./lib/compliance"), {
145
145
  var dataAct = require("./lib/data-act");
146
146
  var problemDetails = require("./lib/problem-details");
147
147
  var cacheStatus = require("./lib/cache-status");
148
+ var cdnCacheControl = require("./lib/cdn-cache-control");
149
+ var clientHints = require("./lib/client-hints");
150
+ var structuredFields = require("./lib/structured-fields");
148
151
  var serverTiming = require("./lib/server-timing");
149
152
  var earlyHints = require("./lib/early-hints");
150
153
  var gateContract = require("./lib/gate-contract");
@@ -377,6 +380,9 @@ module.exports = {
377
380
  dataAct: dataAct,
378
381
  problemDetails: problemDetails,
379
382
  cacheStatus: cacheStatus,
383
+ cdnCacheControl: cdnCacheControl,
384
+ clientHints: clientHints,
385
+ structuredFields: structuredFields,
380
386
  serverTiming: serverTiming,
381
387
  earlyHints: earlyHints,
382
388
  gateContract: gateContract,
package/lib/ai-pref.js CHANGED
@@ -26,8 +26,9 @@
26
26
  * AIPREF (RFC draft) signal — operators publish a machine-readable preference about AI training / agent crawling / etc.
27
27
  */
28
28
 
29
- var audit = require("./audit");
30
- var requestHelpers = require("./request-helpers");
29
+ var audit = require("./audit");
30
+ var requestHelpers = require("./request-helpers");
31
+ var structuredFields = require("./structured-fields");
31
32
  var { defineClass } = require("./framework-error");
32
33
  var AiPrefError = defineClass("AiPrefError", { alwaysPermanent: true });
33
34
 
@@ -145,6 +146,11 @@ function parseHeader(value) {
145
146
  throw AiPrefError.factory("HEADER_TOO_LARGE",
146
147
  "aiPref.parseHeader: value exceeds 1024 chars");
147
148
  }
149
+ structuredFields.refuseControlBytes(value, {
150
+ ErrorClass: AiPrefError,
151
+ code: "BAD_HEADER",
152
+ label: "aiPref.parseHeader",
153
+ });
148
154
  var out = { train: null, infer: null, snippet: null, price: null };
149
155
  var pairs = value.split(",");
150
156
  for (var i = 0; i < pairs.length; i += 1) {
@@ -60,11 +60,12 @@
60
60
  * - auth.stepUp.grant.revoked (elevation grant revoked)
61
61
  */
62
62
 
63
- var lazyRequire = require("../lazy-require");
64
- var validateOpts = require("../validate-opts");
65
- var safeJson = require("../safe-json");
66
- var C = require("../constants");
67
- var { AuthError } = require("../framework-error");
63
+ var lazyRequire = require("../lazy-require");
64
+ var validateOpts = require("../validate-opts");
65
+ var safeJson = require("../safe-json");
66
+ var structuredFields = require("../structured-fields");
67
+ var C = require("../constants");
68
+ var { AuthError } = require("../framework-error");
68
69
 
69
70
  var acr = require("./acr-vocabulary");
70
71
  var authTime = require("./auth-time-tracker");
@@ -362,6 +363,14 @@ function _summarizePresented(presented) {
362
363
 
363
364
  function parseChallenge(headerValue) {
364
365
  if (typeof headerValue !== "string") return null;
366
+ // Refuse C0 / DEL on the RAW value BEFORE the slice + trim
367
+ // normalisation below. WWW-Authenticate is a token-or-quoted-string
368
+ // grammar per RFC 9110 §11.3; a leading `\nBearer ...` would slip
369
+ // past the slice() with a clean `Bearer` token if we trimmed first
370
+ // (same shape as the v0.8.90 mail-require-tls bug class).
371
+ // parseChallenge is a defensive request-shape reader, so the
372
+ // predicate variant returns null rather than throwing.
373
+ if (structuredFields.containsControlBytes(headerValue)) return null;
365
374
  // Tolerate "Bearer " prefix in any case; reject anything else.
366
375
  var idx = headerValue.toLowerCase().indexOf("bearer");
367
376
  if (idx === -1) return null;
@@ -0,0 +1,473 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.cdnCacheControl
4
+ * @nav HTTP
5
+ * @title RFC 9213 Targeted Cache-Control
6
+ * @order 315
7
+ *
8
+ * @intro
9
+ * RFC 9213 Targeted HTTP Cache-Control directives. Operators address
10
+ * specific layers in the caching chain by setting parallel headers
11
+ * that share the `Cache-Control` directive grammar but apply only
12
+ * to caches matching the target. The well-known shapes:
13
+ *
14
+ * Cache-Control: max-age=60 (user-agent cache)
15
+ * CDN-Cache-Control: max-age=3600 (every CDN class cache)
16
+ * Cloudflare-CDN-Cache-Control: max-age=86400 (CDN-specific override)
17
+ * Vercel-CDN-Cache-Control: max-age=86400 (CDN-specific override)
18
+ * Surrogate-Control: max-age=3600 (W3C Edge Architecture)
19
+ *
20
+ * The same response can carry multiple targeted variants — a CDN
21
+ * that recognizes its operator-specific header uses that one; CDNs
22
+ * without a match fall back to `CDN-Cache-Control`; user agents
23
+ * apply only the plain `Cache-Control`. The framework treats every
24
+ * variant as the same RFC 9111 §5.2.2 directive grammar.
25
+ *
26
+ * `build({...})` emits a directive string for any of the targeted
27
+ * headers; the operator chooses which header name to set. `parse()`
28
+ * round-trips: decode an inbound directive list into a normalized
29
+ * object with numeric maxAge / staleWhileRevalidate, boolean flags
30
+ * (public / private / noStore / noCache / mustRevalidate / immutable),
31
+ * and the raw `directives` map for unknown / extension keys.
32
+ *
33
+ * `TARGETED_HEADERS` lists the well-known header names the operator
34
+ * may set; an explicit allowlist instead of guessing prevents
35
+ * operators from emitting a malformed `CDN-Cache-Control-X-Custom`
36
+ * header that no cache will look at.
37
+ *
38
+ * @card
39
+ * RFC 9213 Targeted Cache-Control — build / parse `CDN-Cache-Control`,
40
+ * `Surrogate-Control`, and operator-specific (`Cloudflare-` / `Vercel-`
41
+ * / `Fastly-`) variants with the same directive grammar as plain
42
+ * `Cache-Control`.
43
+ */
44
+
45
+ var numericBounds = require("./numeric-bounds");
46
+ var structuredFields = require("./structured-fields");
47
+ var validateOpts = require("./validate-opts");
48
+ var { defineClass } = require("./framework-error");
49
+
50
+ var CdnCacheControlError = defineClass("CdnCacheControlError",
51
+ { alwaysPermanent: true });
52
+
53
+ // RFC 9213 §3 — well-known targeted header names. The list is curated
54
+ // (not regex-matched) because operators routinely typo `CDN-Cache-
55
+ // Control` into `Cdn-CacheControl` etc. and a typo silently emits a
56
+ // header no cache will read. Operators with a CDN not on this list
57
+ // pass the header name verbatim to `build()` via `headerName:` for
58
+ // audit visibility — the value still goes through directive
59
+ // validation.
60
+ var TARGETED_HEADERS = Object.freeze([
61
+ "Cache-Control",
62
+ "CDN-Cache-Control",
63
+ "Surrogate-Control",
64
+ "Cloudflare-CDN-Cache-Control",
65
+ "Vercel-CDN-Cache-Control",
66
+ "Fastly-CDN-Cache-Control",
67
+ "Akamai-CDN-Cache-Control",
68
+ "Netlify-CDN-Cache-Control",
69
+ ]);
70
+
71
+ var TARGETED_LC = {};
72
+ for (var _i = 0; _i < TARGETED_HEADERS.length; _i += 1) {
73
+ TARGETED_LC[TARGETED_HEADERS[_i].toLowerCase()] = TARGETED_HEADERS[_i];
74
+ }
75
+
76
+ // RFC 9111 §5.2 — directive grammar. Numeric directives carry a
77
+ // delta-seconds value; boolean directives appear as bare tokens.
78
+ var BOOLEAN_DIRECTIVES = Object.freeze([
79
+ "public", "private", "no-store", "no-cache",
80
+ "must-revalidate", "proxy-revalidate", "immutable",
81
+ "no-transform", "must-understand",
82
+ ]);
83
+ var NUMERIC_DIRECTIVES = Object.freeze([
84
+ "max-age", "s-maxage",
85
+ "stale-while-revalidate", "stale-if-error",
86
+ "min-fresh", "max-stale",
87
+ ]);
88
+
89
+ // camelCase → kebab-case so `build({ maxAge: 60 })` emits `max-age=60`.
90
+ var KEBAB = {
91
+ maxAge: "max-age",
92
+ sMaxAge: "s-maxage",
93
+ staleWhileRevalidate: "stale-while-revalidate",
94
+ staleIfError: "stale-if-error",
95
+ minFresh: "min-fresh",
96
+ maxStale: "max-stale",
97
+ noStore: "no-store",
98
+ noCache: "no-cache",
99
+ mustRevalidate: "must-revalidate",
100
+ proxyRevalidate: "proxy-revalidate",
101
+ mustUnderstand: "must-understand",
102
+ noTransform: "no-transform",
103
+ };
104
+
105
+ // RFC 7234 §5.2 token: `token = 1*tchar` where tchar excludes delimiter
106
+ // chars. Directive keys are tchar-only and conventionally ASCII-lower.
107
+ var DIRECTIVE_KEY_RE = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/; // allow:duplicate-regex — RFC 7234 §5.2 tchar (=RFC 8941 token shape)
108
+
109
+ // Internal validation tier: throw at config-time. build() is the
110
+ // operator-facing entry, parse() is the request-shape reader (returns
111
+ // defensive defaults on garbage rather than throwing).
112
+
113
+ function _isNonNegInt(v) {
114
+ return typeof v === "number" && isFinite(v) && v >= 0 && Math.floor(v) === v;
115
+ }
116
+
117
+ /**
118
+ * @primitive b.cdnCacheControl.build
119
+ * @signature b.cdnCacheControl.build(opts)
120
+ * @since 0.8.91
121
+ * @status stable
122
+ * @related b.cdnCacheControl.parse, b.cdnCacheControl.isTargetedHeader
123
+ *
124
+ * Build a Cache-Control-style directive list string for any RFC 9213
125
+ * targeted header. `opts` accepts the standard RFC 9111 §5.2.2
126
+ * directives in camelCase (`maxAge`, `sMaxAge`, `staleWhileRevalidate`,
127
+ * `staleIfError`, `mustRevalidate`, etc.) — kebab-case keys
128
+ * (`max-age`, `s-maxage`, ...) also pass through unchanged for
129
+ * operators porting from existing header-building code.
130
+ *
131
+ * Numeric directives accept non-negative finite integers; the
132
+ * primitive refuses negative / non-integer / `Infinity` / `NaN`
133
+ * inputs (the directive grammar requires delta-seconds). Boolean
134
+ * directives only emit when explicitly `true`; `false` / `undefined`
135
+ * omit the token.
136
+ *
137
+ * Returns the directive list string ready to be assigned to any
138
+ * header in `TARGETED_HEADERS`. Caller is responsible for choosing
139
+ * which header name to set.
140
+ *
141
+ * @opts
142
+ * maxAge: number, // max-age=N (user-agent + shared)
143
+ * sMaxAge: number, // s-maxage=N (shared caches only)
144
+ * staleWhileRevalidate: number, // RFC 5861 §3
145
+ * staleIfError: number, // RFC 5861 §4
146
+ * minFresh: number, // request directive
147
+ * maxStale: number, // request directive
148
+ * public: boolean,
149
+ * private: boolean,
150
+ * noStore: boolean,
151
+ * noCache: boolean,
152
+ * mustRevalidate: boolean,
153
+ * proxyRevalidate: boolean,
154
+ * mustUnderstand: boolean, // RFC 8246
155
+ * noTransform: boolean,
156
+ * immutable: boolean, // RFC 8246
157
+ * extensions: object, // raw key→value/true map for non-standard directives
158
+ *
159
+ * @example
160
+ * res.setHeader("CDN-Cache-Control", b.cdnCacheControl.build({
161
+ * public: true,
162
+ * sMaxAge: 3600,
163
+ * staleWhileRevalidate: 60,
164
+ * staleIfError: 86400,
165
+ * }));
166
+ * // → "public, s-maxage=3600, stale-while-revalidate=60, stale-if-error=86400"
167
+ *
168
+ * res.setHeader("Cache-Control", b.cdnCacheControl.build({
169
+ * private: true, maxAge: 0, noStore: true,
170
+ * }));
171
+ * // → "private, max-age=0, no-store"
172
+ */
173
+ function build(opts) {
174
+ if (!opts || typeof opts !== "object" || Array.isArray(opts)) {
175
+ throw new CdnCacheControlError("cdn-cache-control/bad-opts",
176
+ "build: opts must be a non-null object", true);
177
+ }
178
+ // Conflict check: public + private is incoherent per RFC 9111 §5.2.2;
179
+ // operators almost always meant one or the other.
180
+ if (opts.public === true && opts.private === true) {
181
+ throw new CdnCacheControlError("cdn-cache-control/conflicting-visibility",
182
+ "build: cannot set both 'public' and 'private' (RFC 9111 §5.2.2.5/§5.2.2.6)");
183
+ }
184
+
185
+ var seen = {};
186
+ var parts = [];
187
+
188
+ // Visibility booleans first (operator-facing readability)
189
+ if (opts.public === true) { parts.push("public"); seen.public = true; }
190
+ if (opts.private === true) { parts.push("private"); seen.private = true; }
191
+
192
+ // Numeric directives, both camelCase and raw kebab-case keys
193
+ var numericKeys = [
194
+ "maxAge", "max-age",
195
+ "sMaxAge", "s-maxage",
196
+ "staleWhileRevalidate", "stale-while-revalidate",
197
+ "staleIfError", "stale-if-error",
198
+ "minFresh", "min-fresh",
199
+ "maxStale", "max-stale",
200
+ ];
201
+ for (var i = 0; i < numericKeys.length; i += 1) {
202
+ var k = numericKeys[i];
203
+ if (opts[k] === undefined || opts[k] === null) continue;
204
+ if (!_isNonNegInt(opts[k])) {
205
+ throw new CdnCacheControlError("cdn-cache-control/bad-numeric",
206
+ "build: " + k + " must be a non-negative finite integer (got " +
207
+ typeof opts[k] + " " + String(opts[k]) + ")");
208
+ }
209
+ var kebab = KEBAB[k] || k;
210
+ if (!seen[kebab]) {
211
+ parts.push(kebab + "=" + opts[k]);
212
+ seen[kebab] = true;
213
+ }
214
+ }
215
+
216
+ // Remaining boolean directives
217
+ var boolKeys = [
218
+ "noStore", "no-store",
219
+ "noCache", "no-cache",
220
+ "mustRevalidate", "must-revalidate",
221
+ "proxyRevalidate", "proxy-revalidate",
222
+ "mustUnderstand", "must-understand",
223
+ "noTransform", "no-transform",
224
+ "immutable",
225
+ ];
226
+ for (var j = 0; j < boolKeys.length; j += 1) {
227
+ var bk = boolKeys[j];
228
+ if (opts[bk] === true) {
229
+ var bkebab = KEBAB[bk] || bk;
230
+ if (!seen[bkebab]) {
231
+ parts.push(bkebab);
232
+ seen[bkebab] = true;
233
+ }
234
+ } else if (opts[bk] !== undefined && opts[bk] !== false && opts[bk] !== null) {
235
+ throw new CdnCacheControlError("cdn-cache-control/bad-boolean",
236
+ "build: " + bk + " must be a boolean (got " + typeof opts[bk] + ")");
237
+ }
238
+ }
239
+
240
+ // Operator-supplied extension directives (must be RFC 7234 tchar
241
+ // shape; values are either `true` for bare token or string/number
242
+ // for token=value). Refuses delimiter / control / whitespace in
243
+ // keys so the assembled list can't carry an injection.
244
+ if (opts.extensions !== undefined && opts.extensions !== null) {
245
+ validateOpts.optionalPlainObject(opts.extensions, "opts.extensions",
246
+ CdnCacheControlError, "cdn-cache-control/bad-extensions",
247
+ "non-null object of <directive-key>: true | token-string | non-negative integer");
248
+ var ekeys = Object.keys(opts.extensions);
249
+ // Bound directive-key + value length BEFORE regex test so a
250
+ // multi-MB attacker-supplied string can't burn CPU on the tchar
251
+ // regex. RFC 7234 §5.2 token directives are tiny in practice
252
+ // (max-age = 7 chars, stale-while-revalidate = 22); 64 is the
253
+ // operator-headroom ceiling.
254
+ var DIRECTIVE_MAX = 64; // allow:raw-byte-literal — directive key/value length cap
255
+ for (var e = 0; e < ekeys.length; e += 1) {
256
+ var ek = ekeys[e];
257
+ if (ek.length === 0 || ek.length > DIRECTIVE_MAX || !DIRECTIVE_KEY_RE.test(ek)) {
258
+ throw new CdnCacheControlError("cdn-cache-control/bad-extension-key",
259
+ "build: extensions['" + ek + "'] — key must match RFC 7234 §5.2 token grammar " +
260
+ "(<= " + DIRECTIVE_MAX + " chars)");
261
+ }
262
+ if (seen[ek]) continue;
263
+ var ev = opts.extensions[ek];
264
+ if (ev === true) {
265
+ parts.push(ek);
266
+ seen[ek] = true;
267
+ } else if (typeof ev === "string") {
268
+ // Value must be either RFC 7234 token OR sf-string quoted.
269
+ // We only emit unquoted-token form; operators with delimiter
270
+ // chars must pre-quote and supply via raw header set.
271
+ if (ev.length === 0 || ev.length > DIRECTIVE_MAX || !DIRECTIVE_KEY_RE.test(ev)) {
272
+ throw new CdnCacheControlError("cdn-cache-control/bad-extension-value",
273
+ "build: extensions['" + ek + "'] string value must match RFC 7234 §5.2 token grammar " +
274
+ "(<= " + DIRECTIVE_MAX + " chars); " +
275
+ "for quoted-string values set the header directly");
276
+ }
277
+ parts.push(ek + "=" + ev);
278
+ seen[ek] = true;
279
+ } else if (_isNonNegInt(ev)) {
280
+ parts.push(ek + "=" + ev);
281
+ seen[ek] = true;
282
+ } else {
283
+ throw new CdnCacheControlError("cdn-cache-control/bad-extension-value",
284
+ "build: extensions['" + ek + "'] must be true | token-string | non-negative integer");
285
+ }
286
+ }
287
+ }
288
+
289
+ if (parts.length === 0) {
290
+ throw new CdnCacheControlError("cdn-cache-control/empty",
291
+ "build: no directives supplied — refuse to emit an empty Cache-Control list");
292
+ }
293
+ return parts.join(", ");
294
+ }
295
+
296
+ /**
297
+ * @primitive b.cdnCacheControl.parse
298
+ * @signature b.cdnCacheControl.parse(headerValue)
299
+ * @since 0.8.91
300
+ * @status stable
301
+ * @related b.cdnCacheControl.build
302
+ *
303
+ * Parse a Cache-Control-style directive list (from any RFC 9213
304
+ * targeted header) into a normalized object. Returns `null` for
305
+ * absent / empty / non-string input — operator code branches on
306
+ * `null` vs the populated shape.
307
+ *
308
+ * Numeric directives are surfaced as camelCase number fields
309
+ * (`maxAge`, `sMaxAge`, `staleWhileRevalidate`, `staleIfError`,
310
+ * `minFresh`, `maxStale`); boolean directives as camelCase boolean
311
+ * fields. Unknown directives land in `directives` (a `name → value`
312
+ * map where boolean directives map to `true` and value-bearing
313
+ * directives map to the raw string).
314
+ *
315
+ * Defensive parser: tolerates trailing semicolons, repeated whitespace,
316
+ * and unquoted-quoted-string values; refuses control characters in
317
+ * the header value (CR/LF/NUL/DEL header-injection shape) by throwing
318
+ * `cdn-cache-control/bad-header-value`. ASCII HT remains permitted
319
+ * (structural folding whitespace).
320
+ *
321
+ * @example
322
+ * b.cdnCacheControl.parse("public, s-maxage=3600, stale-while-revalidate=60");
323
+ * // → { public: true, sMaxAge: 3600, staleWhileRevalidate: 60, directives: {} }
324
+ *
325
+ * b.cdnCacheControl.parse("private, no-store, x-foo=bar");
326
+ * // → { private: true, noStore: true, directives: { "x-foo": "bar" } }
327
+ */
328
+ function parse(headerValue) {
329
+ if (typeof headerValue !== "string") return null;
330
+ structuredFields.refuseControlBytes(headerValue, {
331
+ ErrorClass: CdnCacheControlError,
332
+ code: "cdn-cache-control/bad-header-value",
333
+ label: "parse",
334
+ });
335
+ var trimmed = headerValue.trim();
336
+ if (trimmed.length === 0) return null;
337
+
338
+ var out = { directives: {} };
339
+ var pieces = structuredFields.splitTopLevel(trimmed, ",");
340
+ for (var p = 0; p < pieces.length; p += 1) {
341
+ var raw = pieces[p].trim();
342
+ if (raw.length === 0) continue;
343
+ var eq = raw.indexOf("=");
344
+ var key, val, bare;
345
+ if (eq === -1) {
346
+ key = raw.toLowerCase();
347
+ val = "";
348
+ bare = true;
349
+ }
350
+ else {
351
+ key = raw.slice(0, eq).trim().toLowerCase();
352
+ val = raw.slice(eq + 1).trim();
353
+ bare = false;
354
+ // Unquote sf-string per RFC 8941 §3.3.3 (defensive — operators
355
+ // routinely emit `s-maxage="3600"` even though the directive is
356
+ // numeric, and the spec says quoted-string is also valid). The
357
+ // RFC 9111 `private="Authorization, Cookie"` qualified form
358
+ // arrives here too — the top-level comma splitter already
359
+ // preserved the inner comma; this just strips the surrounding
360
+ // quotes so operators read the field-name list directly.
361
+ var _unq = structuredFields.unquoteSfString(val);
362
+ if (_unq !== null) val = _unq;
363
+ }
364
+ if (key.length === 0) continue;
365
+
366
+ // Numeric directive → coerce to non-negative integer or skip.
367
+ // RFC 9111 §5.2.1.2: bare `max-stale` (no argument) means "accept
368
+ // a stale response of any age" — surface as Infinity rather than
369
+ // coercing Number(true) === 1 (which would materially change
370
+ // request semantics and reject otherwise-acceptable cached
371
+ // responses). Other numeric directives in bare form aren't
372
+ // RFC-defined; treat their bare form as absent.
373
+ if (NUMERIC_DIRECTIVES.indexOf(key) !== -1) {
374
+ if (bare) {
375
+ if (key === "max-stale") {
376
+ out[_camel(key)] = Infinity;
377
+ }
378
+ continue;
379
+ }
380
+ var n = Number(val);
381
+ if (isFinite(n) && n >= 0) {
382
+ out[_camel(key)] = Math.floor(n);
383
+ }
384
+ continue;
385
+ }
386
+ // Boolean directive → presence == enabled. RFC 9111 §5.2.2.6
387
+ // (`private`) and §5.2.2.4 (`no-cache`) carry an OPTIONAL field-
388
+ // name list as a qualified-form argument: `private="Authorization"`
389
+ // means "only Authorization is the private bit". The directive is
390
+ // STILL enabled — the argument narrows the scope, not the verdict.
391
+ // A previous version coerced (val === "" || val === "true") which
392
+ // inverted the meaning of a qualified directive.
393
+ if (BOOLEAN_DIRECTIVES.indexOf(key) !== -1) {
394
+ out[_camel(key)] = true;
395
+ // Qualified form: surface the field-name list on `fields` so
396
+ // operators reading the parse output can apply the narrower
397
+ // scope without re-parsing.
398
+ if (!bare && val.length > 0) {
399
+ if (!out.fields) out.fields = {};
400
+ out.fields[_camel(key)] = _splitFieldNameList(val);
401
+ }
402
+ continue;
403
+ }
404
+ // Unknown / extension directive → land in `directives` map.
405
+ // Bare form → true; valued form → the (possibly unquoted) string.
406
+ out.directives[key] = bare ? true : val;
407
+ }
408
+ return out;
409
+ }
410
+
411
+ // Field-name list (RFC 9111 §5.2.2.6 `private="A, B"`). Inner
412
+ // comma-separated header-field-name list; values are tokens per
413
+ // RFC 9110 §5.1. Lowercased + trimmed per piece.
414
+ function _splitFieldNameList(s) {
415
+ var parts = s.split(",");
416
+ var out = [];
417
+ for (var i = 0; i < parts.length; i += 1) {
418
+ var t = parts[i].trim();
419
+ if (t.length > 0) out.push(t.toLowerCase());
420
+ }
421
+ return out;
422
+ }
423
+
424
+ // Reverse of KEBAB — directive-name → camelCase the parser surfaces.
425
+ // Maintained explicitly because `s-maxage` has no internal hyphen
426
+ // (the spec spells it `s-maxage`, not `s-max-age`) so a generic
427
+ // kebab → camel transform produces `sMaxage`. We want `sMaxAge` so
428
+ // the parse output mirrors the build input.
429
+ var CAMEL_OVERRIDE = {
430
+ "s-maxage": "sMaxAge",
431
+ };
432
+ function _camel(kebab) {
433
+ if (CAMEL_OVERRIDE[kebab]) return CAMEL_OVERRIDE[kebab];
434
+ return kebab.replace(/-([a-z])/g, function (_, ch) { return ch.toUpperCase(); });
435
+ }
436
+
437
+ /**
438
+ * @primitive b.cdnCacheControl.isTargetedHeader
439
+ * @signature b.cdnCacheControl.isTargetedHeader(headerName)
440
+ * @since 0.8.91
441
+ * @status stable
442
+ * @related b.cdnCacheControl.parse, b.cdnCacheControl.build
443
+ *
444
+ * Returns `true` when `headerName` matches one of the well-known RFC
445
+ * 9213 targeted header names (case-insensitive). Operators auditing
446
+ * an inbound response's cache headers walk the response headers and
447
+ * call this to identify which directive lists were intended for which
448
+ * cache class.
449
+ *
450
+ * @example
451
+ * b.cdnCacheControl.isTargetedHeader("CDN-Cache-Control"); // → true
452
+ * b.cdnCacheControl.isTargetedHeader("cloudflare-cdn-cache-control"); // → true
453
+ * b.cdnCacheControl.isTargetedHeader("Cache"); // → false
454
+ */
455
+ function isTargetedHeader(headerName) {
456
+ if (typeof headerName !== "string" || headerName.length === 0) return false;
457
+ return Object.prototype.hasOwnProperty.call(TARGETED_LC, headerName.toLowerCase());
458
+ }
459
+
460
+ module.exports = {
461
+ build: build,
462
+ parse: parse,
463
+ isTargetedHeader: isTargetedHeader,
464
+ TARGETED_HEADERS: TARGETED_HEADERS,
465
+ BOOLEAN_DIRECTIVES: BOOLEAN_DIRECTIVES,
466
+ NUMERIC_DIRECTIVES: NUMERIC_DIRECTIVES,
467
+ CdnCacheControlError: CdnCacheControlError,
468
+ };
469
+
470
+ // Reserved for future field validation paths; kept in canonical
471
+ // require ordering.
472
+ void numericBounds;
473
+ void validateOpts;