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