@traffic-twin/sdk 0.0.0-stage → 0.1.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 (133) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +171 -2
  3. package/dist/cjs/agent.d.ts +143 -0
  4. package/dist/cjs/agent.js +927 -0
  5. package/dist/cjs/capture/body.d.ts +11 -0
  6. package/dist/cjs/capture/body.js +73 -0
  7. package/dist/cjs/capture/common.d.ts +75 -0
  8. package/dist/cjs/capture/common.js +180 -0
  9. package/dist/cjs/capture/context.d.ts +28 -0
  10. package/dist/cjs/capture/context.js +92 -0
  11. package/dist/cjs/capture/inbound.d.ts +12 -0
  12. package/dist/cjs/capture/inbound.js +195 -0
  13. package/dist/cjs/capture/outbound-http.d.ts +4 -0
  14. package/dist/cjs/capture/outbound-http.js +200 -0
  15. package/dist/cjs/capture/outbound-undici.d.ts +2 -0
  16. package/dist/cjs/capture/outbound-undici.js +242 -0
  17. package/dist/cjs/capture/wire.d.ts +27 -0
  18. package/dist/cjs/capture/wire.js +68 -0
  19. package/dist/cjs/export/event.d.ts +46 -0
  20. package/dist/cjs/export/event.js +91 -0
  21. package/dist/cjs/export/exporter.d.ts +50 -0
  22. package/dist/cjs/export/exporter.js +192 -0
  23. package/dist/cjs/export/pipeline.d.ts +20 -0
  24. package/dist/cjs/export/pipeline.js +40 -0
  25. package/dist/cjs/fleet/asn1.d.ts +44 -0
  26. package/dist/cjs/fleet/asn1.js +150 -0
  27. package/dist/cjs/fleet/client.d.ts +84 -0
  28. package/dist/cjs/fleet/client.js +216 -0
  29. package/dist/cjs/fleet/http.d.ts +17 -0
  30. package/dist/cjs/fleet/http.js +80 -0
  31. package/dist/cjs/fleet/store.d.ts +18 -0
  32. package/dist/cjs/fleet/store.js +115 -0
  33. package/dist/cjs/index.d.ts +14 -0
  34. package/dist/cjs/index.js +69 -0
  35. package/dist/cjs/log.d.ts +4 -0
  36. package/dist/cjs/log.js +30 -0
  37. package/dist/cjs/package.json +1 -0
  38. package/dist/cjs/register.d.ts +1 -0
  39. package/dist/cjs/register.js +18 -0
  40. package/dist/cjs/sanitizer/bodies.d.ts +55 -0
  41. package/dist/cjs/sanitizer/bodies.js +710 -0
  42. package/dist/cjs/sanitizer/formats.d.ts +26 -0
  43. package/dist/cjs/sanitizer/formats.js +110 -0
  44. package/dist/cjs/sanitizer/graphql.d.ts +23 -0
  45. package/dist/cjs/sanitizer/graphql.js +478 -0
  46. package/dist/cjs/sanitizer/hostpat.d.ts +15 -0
  47. package/dist/cjs/sanitizer/hostpat.js +129 -0
  48. package/dist/cjs/sanitizer/json.d.ts +60 -0
  49. package/dist/cjs/sanitizer/json.js +460 -0
  50. package/dist/cjs/sanitizer/policy.d.ts +224 -0
  51. package/dist/cjs/sanitizer/policy.js +820 -0
  52. package/dist/cjs/sanitizer/sanitizer.d.ts +149 -0
  53. package/dist/cjs/sanitizer/sanitizer.js +1473 -0
  54. package/dist/cjs/sanitizer/utf8.d.ts +22 -0
  55. package/dist/cjs/sanitizer/utf8.js +180 -0
  56. package/dist/cjs/sanitizer/vectors.d.ts +22 -0
  57. package/dist/cjs/sanitizer/vectors.js +130 -0
  58. package/dist/cjs/version.d.ts +2 -0
  59. package/dist/cjs/version.js +5 -0
  60. package/dist/cjs/worker/dir.d.ts +1 -0
  61. package/dist/cjs/worker/dir.js +8 -0
  62. package/dist/cjs/worker/pool.d.ts +47 -0
  63. package/dist/cjs/worker/pool.js +179 -0
  64. package/dist/cjs/worker/protocol.d.ts +37 -0
  65. package/dist/cjs/worker/protocol.js +3 -0
  66. package/dist/cjs/worker/sanitizer-worker.d.ts +1 -0
  67. package/dist/cjs/worker/sanitizer-worker.js +53 -0
  68. package/dist/esm/agent.d.ts +143 -0
  69. package/dist/esm/agent.js +921 -0
  70. package/dist/esm/capture/body.d.ts +11 -0
  71. package/dist/esm/capture/body.js +69 -0
  72. package/dist/esm/capture/common.d.ts +75 -0
  73. package/dist/esm/capture/common.js +166 -0
  74. package/dist/esm/capture/context.d.ts +28 -0
  75. package/dist/esm/capture/context.js +84 -0
  76. package/dist/esm/capture/inbound.d.ts +12 -0
  77. package/dist/esm/capture/inbound.js +187 -0
  78. package/dist/esm/capture/outbound-http.d.ts +4 -0
  79. package/dist/esm/capture/outbound-http.js +193 -0
  80. package/dist/esm/capture/outbound-undici.d.ts +2 -0
  81. package/dist/esm/capture/outbound-undici.js +236 -0
  82. package/dist/esm/capture/wire.d.ts +27 -0
  83. package/dist/esm/capture/wire.js +65 -0
  84. package/dist/esm/export/event.d.ts +46 -0
  85. package/dist/esm/export/event.js +85 -0
  86. package/dist/esm/export/exporter.d.ts +50 -0
  87. package/dist/esm/export/exporter.js +188 -0
  88. package/dist/esm/export/pipeline.d.ts +20 -0
  89. package/dist/esm/export/pipeline.js +37 -0
  90. package/dist/esm/fleet/asn1.d.ts +44 -0
  91. package/dist/esm/fleet/asn1.js +128 -0
  92. package/dist/esm/fleet/client.d.ts +84 -0
  93. package/dist/esm/fleet/client.js +204 -0
  94. package/dist/esm/fleet/http.d.ts +17 -0
  95. package/dist/esm/fleet/http.js +73 -0
  96. package/dist/esm/fleet/store.d.ts +18 -0
  97. package/dist/esm/fleet/store.js +110 -0
  98. package/dist/esm/index.d.ts +14 -0
  99. package/dist/esm/index.js +48 -0
  100. package/dist/esm/log.d.ts +4 -0
  101. package/dist/esm/log.js +25 -0
  102. package/dist/esm/package.json +1 -0
  103. package/dist/esm/register.d.ts +1 -0
  104. package/dist/esm/register.js +16 -0
  105. package/dist/esm/sanitizer/bodies.d.ts +55 -0
  106. package/dist/esm/sanitizer/bodies.js +694 -0
  107. package/dist/esm/sanitizer/formats.d.ts +26 -0
  108. package/dist/esm/sanitizer/formats.js +101 -0
  109. package/dist/esm/sanitizer/graphql.d.ts +23 -0
  110. package/dist/esm/sanitizer/graphql.js +472 -0
  111. package/dist/esm/sanitizer/hostpat.d.ts +15 -0
  112. package/dist/esm/sanitizer/hostpat.js +122 -0
  113. package/dist/esm/sanitizer/json.d.ts +60 -0
  114. package/dist/esm/sanitizer/json.js +447 -0
  115. package/dist/esm/sanitizer/policy.d.ts +224 -0
  116. package/dist/esm/sanitizer/policy.js +800 -0
  117. package/dist/esm/sanitizer/sanitizer.d.ts +149 -0
  118. package/dist/esm/sanitizer/sanitizer.js +1459 -0
  119. package/dist/esm/sanitizer/utf8.d.ts +22 -0
  120. package/dist/esm/sanitizer/utf8.js +166 -0
  121. package/dist/esm/sanitizer/vectors.d.ts +22 -0
  122. package/dist/esm/sanitizer/vectors.js +125 -0
  123. package/dist/esm/version.d.ts +2 -0
  124. package/dist/esm/version.js +2 -0
  125. package/dist/esm/worker/dir.d.ts +1 -0
  126. package/dist/esm/worker/dir.js +5 -0
  127. package/dist/esm/worker/pool.d.ts +47 -0
  128. package/dist/esm/worker/pool.js +175 -0
  129. package/dist/esm/worker/protocol.d.ts +37 -0
  130. package/dist/esm/worker/protocol.js +2 -0
  131. package/dist/esm/worker/sanitizer-worker.d.ts +1 -0
  132. package/dist/esm/worker/sanitizer-worker.js +51 -0
  133. package/package.json +75 -4
@@ -0,0 +1,1473 @@
1
+ "use strict";
2
+ // The sanitizer, docs/spec/sanitizer.md v1.1. Input: one normalized exchange (section 1),
3
+ // a verified compiled policy, the local HMAC key and the clock. Output: the canonical
4
+ // object of section 2, or not_sampled.
5
+ //
6
+ // Guiding rule: in doubt, drop. Any exception escaping from here must be treated by the
7
+ // caller as an internal error (no event, `sanitizer_errors` + 1).
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.Sanitizer = exports.FRAMEWORKS = exports.MAX_EVENT_BYTES = void 0;
10
+ exports.normalizeHeaders = normalizeHeaders;
11
+ exports.contentType = contentType;
12
+ exports.decomposePath = decomposePath;
13
+ exports.normalizeUrlBase = normalizeUrlBase;
14
+ exports.normalizeFrameworkRoute = normalizeFrameworkRoute;
15
+ exports.bucketTime = bucketTime;
16
+ exports.bucketNum = bucketNum;
17
+ exports.decodeBody = decodeBody;
18
+ exports.composeCanonical = composeCanonical;
19
+ exports.parseUrlencoded = parseUrlencoded;
20
+ const node_crypto_1 = require("node:crypto");
21
+ const node_zlib_1 = require("node:zlib");
22
+ const json_js_1 = require("./json.js");
23
+ const formats_js_1 = require("./formats.js");
24
+ const bodies_js_1 = require("./bodies.js");
25
+ const graphql_js_1 = require("./graphql.js");
26
+ const hostpat_js_1 = require("./hostpat.js");
27
+ const policy_js_1 = require("./policy.js");
28
+ const utf8_js_1 = require("./utf8.js");
29
+ exports.MAX_EVENT_BYTES = 524288;
30
+ const D_SUBJECT_YES = Object.freeze({ sampled: true, by: 'subject' });
31
+ const D_SUBJECT_NO = Object.freeze({ sampled: false, by: 'subject' });
32
+ const D_RANDOM_YES = Object.freeze({ sampled: true, by: 'random' });
33
+ const D_RANDOM_NO = Object.freeze({ sampled: false, by: 'random' });
34
+ const ZERO = Buffer.from([0]);
35
+ const SEP_COMMA = Buffer.from(', ');
36
+ const SEP_SEMI = Buffer.from('; ');
37
+ const MAX_SEGMENTS = 32;
38
+ const MAX_NAMES = 64;
39
+ const MAX_DISC = 4096;
40
+ const SUBJECT_MAX = 8192;
41
+ // ---------------------------------------------------------------------------------
42
+ // Header normalization (8.0)
43
+ function normalizeHeaders(list) {
44
+ const order = [];
45
+ const map = new Map();
46
+ for (const [name, value] of list ?? []) {
47
+ if (name.charCodeAt(0) === 0x3a)
48
+ continue;
49
+ const lower = (0, utf8_js_1.asciiLower)(name);
50
+ const out = (0, formats_js_1.headerOutName)(lower);
51
+ let e = map.get(out);
52
+ if (!e) {
53
+ e = { out, values: [], origs: new Set() };
54
+ map.set(out, e);
55
+ order.push(out);
56
+ }
57
+ e.values.push((0, utf8_js_1.trimOws)(value));
58
+ e.origs.add(lower);
59
+ }
60
+ return { order, map };
61
+ }
62
+ function joined(e) {
63
+ if (e.values.length === 1)
64
+ return e.values[0];
65
+ const parts = [];
66
+ const sep = e.out === 'cookie' ? SEP_SEMI : SEP_COMMA;
67
+ e.values.forEach((v, i) => {
68
+ if (i > 0)
69
+ parts.push(sep);
70
+ parts.push(v);
71
+ });
72
+ return Buffer.concat(parts);
73
+ }
74
+ /** Joined value of one exact lowercase header name (subject, baggage). */
75
+ function headerValue(list, lower) {
76
+ const vals = [];
77
+ for (const [n, v] of list ?? []) {
78
+ if (n.charCodeAt(0) === 0x3a)
79
+ continue;
80
+ if ((0, utf8_js_1.asciiLower)(n) === lower)
81
+ vals.push((0, utf8_js_1.trimOws)(v));
82
+ }
83
+ if (vals.length === 0)
84
+ return null;
85
+ if (vals.length === 1)
86
+ return vals[0];
87
+ const sep = lower === 'cookie' ? SEP_SEMI : SEP_COMMA;
88
+ const parts = [];
89
+ vals.forEach((v, i) => {
90
+ if (i > 0)
91
+ parts.push(sep);
92
+ parts.push(v);
93
+ });
94
+ return Buffer.concat(parts);
95
+ }
96
+ const CT_RE = /^[a-z0-9.+-]{1,64}\/[a-z0-9.+-]{1,64}$/;
97
+ /** 8.1.5: `ct` from the normalized content-type value; undefined without the header. */
98
+ function contentType(h) {
99
+ const e = h.map.get('content-type');
100
+ if (!e)
101
+ return undefined;
102
+ let v = joined(e);
103
+ const semi = v.indexOf(0x3b);
104
+ if (semi >= 0)
105
+ v = v.subarray(0, semi);
106
+ const s = (0, utf8_js_1.asciiLower)((0, utf8_js_1.trimOws)(v).toString('latin1'));
107
+ return CT_RE.test(s) ? s : 'invalid';
108
+ }
109
+ function contentEncoding(h) {
110
+ const e = h.map.get('content-encoding');
111
+ if (!e)
112
+ return undefined;
113
+ return (0, utf8_js_1.asciiLower)((0, utf8_js_1.trimOws)(joined(e)).toString('latin1'));
114
+ }
115
+ function decomposePath(raw) {
116
+ const segs = [];
117
+ let truncated = false;
118
+ for (const s of (0, utf8_js_1.splitBytes)(raw, 0x2f)) {
119
+ if (s.length === 0)
120
+ continue;
121
+ if (segs.length === MAX_SEGMENTS) {
122
+ truncated = true;
123
+ break;
124
+ }
125
+ segs.push({ dec: (0, utf8_js_1.decodeUtf8)((0, utf8_js_1.pctDecode)(s, false)) });
126
+ }
127
+ return { segs, truncated };
128
+ }
129
+ /** 3.1 normalization of `url_base`. */
130
+ function normalizeUrlBase(u) {
131
+ const bad = { invalid: true, origin: '', hostport: '', host: '', port: null, ip6: null };
132
+ if (typeof u !== 'string')
133
+ return bad;
134
+ const m = /^([A-Za-z][A-Za-z0-9+.-]*):\/\/(.*)$/s.exec(u);
135
+ if (!m)
136
+ return bad;
137
+ const scheme = (0, utf8_js_1.asciiLower)(m[1]);
138
+ const rest = m[2];
139
+ if (/[@/?#]/.test(rest))
140
+ return bad;
141
+ let host;
142
+ let portText = null;
143
+ let ip6 = null;
144
+ let shown;
145
+ if (rest.startsWith('[')) {
146
+ // IPv6 literal: "]" required, then the end or ":port".
147
+ const end = rest.indexOf(']');
148
+ if (end < 0)
149
+ return bad;
150
+ host = (0, utf8_js_1.asciiLower)(rest.slice(1, end));
151
+ ip6 = (0, hostpat_js_1.parseIpv6)(host);
152
+ if (!ip6)
153
+ return bad;
154
+ const tail = rest.slice(end + 1);
155
+ if (tail.startsWith(':'))
156
+ portText = tail.slice(1);
157
+ else if (tail !== '')
158
+ return bad;
159
+ shown = '[' + host + ']';
160
+ }
161
+ else {
162
+ host = rest;
163
+ const c = rest.lastIndexOf(':');
164
+ if (c >= 0) {
165
+ host = rest.slice(0, c);
166
+ portText = rest.slice(c + 1);
167
+ }
168
+ host = (0, utf8_js_1.asciiLower)(host);
169
+ if (!/^[a-z0-9.-]{1,253}$/.test(host))
170
+ return bad;
171
+ shown = host;
172
+ }
173
+ let port = null;
174
+ if (portText !== null) {
175
+ if (!/^[0-9]{1,5}$/.test(portText))
176
+ return bad;
177
+ const pn = Number(portText);
178
+ if (pn < 1 || pn > 65535)
179
+ return bad;
180
+ port = String(pn); // A-PORT-ZEROS
181
+ if ((scheme === 'http' && pn === 80) || (scheme === 'https' && pn === 443))
182
+ port = null;
183
+ }
184
+ const hostport = port ? shown + ':' + port : shown;
185
+ return { invalid: false, origin: scheme + '://' + hostport, hostport, host, port, ip6 };
186
+ }
187
+ exports.FRAMEWORKS = new Set([
188
+ 'express', 'koa', 'fastify', 'flask', 'fastapi', 'django', 'spring', 'jaxrs',
189
+ 'aspnetcore', 'gin', 'chi', 'nethttp', 'laravel', 'symfony',
190
+ ]);
191
+ const RE_COLON = /^:([A-Za-z_][A-Za-z0-9_]{0,63})$/;
192
+ const RE_COLON_REGEX = /^:([A-Za-z_][A-Za-z0-9_]{0,63})\((.*)\)$/s;
193
+ const RE_ANGLE = /^<(?:([A-Za-z_][A-Za-z0-9_]*):)?([A-Za-z_][A-Za-z0-9_]{0,63})>$/;
194
+ const RE_BRACE = /^\{([A-Za-z_][A-Za-z0-9_]{0,63})\}$/;
195
+ const RE_BRACE_CONV = /^\{([A-Za-z_][A-Za-z0-9_]{0,63}):(.+)\}$/s;
196
+ const RE_BRACE_SYMFONY = /^\{([A-Za-z_][A-Za-z0-9_]{0,63})<(.+)>\}$/s;
197
+ const RE_PNAME = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
198
+ /** One segment of a framework route: the parameter name, or null when it is not a parameter syntax of that framework. */
199
+ function frameworkParam(fw, s) {
200
+ let m;
201
+ switch (fw) {
202
+ case 'express':
203
+ case 'koa':
204
+ case 'fastify':
205
+ if ((m = RE_COLON.exec(s)))
206
+ return m[1];
207
+ // ":name(regex)"; a regex holding another ":param" means several parameters.
208
+ if ((m = RE_COLON_REGEX.exec(s)) && !/:[A-Za-z_]/.test(m[2].replace(/\\./g, '')))
209
+ return m[1];
210
+ return null;
211
+ case 'gin':
212
+ return (m = RE_COLON.exec(s)) ? m[1] : null;
213
+ case 'flask':
214
+ case 'django':
215
+ return (m = RE_ANGLE.exec(s)) && m[1] !== 'path' ? m[2] : null;
216
+ case 'fastapi':
217
+ if ((m = RE_BRACE.exec(s)))
218
+ return m[1];
219
+ return (m = RE_BRACE_CONV.exec(s)) && m[2] !== 'path' ? m[1] : null;
220
+ case 'spring':
221
+ case 'chi':
222
+ if ((m = RE_BRACE.exec(s)))
223
+ return m[1];
224
+ return (m = RE_BRACE_CONV.exec(s)) ? m[1] : null;
225
+ case 'jaxrs': {
226
+ // "{ name : regex }": spaces around the name and the colon are ignored.
227
+ if (s[0] !== '{' || s[s.length - 1] !== '}')
228
+ return null;
229
+ const inner = s.slice(1, -1);
230
+ const c = inner.indexOf(':');
231
+ const name = (c < 0 ? inner : inner.slice(0, c)).trim();
232
+ if (!RE_PNAME.test(name))
233
+ return null;
234
+ if (c >= 0 && inner.slice(c + 1).trim() === '')
235
+ return null;
236
+ return name;
237
+ }
238
+ case 'aspnetcore':
239
+ if ((m = RE_BRACE.exec(s)))
240
+ return m[1];
241
+ // "{name:constraint[:constraint...]}"; "?" marks an optional parameter, "=" a default.
242
+ if ((m = RE_BRACE_CONV.exec(s)) && !m[2].endsWith('?') && !m[2].includes('='))
243
+ return m[1];
244
+ return null;
245
+ case 'nethttp':
246
+ case 'laravel':
247
+ return (m = RE_BRACE.exec(s)) ? m[1] : null;
248
+ case 'symfony':
249
+ if ((m = RE_BRACE.exec(s)))
250
+ return m[1];
251
+ return (m = RE_BRACE_SYMFONY.exec(s)) ? m[1] : null;
252
+ default:
253
+ return null;
254
+ }
255
+ }
256
+ /**
257
+ * 3.4 with A-FRAMEWORK-ID (normative since 1.1.1): normalizes `route` with the table of
258
+ * `framework`. Every segment must be a parameter syntax of that framework or a literal of
259
+ * the operation-key grammar; anything else (every "not normalizable" case) fails.
260
+ */
261
+ function normalizeFrameworkRoute(route, framework) {
262
+ if (!exports.FRAMEWORKS.has(framework))
263
+ return null;
264
+ const raw = route.split('/').filter((s) => s !== '');
265
+ const out = [];
266
+ for (let i = 0; i < raw.length; i++) {
267
+ const s = raw[i];
268
+ if (framework === 'nethttp' && s === '{$}' && i === raw.length - 1)
269
+ continue; // exact-match marker
270
+ const p = frameworkParam(framework, s);
271
+ if (p !== null)
272
+ out.push({ param: p });
273
+ else if (formats_js_1.PATH_TOKEN_RE.test(s))
274
+ out.push({ lit: s });
275
+ else
276
+ return null;
277
+ }
278
+ return out.length > MAX_SEGMENTS ? null : out;
279
+ }
280
+ // ---------------------------------------------------------------------------------
281
+ // Buckets (5.5)
282
+ function daysFromCivil(y, m, d) {
283
+ y -= m <= 2 ? 1 : 0;
284
+ const era = Math.floor(y / 400);
285
+ const yoe = y - era * 400;
286
+ const doy = Math.floor((153 * (m + (m > 2 ? -3 : 9)) + 2) / 5) + d - 1;
287
+ const doe = yoe * 365 + Math.floor(yoe / 4) - Math.floor(yoe / 100) + doy;
288
+ return era * 146097 + doe - 719468;
289
+ }
290
+ function civilFromDays(z) {
291
+ z += 719468;
292
+ const era = Math.floor(z / 146097);
293
+ const doe = z - era * 146097;
294
+ const yoe = Math.floor((doe - Math.floor(doe / 1460) + Math.floor(doe / 36524) - Math.floor(doe / 146096)) / 365);
295
+ const y = yoe + era * 400;
296
+ const doy = doe - (365 * yoe + Math.floor(yoe / 4) - Math.floor(yoe / 100));
297
+ const mp = Math.floor((5 * doy + 2) / 153);
298
+ const d = doy - Math.floor((153 * mp + 2) / 5) + 1;
299
+ const m = mp + (mp < 10 ? 3 : -9);
300
+ return [m <= 2 ? y + 1 : y, m, d];
301
+ }
302
+ const TIME_PARSE = /^([0-9]{4})-([0-9]{2})-([0-9]{2})(?:[Tt ]([0-9]{2}):([0-9]{2}):([0-9]{2})(?:\.[0-9]{1,9})?(?:[Zz]|([+-])([0-9]{2}):([0-9]{2})))?$/;
303
+ function bucketTime(s, unit) {
304
+ if (!(0, formats_js_1.isPrintableAscii)(s) || !(formats_js_1.RE_DATETIME.test(s) || formats_js_1.RE_DATE.test(s)))
305
+ return null;
306
+ const m = TIME_PARSE.exec(s);
307
+ if (!m)
308
+ return null;
309
+ const y = Number(m[1]);
310
+ const mo = Number(m[2]);
311
+ const d = Number(m[3]);
312
+ const h = m[4] ? Number(m[4]) : 0;
313
+ const mi = m[5] ? Number(m[5]) : 0;
314
+ const se = m[6] ? Number(m[6]) : 0;
315
+ if (y < 1 || y > 9999 || mo < 1 || mo > 12 || d < 1)
316
+ return null;
317
+ const leap = (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0;
318
+ const dim = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][mo - 1];
319
+ if (d > dim || h > 23 || mi > 59 || se > 59)
320
+ return null;
321
+ let off = 0;
322
+ if (m[7]) {
323
+ const oh = Number(m[8]);
324
+ const om = Number(m[9]);
325
+ if (oh > 23 || om > 59)
326
+ return null;
327
+ off = (oh * 60 + om) * (m[7] === '+' ? 1 : -1);
328
+ }
329
+ const total = daysFromCivil(y, mo, d) * 1440 + h * 60 + mi - off;
330
+ const days = Math.floor(total / 1440);
331
+ const hour = Math.floor((total - days * 1440) / 60);
332
+ const [uy, um, ud] = civilFromDays(days);
333
+ if (uy < 1 || uy > 9999)
334
+ return null;
335
+ const p2 = (n) => String(n).padStart(2, '0');
336
+ const ys = String(uy).padStart(4, '0');
337
+ switch (unit) {
338
+ case 'year': return 'time:' + ys;
339
+ case 'month': return 'time:' + ys + '-' + p2(um);
340
+ case 'day': return 'time:' + ys + '-' + p2(um) + '-' + p2(ud);
341
+ case 'hour': return 'time:' + ys + '-' + p2(um) + '-' + p2(ud) + 'T' + p2(hour);
342
+ default: return null;
343
+ }
344
+ }
345
+ function bucketNum(lex, a) {
346
+ const b = a.bounds;
347
+ const v = Number(lex);
348
+ if (v < b[0].d)
349
+ return 'num:<' + b[0].canon;
350
+ for (let i = 0; i + 1 < b.length; i++) {
351
+ if (v >= b[i].d && v < b[i + 1].d)
352
+ return 'num:' + b[i].canon + '-' + b[i + 1].canon;
353
+ }
354
+ return 'num:>=' + b[b.length - 1].canon;
355
+ }
356
+ const B64_ANY = /^[A-Za-z0-9_+/-]*$/;
357
+ /**
358
+ * Standard or URL-safe base64 (never mixed), with or without padding (at most two '=' at the end, then a
359
+ * length that is a multiple of 4). Null on any other character or an impossible length.
360
+ */
361
+ function b64Any(s) {
362
+ let body = s;
363
+ for (let k = 0; k < 2 && body.endsWith('='); k++)
364
+ body = body.slice(0, -1);
365
+ if (body === '' || (body.length !== s.length && s.length % 4 !== 0))
366
+ return null;
367
+ if (!B64_ANY.test(body) || body.length % 4 === 1)
368
+ return null;
369
+ const std = body.includes('+') || body.includes('/');
370
+ if (std && (body.includes('-') || body.includes('_')))
371
+ return null;
372
+ return Buffer.from(body, std ? 'base64' : 'base64url');
373
+ }
374
+ /**
375
+ * Spec v1.18 (sanitizer.md 11, step 4 bis): the subject is a member of a header whose value is a JSON object, or
376
+ * base64/base64url of one. No bearer prefix is stripped; the dot always separates members.
377
+ */
378
+ function jsonMember(s, path) {
379
+ let a = 0;
380
+ let b = s.length;
381
+ while (a < b && (s.charCodeAt(a) === 0x20 || s.charCodeAt(a) === 0x09))
382
+ a++;
383
+ while (b > a && (s.charCodeAt(b - 1) === 0x20 || s.charCodeAt(b - 1) === 0x09))
384
+ b--;
385
+ const t = s.slice(a, b);
386
+ if (t === '')
387
+ return null;
388
+ const bytes = t.charCodeAt(0) === 0x7b ? (0, utf8_js_1.utf8)(t) : b64Any(t);
389
+ if (bytes === null)
390
+ return null;
391
+ let v;
392
+ try {
393
+ v = (0, json_js_1.parseJson)(bytes);
394
+ }
395
+ catch {
396
+ return null;
397
+ }
398
+ for (const name of path.split('.')) {
399
+ if (v.k !== 'o')
400
+ return null;
401
+ let found;
402
+ for (const [k, val] of v.m)
403
+ if (!k.bad && k.s === name)
404
+ found = val;
405
+ if (!found)
406
+ return null;
407
+ v = found;
408
+ }
409
+ if (v.k === 's')
410
+ return v.s !== '' && !v.bad ? (0, utf8_js_1.utf8)(v.s) : null;
411
+ if (v.k === 'n')
412
+ return (0, json_js_1.lexemeWithinLimits)(v.lex) ? Buffer.from((0, json_js_1.canonicalNumber)(v.lex), 'latin1') : null;
413
+ return null;
414
+ }
415
+ class Sanitizer {
416
+ policy;
417
+ key;
418
+ options;
419
+ epochBuf;
420
+ constructor(policy, key, options = {}) {
421
+ this.policy = policy;
422
+ this.key = key;
423
+ this.options = options;
424
+ this.epochBuf = Buffer.from(policy.epoch, 'latin1');
425
+ this.subjName = policy.subject ? (policy.subject.from === 'header' ? (0, utf8_js_1.asciiLower)(policy.subject.name) : 'cookie') : '';
426
+ }
427
+ mac(domain, value) {
428
+ const h = (0, node_crypto_1.createHmac)('sha256', this.key.key);
429
+ h.update(this.epochBuf);
430
+ h.update(ZERO);
431
+ h.update(domain, 'latin1');
432
+ h.update(ZERO);
433
+ h.update(value);
434
+ return h.digest();
435
+ }
436
+ token(domain, value) {
437
+ return 'tk1:' + this.key.kid + ':' + this.mac(domain, value).subarray(0, 16).toString('base64url');
438
+ }
439
+ rateBp() {
440
+ const o = this.options.rateOverrideBp;
441
+ return typeof o === 'number' ? Math.min(o, this.policy.rateBp) : this.policy.rateBp;
442
+ }
443
+ // --- subject (11) ---------------------------------------------------------------
444
+ subjectValue(reqHeaders) {
445
+ const spec = this.policy.subject;
446
+ if (!spec)
447
+ return null;
448
+ let raw = null;
449
+ if (spec.from === 'header') {
450
+ raw = headerValue(reqHeaders, (0, utf8_js_1.asciiLower)(spec.name));
451
+ }
452
+ else {
453
+ const cookie = headerValue(reqHeaders, 'cookie');
454
+ if (cookie) {
455
+ for (const p of (0, utf8_js_1.splitBytes)(cookie, 0x3b)) {
456
+ const piece = (0, utf8_js_1.trimOws)(p);
457
+ const eq = piece.indexOf(0x3d);
458
+ if (eq < 0)
459
+ continue;
460
+ if ((0, utf8_js_1.decodeUtf8)(piece.subarray(0, eq)) === spec.name) {
461
+ raw = piece.subarray(eq + 1);
462
+ break;
463
+ }
464
+ }
465
+ }
466
+ }
467
+ if (raw === null || raw.length === 0 || raw.length > SUBJECT_MAX)
468
+ return null;
469
+ let s = (0, utf8_js_1.decodeUtf8)(raw);
470
+ if (s === null || s === '')
471
+ return null;
472
+ if (spec.from === 'header' && spec.jsonPath !== undefined)
473
+ return jsonMember(s, spec.jsonPath);
474
+ if (spec.from === 'header') {
475
+ const m = /^bearer[ \t]+/i.exec(s);
476
+ if (m) {
477
+ s = s.slice(m[0].length);
478
+ if (s === '')
479
+ return null;
480
+ }
481
+ }
482
+ if (spec.jwtClaim === undefined)
483
+ return (0, utf8_js_1.utf8)(s);
484
+ const parts = s.split('.');
485
+ if (parts.length !== 3)
486
+ return null;
487
+ const payload = parts[1].replace(/={1,2}$/, '');
488
+ const bytes = (0, policy_js_1.b64urlDecodeStrict)(payload);
489
+ if (bytes === null)
490
+ return null;
491
+ let doc;
492
+ try {
493
+ doc = (0, json_js_1.parseJson)(bytes);
494
+ }
495
+ catch {
496
+ return null;
497
+ }
498
+ if (doc.k !== 'o')
499
+ return null;
500
+ let v;
501
+ for (const [k, val] of doc.m)
502
+ if (!k.bad && k.s === spec.jwtClaim)
503
+ v = val;
504
+ if (!v)
505
+ return null;
506
+ if (v.k === 's')
507
+ return v.s !== '' && !v.bad ? (0, utf8_js_1.utf8)(v.s) : null;
508
+ if (v.k === 'n')
509
+ return (0, json_js_1.lexemeWithinLimits)(v.lex) ? Buffer.from((0, json_js_1.canonicalNumber)(v.lex), 'latin1') : null;
510
+ return null;
511
+ }
512
+ // --- sampling (12) --------------------------------------------------------------
513
+ hashSample(domain, value) {
514
+ return this.mac(domain, value).readUInt32BE(0) % 10000 < this.rateBp();
515
+ }
516
+ randomSample() {
517
+ const rate = this.rateBp();
518
+ if (rate <= 0)
519
+ return false;
520
+ if (rate >= 10000)
521
+ return true;
522
+ return Math.floor((this.options.rand ?? Math.random)() * 10000) < rate;
523
+ }
524
+ subjName;
525
+ /** Subject header value (as received) -> 1 sampled, 2 not sampled, 3 no usable subject. Per policy and key, bounded. */
526
+ subjCache = new Map();
527
+ /**
528
+ * Inbound decision straight from node's rawHeaders (flat latin1 strings) and the
529
+ * traceparent trace id, with no per-request allocation on the common paths. It returns the
530
+ * same decision as `decide()` on the same headers, or null when it cannot answer fast
531
+ * (baggage trusted, repeated subject header): the caller then uses `decide()`.
532
+ * The subject answer is remembered per header value: the HMAC and the parsing run once per
533
+ * distinct value (a user's cookie or token repeats on every request).
534
+ */
535
+ decideRaw(service, raw, traceId) {
536
+ const p = this.policy;
537
+ if (p.trustBaggage && p.inbound.get(service)?.internal)
538
+ return null;
539
+ const spec = p.subject;
540
+ if (spec) {
541
+ const want = this.subjName;
542
+ let val;
543
+ for (let i = 0; i + 1 < raw.length; i += 2) {
544
+ const n = raw[i];
545
+ if (n.length === want.length && (n === want || (0, utf8_js_1.asciiLower)(n) === want)) {
546
+ if (val !== undefined)
547
+ return null;
548
+ val = raw[i + 1];
549
+ }
550
+ }
551
+ if (val !== undefined) {
552
+ let c = this.subjCache.get(val);
553
+ if (c === undefined) {
554
+ const subj = this.subjectValue([[want, Buffer.from(val, 'latin1')]]);
555
+ c = subj ? (this.hashSample('sample', subj) ? 1 : 2) : 3;
556
+ if (val.length <= 2048) {
557
+ if (this.subjCache.size >= 4096)
558
+ this.subjCache.clear();
559
+ this.subjCache.set(val, c);
560
+ }
561
+ }
562
+ if (c === 1)
563
+ return D_SUBJECT_YES;
564
+ if (c === 2)
565
+ return D_SUBJECT_NO;
566
+ }
567
+ }
568
+ if (traceId)
569
+ return { sampled: this.hashSample('sample-trace', Buffer.from((0, utf8_js_1.asciiLower)(traceId), 'latin1')), by: 'trace' };
570
+ const rate = this.rateBp();
571
+ if (rate <= 0)
572
+ return D_RANDOM_NO;
573
+ if (rate >= 10000)
574
+ return D_RANDOM_YES;
575
+ return Math.floor((this.options.rand ?? Math.random)() * 10000) < rate ? D_RANDOM_YES : D_RANDOM_NO;
576
+ }
577
+ /** Sampling decision for an exchange (needs only the request line and headers). */
578
+ decide(ex, subject) {
579
+ if (ex.dir === 'inbound') {
580
+ const svc = ex.service_name !== undefined ? this.policy.inbound.get(ex.service_name) : undefined;
581
+ if (this.policy.trustBaggage && svc?.internal) {
582
+ const bag = headerValue(ex.req_headers, 'baggage');
583
+ const s = bag ? (0, utf8_js_1.decodeUtf8)(bag) : null;
584
+ if (s !== null) {
585
+ for (const member of s.split(',')) {
586
+ const kv = (0, utf8_js_1.trimOwsStr)(member.split(';', 1)[0]);
587
+ const eq = kv.indexOf('=');
588
+ if (eq < 0)
589
+ continue;
590
+ const k = (0, utf8_js_1.trimOwsStr)(kv.slice(0, eq));
591
+ const v = (0, utf8_js_1.trimOwsStr)(kv.slice(eq + 1));
592
+ if (k === 'tt.s' && (v === '0' || v === '1'))
593
+ return { sampled: v === '1', by: 'baggage' };
594
+ }
595
+ }
596
+ }
597
+ const subj = subject === undefined ? this.subjectValue(ex.req_headers) : subject;
598
+ if (subj)
599
+ return { sampled: this.hashSample('sample', subj), by: 'subject' };
600
+ }
601
+ else if (typeof ex.inherited === 'boolean') {
602
+ return { sampled: ex.inherited, by: 'inherited' };
603
+ }
604
+ if (ex.trace_id)
605
+ return { sampled: this.hashSample('sample-trace', Buffer.from((0, utf8_js_1.asciiLower)(ex.trace_id), 'latin1')), by: 'trace' };
606
+ return { sampled: this.randomSample(), by: 'random' };
607
+ }
608
+ // --- whole exchange -------------------------------------------------------------
609
+ run(ex, now, decision) {
610
+ const subject = this.subjectValue(ex.req_headers);
611
+ const d = decision ?? this.decide(ex, subject);
612
+ if (!d.sampled)
613
+ return { outcome: 'not_sampled', sampledBy: d.by };
614
+ const run = new Run(this, ex, now);
615
+ const out = run.build(d.by, subject);
616
+ // Each top-level member is serialized once; the source reuses these parts to assemble
617
+ // the full event without serializing the tree again.
618
+ const parts = new Map();
619
+ for (const [k, v] of out)
620
+ parts.set(k, (0, json_js_1.writeCanonical)(v));
621
+ let canonical = composeCanonical(parts);
622
+ // 10.3, measured on the sanitizer output (A-EVENT-SIZE-BASE). Canonical output is ASCII.
623
+ if (canonical.length > exports.MAX_EVENT_BYTES) {
624
+ const steps = [
625
+ () => (replaceBody(out.get('req')) ? 'req' : null),
626
+ () => (replaceBody(out.get('resp')) ? 'resp' : null),
627
+ () => (out.delete('disc') ? 'disc' : null),
628
+ ];
629
+ for (const step of steps) {
630
+ const k = step();
631
+ if (k === null)
632
+ continue;
633
+ if (out.has(k))
634
+ parts.set(k, (0, json_js_1.writeCanonical)(out.get(k)));
635
+ else
636
+ parts.delete(k);
637
+ canonical = composeCanonical(parts);
638
+ if (canonical.length <= exports.MAX_EVENT_BYTES)
639
+ break;
640
+ }
641
+ if (canonical.length > exports.MAX_EVENT_BYTES)
642
+ return { outcome: 'too_large', sampledBy: d.by };
643
+ }
644
+ return { outcome: 'event', out, parts, canonical, sampledBy: d.by };
645
+ }
646
+ }
647
+ exports.Sanitizer = Sanitizer;
648
+ /** Ops of a service with this method and segment count, in policy order. */
649
+ function candidateOps(svc, method, nsegs) {
650
+ let idx = svc.opIndex;
651
+ if (!idx) {
652
+ idx = new Map();
653
+ for (const op of svc.ops) {
654
+ const k = op.method + '/' + op.segs.length;
655
+ const l = idx.get(k);
656
+ if (l)
657
+ l.push(op);
658
+ else
659
+ idx.set(k, [op]);
660
+ }
661
+ svc.opIndex = idx;
662
+ }
663
+ return idx.get(method + '/' + nsegs) ?? NO_OPS;
664
+ }
665
+ const NO_OPS = [];
666
+ /** Per-exchange state. */
667
+ class Run {
668
+ s;
669
+ ex;
670
+ now;
671
+ p;
672
+ disc;
673
+ autoWords;
674
+ svc;
675
+ op;
676
+ /** limits.max_array, raised for the response body when the introspection gate is open (5.7.2). */
677
+ maxArray;
678
+ // GraphQL (5.7): resp is set while the response is sanitized; gqlSeen and gqlNonIntro
679
+ // record the request values with a graphql rule.
680
+ resp = false;
681
+ gqlSeen = false;
682
+ gqlNonIntro = false;
683
+ constructor(s, ex, now) {
684
+ this.s = s;
685
+ this.ex = ex;
686
+ this.now = now;
687
+ this.p = s.policy;
688
+ this.maxArray = this.p.limits.maxArray;
689
+ this.disc = this.p.discoveryActive(now) ? new Map() : null;
690
+ this.autoWords = this.p.autoWordsActive(now);
691
+ }
692
+ build(by, subject) {
693
+ const ex = this.ex;
694
+ const p = this.p;
695
+ let method = (0, utf8_js_1.asciiUpper)(ex.method ?? '');
696
+ if (!/^[A-Z]{1,16}$/.test(method))
697
+ method = 'OTHER';
698
+ const { segs, truncated } = decomposePath(ex.raw_path ?? Buffer.alloc(0));
699
+ // 3.1 service
700
+ let service;
701
+ let baseCount = 0;
702
+ if (ex.dir === 'inbound') {
703
+ service = ex.service_name ?? '';
704
+ this.svc = p.inbound.get(service);
705
+ }
706
+ else {
707
+ const ub = normalizeUrlBase(ex.url_base);
708
+ let best;
709
+ if (!ub.invalid) {
710
+ for (const svc of p.services) {
711
+ if (!svc.base || svc.base.origin !== ub.origin)
712
+ continue;
713
+ const bs = svc.base.segs;
714
+ if (bs.length > segs.length)
715
+ continue;
716
+ let ok = true;
717
+ for (let i = 0; i < bs.length; i++)
718
+ if (segs[i].dec !== bs[i])
719
+ ok = false;
720
+ if (ok && (!best || bs.length > best.base.segs.length))
721
+ best = svc;
722
+ }
723
+ }
724
+ if (best) {
725
+ this.svc = best;
726
+ service = best.name;
727
+ baseCount = best.base.segs.length;
728
+ }
729
+ else if (ub.invalid)
730
+ service = 'host:invalid';
731
+ else if (!ub.ip6 && p.hostsAllow.has(ub.hostport))
732
+ service = 'host:' + ub.hostport;
733
+ else if (p.hostsAuto || this.disc) {
734
+ // v1.5: the host leaves as a pattern, never in clear (3.5). An empty label is invalid.
735
+ const pat = (0, hostpat_js_1.hostPattern)(ub.host, ub.port, ub.ip6);
736
+ service = pat === null ? 'host:invalid' : 'host:' + pat;
737
+ }
738
+ else
739
+ service = 'host:unlisted';
740
+ }
741
+ // 3.3 template
742
+ const opSegs = segs.slice(baseCount);
743
+ let routeSource = 'raw';
744
+ if (this.svc && method !== 'OTHER' && !truncated) {
745
+ if (ex.dir === 'inbound' && typeof ex.framework_route === 'string' && typeof ex.framework === 'string') {
746
+ const fr = normalizeFrameworkRoute(ex.framework_route, ex.framework);
747
+ if (fr && fr.length === opSegs.length) {
748
+ for (const op of this.svc.ops) {
749
+ if (op.method !== method || op.segs.length !== fr.length)
750
+ continue;
751
+ if (op.segs.every((t, i) => (t.lit !== undefined ? fr[i].lit === t.lit : fr[i].param !== undefined))) {
752
+ this.op = op;
753
+ routeSource = 'framework';
754
+ break;
755
+ }
756
+ }
757
+ }
758
+ }
759
+ if (!this.op) {
760
+ let best;
761
+ for (const op of candidateOps(this.svc, method, opSegs.length)) {
762
+ let ok = true;
763
+ for (let i = 0; i < op.segs.length && ok; i++) {
764
+ const t = op.segs[i];
765
+ const sd = opSegs[i].dec;
766
+ ok = t.lit !== undefined ? sd === t.lit : sd !== null;
767
+ }
768
+ if (!ok)
769
+ continue;
770
+ if (!best || op.literals > best.literals || (op.literals === best.literals && (0, utf8_js_1.cmpUtf8)(op.key, best.key) < 0))
771
+ best = op;
772
+ }
773
+ if (best) {
774
+ this.op = best;
775
+ routeSource = 'policy';
776
+ }
777
+ }
778
+ }
779
+ // 4.2 path
780
+ const outSegs = [];
781
+ for (let i = 0; i < segs.length; i++) {
782
+ const dec = segs[i].dec;
783
+ if (dec === null) {
784
+ outSegs.push('{drop:bytes}');
785
+ continue;
786
+ }
787
+ if (i < baseCount) {
788
+ outSegs.push(dec);
789
+ continue;
790
+ }
791
+ if (this.op) {
792
+ const t = this.op.segs[i - baseCount];
793
+ if (t.lit !== undefined) {
794
+ outSegs.push(t.lit);
795
+ continue;
796
+ }
797
+ const action = this.op.request?.path.get(t.param);
798
+ outSegs.push('{' + this.strAction(dec, action, true, 'req.path.' + i) + '}');
799
+ continue;
800
+ }
801
+ if (p.allow.has(dec) || (this.autoWords && formats_js_1.AUTO_WORD_RE.test(dec) && (0, utf8_js_1.utf8Length)(dec) <= 40)) {
802
+ outSegs.push(dec);
803
+ continue;
804
+ }
805
+ outSegs.push('{' + this.strAction(dec, undefined, true, 'req.path.' + i) + '}');
806
+ }
807
+ if (truncated)
808
+ outSegs.push('{trunc}');
809
+ const path = '/' + outSegs.join('/');
810
+ const op = this.op ? this.op.key : method + ' /' + outSegs.slice(baseCount).join('/');
811
+ const out = new Map();
812
+ out.set('service', service);
813
+ out.set('op', op);
814
+ out.set('route_source', routeSource);
815
+ out.set('sampled_by', by);
816
+ // request
817
+ const req = new Map();
818
+ req.set('path', path);
819
+ const reqGroups = this.groups('req', undefined);
820
+ if (ex.raw_query !== undefined && ex.raw_query !== null) {
821
+ const q = this.namedMap(parseUrlencoded(ex.raw_query), reqGroups, 'query', 'req.query');
822
+ if (q.size > 0)
823
+ req.set('query', q);
824
+ }
825
+ const rh = normalizeHeaders(ex.req_headers);
826
+ this.side(req, 'req', rh, reqGroups, ex.req_body, ex.req_body_total, ex.req_body_consumed, ex.req_body_decoded);
827
+ out.set('req', req);
828
+ // Response: ignored with net_error, even if the source passed parts.
829
+ this.resp = true;
830
+ if (ex.net_error === undefined || ex.net_error === null) {
831
+ const resp = new Map();
832
+ const respGroups = this.groups('resp', ex.status);
833
+ const sh = normalizeHeaders(ex.resp_headers);
834
+ this.side(resp, 'resp', sh, respGroups, ex.resp_body, ex.resp_body_total, ex.resp_body_consumed, ex.resp_body_decoded);
835
+ out.set('resp', resp);
836
+ }
837
+ if (subject)
838
+ out.set('subject', this.s.token('subject', subject));
839
+ if (this.disc && this.disc.size > 0) {
840
+ const d = new Map();
841
+ const keys = [...this.disc.keys()].sort(utf8_js_1.cmpUtf8).slice(0, MAX_DISC);
842
+ for (const k of keys) {
843
+ const mac = this.s.mac('discovery', Buffer.concat([Buffer.from(k, 'utf8'), Buffer.from([0]), this.disc.get(k)]));
844
+ d.set(k, 'tkd:' + mac.subarray(0, 4).toString('base64url'));
845
+ }
846
+ out.set('disc', d);
847
+ }
848
+ return out;
849
+ }
850
+ groups(side, status) {
851
+ const key = side === 'req' ? 'request' : 'response';
852
+ const err = side === 'resp' && typeof status === 'number' && status >= 400;
853
+ return [
854
+ this.op?.[key],
855
+ err ? this.svc?.errors : undefined,
856
+ err ? this.p.global.errors : undefined,
857
+ this.svc?.[key],
858
+ this.p.global[key],
859
+ ];
860
+ }
861
+ side(dst, side, h, groups, body, total, consumed, decoded) {
862
+ // 8.1 headers (cookie / set-cookie never appear)
863
+ const hdrEntries = [];
864
+ for (const name of h.order) {
865
+ if (name === 'cookie' || name === 'set-cookie')
866
+ continue;
867
+ const e = h.map.get(name);
868
+ hdrEntries.push({ orig: e.origs.size === 1 ? [...e.origs][0] : null, out: name, value: joined(e), noEq: false });
869
+ }
870
+ const hm = this.namedMap(hdrEntries, groups, 'headers', side + '.headers');
871
+ if (hm.size > 0)
872
+ dst.set('headers', hm);
873
+ // 8.2 cookies
874
+ const ck = [];
875
+ if (side === 'req') {
876
+ const e = h.map.get('cookie');
877
+ if (e) {
878
+ for (const p of (0, utf8_js_1.splitBytes)(joined(e), 0x3b))
879
+ cookiePart((0, utf8_js_1.trimOws)(p), ck);
880
+ }
881
+ }
882
+ else {
883
+ const e = h.map.get('set-cookie');
884
+ if (e) {
885
+ for (const v of e.values) {
886
+ const semi = v.indexOf(0x3b);
887
+ cookiePart((0, utf8_js_1.trimOws)(semi >= 0 ? v.subarray(0, semi) : v), ck);
888
+ }
889
+ }
890
+ }
891
+ const cm = this.namedMap(ck, groups, 'cookies', side + '.cookies');
892
+ if (cm.size > 0)
893
+ dst.set('cookies', cm);
894
+ const ct = contentType(h);
895
+ if (ct !== undefined)
896
+ dst.set('ct', ct);
897
+ const kind = this.treatment(ct);
898
+ if (side === 'req' && kind === 'xml') {
899
+ // 22.2 point 11: the SOAP operation, also without a body.
900
+ const sa = h.map.get('soapaction');
901
+ let raw;
902
+ if (sa === undefined) {
903
+ const ctv = h.map.get('content-type');
904
+ raw = (0, bodies_js_1.mediaParams)(ctv ? joined(ctv) : null).get('action') ?? null;
905
+ }
906
+ else {
907
+ raw = (0, utf8_js_1.decodeUtf8)(joined(sa)) ?? '{*}';
908
+ }
909
+ if (raw !== null) {
910
+ const name = raw === '{*}' ? '{*}' : (0, bodies_js_1.soapActionName)(raw, formats_js_1.isLiteralName);
911
+ if (name !== null)
912
+ dst.set('soap_action', name);
913
+ }
914
+ }
915
+ // 5.7.2: with the introspection gate open the response body keeps arrays up to 4096.
916
+ const raise = side === 'resp' && this.gqlGate() && this.maxArray < graphql_js_1.GQL_INTROSPECTION_MAX_ARRAY;
917
+ if (raise)
918
+ this.maxArray = graphql_js_1.GQL_INTROSPECTION_MAX_ARRAY;
919
+ try {
920
+ this.body(dst, side, h, kind, groups, body ?? Buffer.alloc(0), total, consumed, decoded);
921
+ }
922
+ finally {
923
+ if (raise)
924
+ this.maxArray = this.p.limits.maxArray;
925
+ }
926
+ // 22.1: fmt whenever a body leaves under the XML or multipart treatment.
927
+ if ((kind === 'xml' || kind === 'multipart') && dst.has('body') && !dst.has('fmt'))
928
+ dst.set('fmt', kind);
929
+ }
930
+ /** 9.1 and 22.1: how a body with this `ct` is read. */
931
+ treatment(ct) {
932
+ if (ct === undefined)
933
+ return null;
934
+ if (ct === 'application/json' || ct.endsWith('+json'))
935
+ return 'json';
936
+ if (ct === 'application/x-www-form-urlencoded')
937
+ return 'form';
938
+ const b = this.p.bodies;
939
+ if (b.xml && (ct === 'application/xml' || ct === 'text/xml' || ct.endsWith('+xml')))
940
+ return 'xml';
941
+ if (b.multipart && ct === 'multipart/form-data')
942
+ return 'multipart';
943
+ return null;
944
+ }
945
+ // --- body (9) -------------------------------------------------------------------
946
+ body(dst, side, h, kind, groups, data, total, consumed, decoded) {
947
+ const max = this.p.limits.maxBodyBytes;
948
+ const largeMax = this.p.bodies.maxLargeBodyBytes;
949
+ let tot = total === undefined ? data.length : total;
950
+ if (consumed === false || tot === 'unknown') {
951
+ dst.set('body', 'trunc:size');
952
+ dst.set('size', 'lmax');
953
+ return;
954
+ }
955
+ if (tot === 0) {
956
+ dst.set('size', 'l8');
957
+ return;
958
+ }
959
+ const ce = contentEncoding(h);
960
+ if (ce !== undefined && ce !== 'identity') {
961
+ if (ce !== 'gzip' && ce !== 'x-gzip' && ce !== 'deflate') {
962
+ dst.set('body', 'drop:bytes');
963
+ return;
964
+ }
965
+ if (decoded !== true) {
966
+ // 22.5: the decoding cap includes the large-body limit.
967
+ const r = decodeBody(data, ce === 'deflate', Math.max(max, largeMax) + 1);
968
+ if (r === 'cap') {
969
+ dst.set('body', 'trunc:size');
970
+ dst.set('size', 'lmax');
971
+ return;
972
+ }
973
+ if (r === null) {
974
+ dst.set('body', 'drop:bytes');
975
+ return;
976
+ }
977
+ data = r;
978
+ tot = data.length;
979
+ }
980
+ }
981
+ let large = false;
982
+ if (tot > max) {
983
+ if (kind === null || tot > largeMax) {
984
+ dst.set('body', 'trunc:size');
985
+ dst.set('size', 'lmax');
986
+ return;
987
+ }
988
+ large = true; // 22.5
989
+ }
990
+ dst.set('size', (0, formats_js_1.lenClass)(tot));
991
+ if (kind !== null && data.length < tot) {
992
+ // 9.1: only a prefix of a parsed body was kept; it cannot be analysed.
993
+ dst.set('body', 'trunc:size');
994
+ return;
995
+ }
996
+ const dpath = this.disc ? side + '.body' : null;
997
+ switch (kind) {
998
+ case 'json': {
999
+ let root;
1000
+ try {
1001
+ root = (0, json_js_1.parseJson)(data);
1002
+ }
1003
+ catch (e) {
1004
+ if (e instanceof json_js_1.JsonError) {
1005
+ dst.set('body', 'trunc:invalid-json');
1006
+ return;
1007
+ }
1008
+ throw e;
1009
+ }
1010
+ dst.set('body', this.walk(root, 0, groups.map((g) => g?.body ?? []), dpath, false, large, 0));
1011
+ return;
1012
+ }
1013
+ case 'form':
1014
+ dst.set('body', this.namedMap(parseUrlencoded(data), groups, 'form', side + '.body'));
1015
+ return;
1016
+ case 'xml': {
1017
+ let root;
1018
+ try {
1019
+ root = (0, bodies_js_1.parseXml)(data);
1020
+ }
1021
+ catch (e) {
1022
+ if (e instanceof bodies_js_1.InvalidXml) {
1023
+ dst.set('body', 'trunc:invalid-xml');
1024
+ return;
1025
+ }
1026
+ throw e;
1027
+ }
1028
+ const { entries, fmt } = (0, bodies_js_1.xmlNamespaces)(root, formats_js_1.isLiteralName, this.p.limits.maxDepth);
1029
+ dst.set('fmt', fmt);
1030
+ const tree = { k: 'o', m: [[jstr(root.local), xmlToNode((0, bodies_js_1.xmlValue)(root))]] };
1031
+ dst.set('body', this.walk(tree, 0, groups.map((g) => g?.body ?? []), dpath, true, large, 0));
1032
+ if (entries.length > 0) {
1033
+ const ns = new Map();
1034
+ for (const [k, v] of entries.slice(0, MAX_NAMES))
1035
+ ns.set(k, v);
1036
+ if (entries.length > MAX_NAMES)
1037
+ ns.set('{trunc}', 'trunc:keys:' + (0, formats_js_1.lenClass)(entries.length));
1038
+ dst.set('xmlns', ns);
1039
+ }
1040
+ return;
1041
+ }
1042
+ case 'multipart': {
1043
+ const ctv = h.map.get('content-type');
1044
+ const boundary = (0, bodies_js_1.mediaParams)(ctv ? joined(ctv) : null).get('boundary');
1045
+ let parts;
1046
+ try {
1047
+ parts = (0, bodies_js_1.parseMultipart)(data, boundary);
1048
+ }
1049
+ catch (e) {
1050
+ if (e instanceof bodies_js_1.InvalidMultipart) {
1051
+ dst.set('body', 'trunc:invalid-multipart');
1052
+ return;
1053
+ }
1054
+ throw e;
1055
+ }
1056
+ const entries = parts.map((p) => {
1057
+ const e = { orig: p.name, out: (0, formats_js_1.isLiteralName)(p.name) ? p.name : '{*}', value: p.content, noEq: false };
1058
+ if (p.isFile)
1059
+ e.token = (0, bodies_js_1.fileToken)(p.content, p.ct);
1060
+ return e;
1061
+ });
1062
+ dst.set('body', this.namedMap(entries, groups, 'form', side + '.body'));
1063
+ return;
1064
+ }
1065
+ default: {
1066
+ if (contentType(h) === 'application/graphql' && data.length === tot) {
1067
+ // 9.1 (spec v1.14): with a winning graphql rule for $ the body is one string leaf.
1068
+ const a = rootRule(groups);
1069
+ if (a?.action === 'graphql') {
1070
+ const s = (0, utf8_js_1.decodeUtf8)(data);
1071
+ if (s === null) {
1072
+ this.gqlUnusable(a);
1073
+ dst.set('body', 'drop:str:invalid');
1074
+ }
1075
+ else {
1076
+ dst.set('body', this.strAction(s, a, false, null));
1077
+ }
1078
+ return;
1079
+ }
1080
+ }
1081
+ dst.set('body', 'drop:bytes:' + (0, formats_js_1.lenClass)(tot));
1082
+ }
1083
+ }
1084
+ }
1085
+ /**
1086
+ * `xml`: keys follow 22.2 point 8; `large`: 22.5 array caps, chosen by `arrLevel` (the
1087
+ * number of arrays among the ancestors).
1088
+ */
1089
+ walk(node, pos, alive, dpath, xml, large, arrLevel) {
1090
+ const lim = this.p.limits;
1091
+ const maxArray = this.maxArray;
1092
+ if (node.k === 'o' || node.k === 'a') {
1093
+ if (pos + 1 > lim.maxDepth)
1094
+ return 'trunc:depth';
1095
+ // 5.0 explicit drop on a container
1096
+ for (let g = 0; g < alive.length; g++) {
1097
+ if (!alive[g].some((r) => r.steps.length === pos && r.action.action === 'drop'))
1098
+ continue;
1099
+ let reach = false;
1100
+ for (let h = 0; h <= g && !reach; h++)
1101
+ reach = alive[h].some((r) => r.steps.length > pos);
1102
+ if (!reach) {
1103
+ if (node.k === 'a')
1104
+ return 'drop:arr:' + (0, formats_js_1.lenClass)(node.a.length);
1105
+ return 'drop:obj:' + (0, formats_js_1.lenClass)(dedupe(node.m).length);
1106
+ }
1107
+ break;
1108
+ }
1109
+ const anyAlive = alive.some((rs) => rs.length > 0);
1110
+ if (node.k === 'a') {
1111
+ const res = [];
1112
+ const cap = large ? (arrLevel === 0 ? this.p.bodies.largeArray : this.p.bodies.largeNestedArray) : maxArray;
1113
+ const n = Math.min(node.a.length, cap);
1114
+ const child = anyAlive
1115
+ ? alive.map((rs) => rs.filter((r) => r.steps.length > pos && r.steps[pos].t === 'idx'))
1116
+ : alive;
1117
+ for (let i = 0; i < n; i++)
1118
+ res.push(this.walk(node.a[i], pos + 1, child, dpath === null ? null : dpath + '[*]', xml, large, arrLevel + 1));
1119
+ if (node.a.length > cap)
1120
+ res.push('trunc:n:' + (0, formats_js_1.lenClass)(node.a.length));
1121
+ return res;
1122
+ }
1123
+ const members = dedupe(node.m);
1124
+ const res = new Map();
1125
+ const dyn = [];
1126
+ const kept = members.length > lim.maxKeys ? members.slice(0, lim.maxKeys) : members;
1127
+ for (const [k, v] of kept) {
1128
+ const literal = xml ? xmlKeyIsLiteral(k.s) : !k.bad && (0, formats_js_1.isLiteralName)(k.s);
1129
+ const child = anyAlive
1130
+ ? alive.map((rs) => rs.filter((r) => {
1131
+ if (r.steps.length <= pos)
1132
+ return false;
1133
+ const st = r.steps[pos];
1134
+ return st.t === 'any' || (st.t === 'name' && !k.bad && st.name === k.s);
1135
+ }))
1136
+ : alive;
1137
+ const o = this.walk(v, pos + 1, child, dpath === null ? null : dpath + (literal ? '.' + k.s : '.*'), xml, large, arrLevel);
1138
+ if (literal)
1139
+ res.set(k.s, o);
1140
+ else
1141
+ dyn.push(o);
1142
+ }
1143
+ if (dyn.length > 0) {
1144
+ const keyed = dyn.map((o) => [(0, json_js_1.writeCanonical)(o), o]);
1145
+ keyed.sort((a, b) => (0, utf8_js_1.cmpUtf8)(a[0], b[0]));
1146
+ res.set('{*}', keyed.map((x) => x[1]));
1147
+ }
1148
+ if (members.length > lim.maxKeys)
1149
+ res.set('{trunc}', 'trunc:keys:' + (0, formats_js_1.lenClass)(members.length));
1150
+ return res;
1151
+ }
1152
+ // leaf
1153
+ for (const rs of alive) {
1154
+ let best;
1155
+ for (const r of rs) {
1156
+ if (r.steps.length !== pos)
1157
+ continue;
1158
+ if (!best || r.names > best.names || (r.names === best.names && (0, utf8_js_1.cmpUtf8)(r.path, best.path) < 0))
1159
+ best = r;
1160
+ }
1161
+ if (best)
1162
+ return this.leaf(node, best.action, null);
1163
+ }
1164
+ return this.leaf(node, undefined, dpath);
1165
+ }
1166
+ leaf(node, a, dpath) {
1167
+ // 5.7: a number, boolean, null or invalid string under graphql leaves as its form; in
1168
+ // the request it closes the introspection gate.
1169
+ if (a?.action === 'graphql' && (node.k !== 's' || node.bad))
1170
+ this.gqlUnusable(a);
1171
+ switch (node.k) {
1172
+ case 'z':
1173
+ if (a?.action === 'keep')
1174
+ return null;
1175
+ if (!a && dpath !== null)
1176
+ this.discAdd(dpath, Buffer.from('null'));
1177
+ return 'drop:null';
1178
+ case 'b':
1179
+ if (a?.action === 'keep')
1180
+ return node.v;
1181
+ if (!a && dpath !== null)
1182
+ this.discAdd(dpath, Buffer.from(node.v ? 'true' : 'false'));
1183
+ return 'drop:bool';
1184
+ case 'n': {
1185
+ const within = (0, json_js_1.lexemeWithinLimits)(node.lex);
1186
+ const shape = json_js_1.INT_LEXEME.test(node.lex) ? 'drop:int' : 'drop:dec';
1187
+ if (!a) {
1188
+ if (dpath !== null && within)
1189
+ this.discAdd(dpath, Buffer.from((0, json_js_1.canonicalNumber)(node.lex), 'latin1'));
1190
+ return shape;
1191
+ }
1192
+ if (!within)
1193
+ return shape;
1194
+ if (a.action === 'keep')
1195
+ return new json_js_1.CNum((0, json_js_1.canonicalNumber)(node.lex));
1196
+ if (a.action === 'hmac')
1197
+ return this.s.token(a.domain ?? 'id', Buffer.from((0, json_js_1.canonicalNumber)(node.lex), 'latin1'));
1198
+ if (a.action === 'bucket' && a.kind === 'num')
1199
+ return bucketNum(node.lex, a);
1200
+ return shape;
1201
+ }
1202
+ case 's':
1203
+ if (node.bad)
1204
+ return 'drop:str:invalid';
1205
+ return this.strAction(node.s, a, false, dpath);
1206
+ default:
1207
+ return 'drop:null';
1208
+ }
1209
+ }
1210
+ /** Action on a string value (named parts, path segments, JSON strings). null = not UTF-8. */
1211
+ strAction(s, a, pathParam, dpath) {
1212
+ if (s === null) {
1213
+ this.gqlUnusable(a);
1214
+ return 'drop:bytes';
1215
+ }
1216
+ if (!a) {
1217
+ if (dpath !== null && this.disc)
1218
+ this.discAdd(dpath, (0, utf8_js_1.utf8)(s));
1219
+ return this.shape(s);
1220
+ }
1221
+ switch (a.action) {
1222
+ case 'keep':
1223
+ if ((0, utf8_js_1.utf8Length)(s) <= this.p.limits.keepMaxBytes && (!pathParam || formats_js_1.PATH_TOKEN_RE.test(s)))
1224
+ return 'keep:' + s;
1225
+ return this.shape(s);
1226
+ case 'code':
1227
+ // 5.6 (spec v1.11): only a code-shaped string within keep_max_bytes leaves;
1228
+ // v1.19: the alnum shape allows digits and dots, 1 to 16 bytes.
1229
+ if ((a.shape === 'alnum' ? formats_js_1.CODE_ALNUM_RE : formats_js_1.CODE_RE).test(s) && s.length <= this.p.limits.keepMaxBytes)
1230
+ return 'keep:' + s;
1231
+ return this.shape(s);
1232
+ case 'graphql':
1233
+ return this.graphqlStr(s);
1234
+ case 'hmac':
1235
+ return this.s.token(a.domain ?? 'id', (0, utf8_js_1.utf8)(s));
1236
+ case 'bucket':
1237
+ if (a.kind === 'time' && (!pathParam || formats_js_1.PATH_TOKEN_RE.test(s)))
1238
+ return bucketTime(s, a.unit ?? '') ?? this.shape(s);
1239
+ return this.shape(s);
1240
+ default:
1241
+ return this.shape(s);
1242
+ }
1243
+ }
1244
+ /** 6.2 drop shape of a well-formed string, with the document shape of 22.4 when `bodies.docs` is on. */
1245
+ shape(s) {
1246
+ if (this.p.bodies.docs && (0, formats_js_1.detectFormat)(s) === null) {
1247
+ const d = (0, bodies_js_1.base64Doc)(s);
1248
+ if (d !== null)
1249
+ return d;
1250
+ }
1251
+ return (0, formats_js_1.strShape)(s);
1252
+ }
1253
+ // --- graphql (5.7, spec v1.14) ------------------------------------------------------
1254
+ /**
1255
+ * Request: a document leaves as gql: and its shape, anything else as its form; every
1256
+ * value is recorded for the gate. Response: with the gate open, a GraphQL name passing
1257
+ * the name rule leaves as gql: and the name; anything else as its form.
1258
+ */
1259
+ graphqlStr(s) {
1260
+ if (!this.resp) {
1261
+ this.gqlSeen = true;
1262
+ const r = (0, graphql_js_1.gqlShape)(s);
1263
+ if (r === null) {
1264
+ this.gqlNonIntro = true;
1265
+ return this.shape(s);
1266
+ }
1267
+ if (!r.introspection)
1268
+ this.gqlNonIntro = true;
1269
+ return 'gql:' + r.shape;
1270
+ }
1271
+ if (this.gqlGate() && (0, graphql_js_1.gqlIsName)(s) && (0, graphql_js_1.gqlNameSafe)(s))
1272
+ return 'gql:' + s;
1273
+ return this.shape(s);
1274
+ }
1275
+ /** 5.7.2: open iff the request had a graphql value and all of them were introspection-only documents. */
1276
+ gqlGate() {
1277
+ return this.gqlSeen && !this.gqlNonIntro;
1278
+ }
1279
+ /** A request value under a graphql rule without a usable string closes the gate. */
1280
+ gqlUnusable(a) {
1281
+ if (a?.action === 'graphql' && !this.resp) {
1282
+ this.gqlSeen = true;
1283
+ this.gqlNonIntro = true;
1284
+ }
1285
+ }
1286
+ discAdd(path, value) {
1287
+ if (!this.disc)
1288
+ return;
1289
+ const prev = this.disc.get(path);
1290
+ if (prev === undefined || Buffer.compare(value, prev) < 0)
1291
+ this.disc.set(path, value);
1292
+ }
1293
+ // --- named parts (8.1-8.3, 9.1 form) ---------------------------------------------
1294
+ namedRule(groups, section, name) {
1295
+ if (name === null)
1296
+ return undefined;
1297
+ for (const g of groups) {
1298
+ if (!g)
1299
+ continue;
1300
+ if (section === 'form') {
1301
+ let best;
1302
+ for (const r of g.body) {
1303
+ if (r.steps.length !== 1)
1304
+ continue;
1305
+ const st = r.steps[0];
1306
+ if (!(st.t === 'any' || (st.t === 'name' && st.name === name)))
1307
+ continue;
1308
+ if (!best || r.names > best.names || (r.names === best.names && (0, utf8_js_1.cmpUtf8)(r.path, best.path) < 0))
1309
+ best = r;
1310
+ }
1311
+ if (best)
1312
+ return best.action;
1313
+ continue;
1314
+ }
1315
+ const a = g[section].get(name);
1316
+ if (a)
1317
+ return a;
1318
+ }
1319
+ return undefined;
1320
+ }
1321
+ namedMap(entries, groups, section, disc) {
1322
+ const order = [];
1323
+ const byName = new Map();
1324
+ for (const e of entries) {
1325
+ let l = byName.get(e.out);
1326
+ if (!l) {
1327
+ l = [];
1328
+ byName.set(e.out, l);
1329
+ order.push(e.out);
1330
+ }
1331
+ l.push(e);
1332
+ }
1333
+ const res = new Map();
1334
+ // After 8.0 every header name has exactly one value: max_array applies to cookies,
1335
+ // query and form values only (8.2, 8.3).
1336
+ const maxArr = section === 'headers' ? Number.MAX_SAFE_INTEGER : this.maxArray;
1337
+ for (const name of order.slice(0, MAX_NAMES)) {
1338
+ const list = byName.get(name);
1339
+ const vals = [];
1340
+ // 15: named parts use the output name ({*} when collapsed); form fields use tt-path style.
1341
+ const dpath = this.disc ? disc + '.' + (section === 'form' && name === '{*}' ? '*' : name) : null;
1342
+ for (let i = 0; i < Math.min(list.length, maxArr); i++) {
1343
+ const e = list[i];
1344
+ if (e.token !== undefined) {
1345
+ vals.push(e.token); // 22.3: a file part, whatever the rule; no discovery entry
1346
+ continue;
1347
+ }
1348
+ // Parts without "=" have no name: always drop (no rule can select them).
1349
+ const a = e.noEq ? undefined : this.namedRule(groups, section, e.orig);
1350
+ vals.push(this.strAction((0, utf8_js_1.decodeUtf8)(e.value), a, false, dpath));
1351
+ }
1352
+ if (list.length > maxArr)
1353
+ vals.push('trunc:n:' + (0, formats_js_1.lenClass)(list.length));
1354
+ res.set(name, vals);
1355
+ }
1356
+ if (order.length > MAX_NAMES)
1357
+ res.set('{trunc}', ['trunc:keys:' + (0, formats_js_1.lenClass)(order.length)]);
1358
+ return res;
1359
+ }
1360
+ }
1361
+ /**
1362
+ * 9.0 decoding, one level, with an output cap (A-GZIP-STRICT): returns the decoded bytes,
1363
+ * 'cap' when the output reaches `cap` bytes, or null on any error (truncated stream,
1364
+ * bytes after the end of the stream, corrupt data).
1365
+ */
1366
+ function decodeBody(data, zlibFormat, cap) {
1367
+ try {
1368
+ const fn = zlibFormat ? node_zlib_1.inflateSync : node_zlib_1.gunzipSync;
1369
+ const res = fn(data, { maxOutputLength: cap, info: true });
1370
+ if (res.buffer.length >= cap)
1371
+ return 'cap';
1372
+ if (res.engine.bytesWritten !== data.length)
1373
+ return null;
1374
+ return res.buffer;
1375
+ }
1376
+ catch (e) {
1377
+ if (e.code === 'ERR_BUFFER_TOO_LARGE')
1378
+ return 'cap';
1379
+ return null;
1380
+ }
1381
+ }
1382
+ /** tt-cjson object from already serialized members (keys sorted by bytes). */
1383
+ function composeCanonical(parts) {
1384
+ const keys = [...parts.keys()].sort(utf8_js_1.cmpUtf8);
1385
+ let s = '{';
1386
+ for (let i = 0; i < keys.length; i++) {
1387
+ if (i > 0)
1388
+ s += ',';
1389
+ s += (0, json_js_1.writeString)(keys[i]) + ':' + parts.get(keys[i]);
1390
+ }
1391
+ return s + '}';
1392
+ }
1393
+ /** Winning body rule for the path $ (5.0): first group with a rule without steps. */
1394
+ function rootRule(groups) {
1395
+ for (const g of groups) {
1396
+ let best;
1397
+ for (const r of g?.body ?? []) {
1398
+ if (r.steps.length !== 0)
1399
+ continue;
1400
+ if (!best || (0, utf8_js_1.cmpUtf8)(r.path, best.path) < 0)
1401
+ best = r;
1402
+ }
1403
+ if (best)
1404
+ return best.action;
1405
+ }
1406
+ return undefined;
1407
+ }
1408
+ function replaceBody(side) {
1409
+ if (!(side instanceof Map) || !side.has('body'))
1410
+ return false;
1411
+ side.set('body', 'trunc:size');
1412
+ return true;
1413
+ }
1414
+ /** 22.2 point 8: keys of an XML tree: #text, @ + attribute name, element name. */
1415
+ function xmlKeyIsLiteral(k) {
1416
+ if (k === '#text')
1417
+ return true;
1418
+ return (0, formats_js_1.isLiteralName)(k.charCodeAt(0) === 0x40 ? k.slice(1) : k);
1419
+ }
1420
+ function jstr(s) {
1421
+ return { k: 's', s, bad: false };
1422
+ }
1423
+ /** XML tree value (22.2 point 7) as a JSON node: every leaf is a string. */
1424
+ function xmlToNode(v) {
1425
+ if (typeof v === 'string')
1426
+ return jstr(v);
1427
+ if (Array.isArray(v))
1428
+ return { k: 'a', a: v.map(xmlToNode) };
1429
+ return { k: 'o', m: v.members.map(([k, x]) => [jstr(k), xmlToNode(x)]) };
1430
+ }
1431
+ function dedupe(m) {
1432
+ if (m.length < 2)
1433
+ return m;
1434
+ const last = new Map();
1435
+ m.forEach(([k], i) => last.set(k.s, i));
1436
+ if (last.size === m.length)
1437
+ return m;
1438
+ return m.filter(([k], i) => last.get(k.s) === i);
1439
+ }
1440
+ function cookiePart(piece, out) {
1441
+ const eq = piece.indexOf(0x3d);
1442
+ if (eq < 0)
1443
+ return;
1444
+ const nb = piece.subarray(0, eq);
1445
+ const name = (0, utf8_js_1.decodeUtf8)(nb);
1446
+ out.push({
1447
+ orig: name,
1448
+ out: name !== null && (0, formats_js_1.isLiteralName)(name) ? name : '{*}',
1449
+ value: piece.subarray(eq + 1),
1450
+ noEq: false,
1451
+ });
1452
+ }
1453
+ /** 8.3 / 9.1: query string or form body into named entries. */
1454
+ function parseUrlencoded(raw) {
1455
+ const out = [];
1456
+ for (const part of (0, utf8_js_1.splitBytes)(raw, 0x26)) {
1457
+ if (part.length === 0)
1458
+ continue;
1459
+ const eq = part.indexOf(0x3d);
1460
+ if (eq < 0) {
1461
+ out.push({ orig: null, out: '{*}', value: (0, utf8_js_1.pctDecode)(part, true), noEq: true });
1462
+ continue;
1463
+ }
1464
+ const name = (0, utf8_js_1.decodeUtf8)((0, utf8_js_1.pctDecode)(part.subarray(0, eq), true));
1465
+ out.push({
1466
+ orig: name,
1467
+ out: name !== null && (0, formats_js_1.isLiteralName)(name) ? name : '{*}',
1468
+ value: (0, utf8_js_1.pctDecode)(part.subarray(eq + 1), true),
1469
+ noEq: false,
1470
+ });
1471
+ }
1472
+ return out;
1473
+ }