@foam-ai/node 0.1.0-alpha.4 → 0.1.0-alpha.6

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 (44) hide show
  1. package/README.md +393 -214
  2. package/dist/before-send.d.ts +16 -0
  3. package/dist/before-send.js +41 -0
  4. package/dist/constants.d.ts +10 -8
  5. package/dist/constants.js +19 -6
  6. package/dist/endpoint.d.ts +2 -0
  7. package/dist/endpoint.js +11 -0
  8. package/dist/exporters.d.ts +11 -3
  9. package/dist/exporters.js +127 -53
  10. package/dist/index.d.ts +7 -2
  11. package/dist/index.js +8 -1
  12. package/dist/ingest.d.ts +8 -2
  13. package/dist/ingest.js +28 -16
  14. package/dist/init.d.ts +7 -3
  15. package/dist/init.js +85 -37
  16. package/dist/instrumentations.js +25 -3
  17. package/dist/logs.d.ts +10 -0
  18. package/dist/logs.js +61 -0
  19. package/dist/network-capture/collector.js +168 -69
  20. package/dist/network-capture/http.d.ts +0 -1
  21. package/dist/network-capture/http.js +23 -25
  22. package/dist/network-capture/index.js +10 -0
  23. package/dist/network-capture/redact.d.ts +9 -0
  24. package/dist/network-capture/redact.js +401 -0
  25. package/dist/network-capture/undici.d.ts +0 -4
  26. package/dist/network-capture/undici.js +55 -22
  27. package/dist/otlp.d.ts +8 -0
  28. package/dist/otlp.js +46 -0
  29. package/dist/propagation.d.ts +1 -1
  30. package/dist/propagation.js +6 -4
  31. package/dist/redaction-keys.d.ts +3 -0
  32. package/dist/redaction-keys.js +421 -0
  33. package/dist/redaction.d.ts +24 -0
  34. package/dist/redaction.js +270 -0
  35. package/dist/report.d.ts +9 -0
  36. package/dist/report.js +56 -0
  37. package/dist/state.d.ts +11 -4
  38. package/dist/state.js +26 -9
  39. package/dist/traces.d.ts +1 -0
  40. package/dist/traces.js +21 -0
  41. package/dist/utils.js +1 -1
  42. package/package.json +1 -1
  43. package/dist/diagnostics.d.ts +0 -13
  44. package/dist/diagnostics.js +0 -83
@@ -46,7 +46,7 @@ function observeDirectHeaders(args, headers) {
46
46
  append(rawKey, value);
47
47
  }
48
48
  }
49
- /** Replaces a method with a wrapped version; returns an undo that restores it unless someone else re-patched it. */
49
+ // pcga11:Replaces a method with a wrapped version; returns an undo that restores it unless someone else re-patched it.
50
50
  function swapMethod(target, key, wrap) {
51
51
  const original = target[key];
52
52
  const wrapped = wrap(original);
@@ -56,7 +56,7 @@ function swapMethod(target, key, wrap) {
56
56
  target[key] = original;
57
57
  };
58
58
  }
59
- /** Wraps writeHead (when present) to record inline headers; returns an undo. */
59
+ // Wraps writeHead (when present) to record inline headers; returns an undo.
60
60
  function swapWriteHead(target, directHeaders, afterWriteHead) {
61
61
  const original = target.writeHead;
62
62
  if (!original)
@@ -75,7 +75,7 @@ function swapWriteHead(target, directHeaders, afterWriteHead) {
75
75
  target.writeHead = original;
76
76
  };
77
77
  }
78
- /** Attaches one-shot listeners; returns an undo that removes all of them. */
78
+ // Attaches one-shot listeners; returns an undo that removes all of them.
79
79
  function listenOnce(target, listeners) {
80
80
  for (const [event, listener] of listeners)
81
81
  target.once(event, listener);
@@ -97,11 +97,13 @@ function observeWritableHeaders(message, span, direction, directHeaders) {
97
97
  });
98
98
  const restoreWriteHead = swapWriteHead(target, directHeaders, applyHeaders);
99
99
  let removeListeners = () => { };
100
- const cleanup = () => {
100
+ // Runs inside the customer's EventEmitter emit; a throw here would surface
101
+ // as an uncaughtException in their app.
102
+ const cleanup = () => (0, utils_js_1.safely)(() => {
101
103
  restoreEnd();
102
104
  restoreWriteHead();
103
105
  removeListeners();
104
- };
106
+ });
105
107
  removeListeners = listenOnce(target, [
106
108
  ["finish", cleanup],
107
109
  ["close", cleanup],
@@ -121,8 +123,8 @@ function observeReadable(message, collector) {
121
123
  });
122
124
  return result;
123
125
  });
124
- const onComplete = () => collector.finalize(true);
125
- const onIncomplete = () => collector.finalize(false);
126
+ const onComplete = () => (0, utils_js_1.safely)(() => collector.finalize(true));
127
+ const onIncomplete = () => (0, utils_js_1.safely)(() => collector.finalize(false));
126
128
  const removeListeners = listenOnce(target, [
127
129
  ["end", onComplete],
128
130
  ["aborted", onIncomplete],
@@ -156,8 +158,8 @@ function observeWritable(message, collector, directHeaders) {
156
158
  }
157
159
  });
158
160
  const restoreWriteHead = swapWriteHead(target, directHeaders);
159
- const onComplete = () => collector.finalize(true);
160
- const onIncomplete = () => collector.finalize(false);
161
+ const onComplete = () => (0, utils_js_1.safely)(() => collector.finalize(true));
162
+ const onIncomplete = () => (0, utils_js_1.safely)(() => collector.finalize(false));
161
163
  const removeListeners = listenOnce(target, [
162
164
  ["finish", onComplete],
163
165
  ["close", onIncomplete],
@@ -170,19 +172,14 @@ function observeWritable(message, collector, directHeaders) {
170
172
  removeListeners();
171
173
  });
172
174
  }
173
- /**
174
- * Instruments one HTTP message (either side, either direction) on a span:
175
- * records headers as `http.<direction>.header.*` attributes and, when enabled,
176
- * captures the body via a BodyCollector.
177
- */
175
+ // pcga11: Instruments one HTTP message on a span:
176
+ // - records headers as `http.<direction>.header.*` attributes
177
+ // - captures the body via a BodyCollector when enabled.
178
178
  function hookMessage(span, message, direction, bodyCaptureEnabled) {
179
- // Duck-type: only outgoing messages (ClientRequest/ServerResponse) have write/end.
179
+ // pcga11: Duck-type: only outgoing messages (ClientRequest/ServerResponse) have write/end.
180
180
  if ("write" in message && "end" in message) {
181
- // A writable request is an outgoing client call; a writable response is served by us.
182
181
  const side = direction === "request" ? "client" : "server";
183
- // Collects headers passed inline to writeHead(), which don't show up in getHeaders().
184
182
  const directHeaders = new Map();
185
- // Eagerly record whatever headers exist right now; late-set headers are handled below.
186
183
  (0, collector_js_1.setHeaderAttributes)(span, direction, outgoingHeaders(message, directHeaders));
187
184
  const collector = bodyCaptureEnabled
188
185
  ? // Headers are passed as a lazy callback so late-set headers are seen at finalize time.
@@ -192,32 +189,33 @@ function hookMessage(span, message, direction, bodyCaptureEnabled) {
192
189
  if (collector)
193
190
  observeWritable(message, collector, directHeaders);
194
191
  else if (span.isRecording()) {
195
- // No body capture: still patch end/writeHead so late-set headers land on the span.
196
192
  observeWritableHeaders(message, span, direction, directHeaders);
197
193
  }
198
194
  }
199
195
  else {
200
- // Inverse of the writable case: a request arriving at us means we are the server.
201
196
  const side = direction === "request" ? "server" : "client";
202
- // Incoming message: headers are fully parsed by Node before the hook runs, so snapshot once.
203
197
  const headers = incomingHeaders(message);
204
198
  (0, collector_js_1.setHeaderAttributes)(span, direction, headers);
205
199
  const collector = bodyCaptureEnabled
206
200
  ? collector_js_1.BodyCollector.create(span, side, direction, () => headers)
207
201
  : undefined;
208
- // Patch the stream's push() to copy body chunks as they arrive from the socket.
209
202
  if (collector)
210
203
  observeReadable(message, collector);
211
204
  }
212
205
  }
213
- /** Builds OTel HTTP request/response hooks that record headers as span attributes and stream bodies as log events. */
206
+ // pcga11: Creates network capture hooks for node:http / node:https. Wraps instance
207
+ // methods: push(), write(), and end() on IncomingMessage / ClientRequest /
208
+ // ServerResponse. This works for all Node versions Foam supports.
214
209
  function createNetworkCaptureHooks(bodyCaptureEnabled = true) {
210
+ // OTel guards hook errors today, but Foam's no-crash guarantee must not
211
+ // depend on that: any failure here is swallowed and the request proceeds
212
+ // as if Foam were not installed.
215
213
  return {
216
214
  requestHook(span, request) {
217
- hookMessage(span, request, "request", bodyCaptureEnabled);
215
+ (0, utils_js_1.safely)(() => hookMessage(span, request, "request", bodyCaptureEnabled));
218
216
  },
219
217
  responseHook(span, response) {
220
- hookMessage(span, response, "response", bodyCaptureEnabled);
218
+ (0, utils_js_1.safely)(() => hookMessage(span, response, "response", bodyCaptureEnabled));
221
219
  },
222
220
  };
223
221
  }
@@ -1,6 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createUndiciNetworkCaptureHooks = exports.createNetworkCaptureHooks = void 0;
4
+ //
5
+ // Advanced body capture uses three adapters across Foam's Node range:
6
+ // 1. node:http / node:https — instance push(), write(), and end() wrapping
7
+ // (every supported Node version).
8
+ // 2. Undici legacy — request:create plus instance onBodySent / onData
9
+ // (bundled fetch on Node 18.19, 20, 22, and 24.0–24.3, whose Undici is
10
+ // older than 7.11).
11
+ // 3. Undici modern — bodyChunkSent / bodyChunkReceived channels (Undici
12
+ // 7.11+, including Node 24.4+ and a separately installed undici). Future
13
+ // Node is chosen by capability: modern channels win when they fire.
4
14
  var http_js_1 = require("./http.js");
5
15
  Object.defineProperty(exports, "createNetworkCaptureHooks", { enumerable: true, get: function () { return http_js_1.createNetworkCaptureHooks; } });
6
16
  var undici_js_1 = require("./undici.js");
@@ -0,0 +1,9 @@
1
+ export interface RedactionResult {
2
+ readonly text: string;
3
+ readonly changed: boolean;
4
+ }
5
+ export declare function isSensitiveBodyKey(key: string): boolean;
6
+ export declare function isNdjsonMediaType(media: string): boolean;
7
+ export declare function isJsonSequenceMediaType(media: string): boolean;
8
+ export declare function isTextualBodyMediaType(media: string): boolean;
9
+ export declare function redactBodyText(text: string, contentType: unknown): RedactionResult;
@@ -0,0 +1,401 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isSensitiveBodyKey = isSensitiveBodyKey;
4
+ exports.isNdjsonMediaType = isNdjsonMediaType;
5
+ exports.isJsonSequenceMediaType = isJsonSequenceMediaType;
6
+ exports.isTextualBodyMediaType = isTextualBodyMediaType;
7
+ exports.redactBodyText = redactBodyText;
8
+ const constants_js_1 = require("../constants.js");
9
+ const redaction_js_1 = require("../redaction.js");
10
+ const NDJSON_MEDIA_TYPES = new Set([
11
+ "application/json-stream",
12
+ "application/jsonl",
13
+ "application/jsonlines",
14
+ "application/ndjson",
15
+ "application/stream+json",
16
+ "application/x-json-stream",
17
+ "application/x-jsonlines",
18
+ "application/x-ldjson",
19
+ "application/x-ndjson",
20
+ ]);
21
+ const JSON_SEQUENCE_MEDIA_TYPES = new Set([
22
+ "application/json-seq",
23
+ "application/x-json-seq",
24
+ ]);
25
+ const XML_MEDIA_TYPES = new Set([
26
+ "application/xml",
27
+ "text/html",
28
+ "text/xml",
29
+ ]);
30
+ const YAML_MEDIA_TYPES = new Set([
31
+ "application/x-yaml",
32
+ "application/yaml",
33
+ "text/x-yaml",
34
+ "text/yaml",
35
+ ]);
36
+ function rawHeader(value) {
37
+ const first = Array.isArray(value) ? value[0] : value;
38
+ return typeof first === "string" ? first : "";
39
+ }
40
+ function parseContentType(value) {
41
+ const raw = rawHeader(value);
42
+ const media = (raw.split(";", 1)[0] ?? "").trim().toLowerCase();
43
+ const params = new Map();
44
+ const pattern = /;\s*([^=;\s]+)\s*=\s*(?:"([^"]*)"|([^;]*))/g;
45
+ let match;
46
+ while ((match = pattern.exec(raw)) !== null) {
47
+ params.set(match[1].toLowerCase(), (match[2] ?? match[3] ?? "").trim());
48
+ }
49
+ return { media, params };
50
+ }
51
+ function isSensitiveBodyKey(key) {
52
+ return (0, redaction_js_1.keyMatch)(key) !== undefined;
53
+ }
54
+ function isNdjsonMediaType(media) {
55
+ return (NDJSON_MEDIA_TYPES.has(media) ||
56
+ media.endsWith("+ndjson") ||
57
+ media.endsWith("/ndjson"));
58
+ }
59
+ function isJsonSequenceMediaType(media) {
60
+ return JSON_SEQUENCE_MEDIA_TYPES.has(media) || media.endsWith("+json-seq");
61
+ }
62
+ function isTextualBodyMediaType(media) {
63
+ return (media.startsWith("text/") ||
64
+ media.startsWith("multipart/") ||
65
+ media.endsWith("/json") ||
66
+ media.endsWith("+json") ||
67
+ media.endsWith("/xml") ||
68
+ media.endsWith("+xml") ||
69
+ media.endsWith("/yaml") ||
70
+ media.endsWith("+yaml") ||
71
+ isNdjsonMediaType(media) ||
72
+ isJsonSequenceMediaType(media) ||
73
+ media === "application/x-www-form-urlencoded");
74
+ }
75
+ function redactNode(node, state) {
76
+ if (typeof node === "string") {
77
+ const value = (0, redaction_js_1.redactString)(node);
78
+ if (value !== node)
79
+ state.changed = true;
80
+ return value;
81
+ }
82
+ if (Array.isArray(node)) {
83
+ return node.map((item) => redactNode(item, state));
84
+ }
85
+ if (node !== null && typeof node === "object") {
86
+ const result = {};
87
+ for (const [key, value] of Object.entries(node)) {
88
+ const match = (0, redaction_js_1.keyMatch)(key);
89
+ if (match && value !== null && value !== undefined) {
90
+ result[key] = (0, redaction_js_1.maskForMatch)(match, value);
91
+ state.changed = true;
92
+ }
93
+ else {
94
+ result[key] = redactNode(value, state);
95
+ }
96
+ }
97
+ return result;
98
+ }
99
+ return node;
100
+ }
101
+ const JSON_PAIR_PATTERN = /"((?:[^"\\]|\\.)*)"(\s*:\s*)("(?:[^"\\]|\\.)*"?|-?\d[\d.eE+-]*|true|false|null)/g;
102
+ function fallbackMask(kind) {
103
+ return kind === "secret" ? constants_js_1.FULL_MASK : constants_js_1.REDACTED_VALUE;
104
+ }
105
+ function redactJsonFallback(text) {
106
+ let changed = false;
107
+ const redacted = text.replace(JSON_PAIR_PATTERN, (match, key, separator) => {
108
+ const kind = (0, redaction_js_1.keyMatch)(key);
109
+ if (!kind)
110
+ return match;
111
+ changed = true;
112
+ return `"${key}"${separator}"${fallbackMask(kind)}"`;
113
+ });
114
+ return changed ? { text: redacted, changed } : { text, changed: false };
115
+ }
116
+ function redactJson(text) {
117
+ let parsed;
118
+ try {
119
+ parsed = JSON.parse(text);
120
+ }
121
+ catch {
122
+ return redactJsonFallback(text);
123
+ }
124
+ const state = { changed: false };
125
+ const redacted = redactNode(parsed, state);
126
+ return state.changed
127
+ ? { text: JSON.stringify(redacted), changed: true }
128
+ : { text, changed: false };
129
+ }
130
+ function redactByLine(text, redactLine) {
131
+ let changed = false;
132
+ const lines = text.split("\n").map((raw) => {
133
+ const hasCr = raw.endsWith("\r");
134
+ const line = hasCr ? raw.slice(0, -1) : raw;
135
+ if (!line)
136
+ return raw;
137
+ const result = redactLine(line);
138
+ if (!result.changed)
139
+ return raw;
140
+ changed = true;
141
+ return hasCr ? `${result.text}\r` : result.text;
142
+ });
143
+ return changed
144
+ ? { text: lines.join("\n"), changed: true }
145
+ : { text, changed: false };
146
+ }
147
+ function redactNdjson(text) {
148
+ return redactByLine(text, redactJson);
149
+ }
150
+ function redactJsonSequence(text) {
151
+ if (!text.includes("\u001e"))
152
+ return redactJson(text);
153
+ let changed = false;
154
+ const segments = text.split("\u001e");
155
+ const redacted = segments.map((segment, index) => {
156
+ if (index === 0 || !segment)
157
+ return segment;
158
+ const ending = segment.match(/(?:\r\n|\n|\r)$/)?.[0] ?? "";
159
+ const document = ending ? segment.slice(0, -ending.length) : segment;
160
+ const result = redactJson(document);
161
+ if (!result.changed)
162
+ return segment;
163
+ changed = true;
164
+ return `${result.text}${ending}`;
165
+ });
166
+ return changed
167
+ ? { text: redacted.join("\u001e"), changed: true }
168
+ : { text, changed: false };
169
+ }
170
+ function redactSseBlock(block) {
171
+ const lineEnding = block.match(/\r\n|\n|\r/)?.[0] ?? "\n";
172
+ const lines = block.split(/\r\n|\n|\r/);
173
+ const data = [];
174
+ lines.forEach((line, index) => {
175
+ const match = /^(data: ?)(.*)$/.exec(line);
176
+ if (match) {
177
+ data.push({ index, prefix: match[1], value: match[2] });
178
+ }
179
+ });
180
+ if (data.length === 0)
181
+ return { text: block, changed: false };
182
+ const payload = data.map((entry) => entry.value).join("\n");
183
+ const json = redactJson(payload);
184
+ const plain = json.changed ? json : redactPlainText(payload);
185
+ if (!plain.changed)
186
+ return { text: block, changed: false };
187
+ lines[data[0].index] = `${data[0].prefix}${plain.text}`;
188
+ for (let index = data.length - 1; index >= 1; index -= 1) {
189
+ lines.splice(data[index].index, 1);
190
+ }
191
+ return { text: lines.join(lineEnding), changed: true };
192
+ }
193
+ function redactSse(text) {
194
+ const separator = /(\r\n\r\n|\n\n|\r\r)/;
195
+ const parts = text.split(separator);
196
+ let changed = false;
197
+ for (let index = 0; index < parts.length; index += 2) {
198
+ const result = redactSseBlock(parts[index]);
199
+ if (result.changed) {
200
+ parts[index] = result.text;
201
+ changed = true;
202
+ }
203
+ }
204
+ return changed
205
+ ? { text: parts.join(""), changed: true }
206
+ : { text, changed: false };
207
+ }
208
+ function redactForm(text) {
209
+ const params = new URLSearchParams(text);
210
+ let changed = false;
211
+ for (const key of new Set(params.keys())) {
212
+ if (!(0, redaction_js_1.keyMatch)(key) && !(0, redaction_js_1.isSensitiveQueryKey)(key))
213
+ continue;
214
+ params.set(key, constants_js_1.REDACTED_VALUE);
215
+ changed = true;
216
+ }
217
+ return changed
218
+ ? { text: params.toString(), changed: true }
219
+ : { text, changed: false };
220
+ }
221
+ function localName(name) {
222
+ const separator = name.lastIndexOf(":");
223
+ return separator < 0 ? name : name.slice(separator + 1);
224
+ }
225
+ function escapePattern(value) {
226
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
227
+ }
228
+ function redactXmlElements(text) {
229
+ let changed = false;
230
+ let cursor = 0;
231
+ let output = "";
232
+ const opening = /<([A-Za-z_][\w:.-]*)(?:\s[^>]*)?>/g;
233
+ let match;
234
+ while ((match = opening.exec(text)) !== null) {
235
+ if (match[0].endsWith("/>"))
236
+ continue;
237
+ const kind = (0, redaction_js_1.keyMatch)(localName(match[1]));
238
+ if (!kind)
239
+ continue;
240
+ const closing = new RegExp(`</${escapePattern(match[1])}\\s*>`, "g");
241
+ closing.lastIndex = opening.lastIndex;
242
+ const close = closing.exec(text);
243
+ output += text.slice(cursor, opening.lastIndex);
244
+ if (!close) {
245
+ output += fallbackMask(kind);
246
+ cursor = text.length;
247
+ changed = true;
248
+ break;
249
+ }
250
+ const value = text.slice(opening.lastIndex, close.index);
251
+ output += `${value.includes("<") ? fallbackMask(kind) : (0, redaction_js_1.maskForMatch)(kind, value)}${close[0]}`;
252
+ cursor = closing.lastIndex;
253
+ opening.lastIndex = cursor;
254
+ changed = true;
255
+ }
256
+ if (!changed)
257
+ return { text, changed: false };
258
+ return { text: output + text.slice(cursor), changed: true };
259
+ }
260
+ function redactXml(text) {
261
+ const elements = redactXmlElements(text);
262
+ let changed = elements.changed;
263
+ let output = elements.text;
264
+ output = output.replace(/(\s)([A-Za-z_:][\w:.-]*)(\s*=\s*)(["'])(.*?)\4/g, (match, spacing, name, separator, quote, value) => {
265
+ const kind = (0, redaction_js_1.keyMatch)(localName(name));
266
+ if (!kind)
267
+ return match;
268
+ changed = true;
269
+ return `${spacing}${name}${separator}${quote}${(0, redaction_js_1.maskForMatch)(kind, value)}${quote}`;
270
+ });
271
+ output = output.replace(/<input\b[^>]*>/gi, (tag) => {
272
+ const name = /\bname\s*=\s*(["'])(.*?)\1/i.exec(tag)?.[2];
273
+ if (!name || (!(0, redaction_js_1.keyMatch)(name) && !(0, redaction_js_1.isSensitiveQueryKey)(name)))
274
+ return tag;
275
+ const replaced = tag.replace(/(\bvalue\s*=\s*)(["'])(.*?)\2/i, `$1$2${constants_js_1.REDACTED_VALUE}$2`);
276
+ if (replaced === tag)
277
+ return tag;
278
+ changed = true;
279
+ return replaced;
280
+ });
281
+ return changed ? { text: output, changed: true } : { text, changed: false };
282
+ }
283
+ const YAML_PAIR = /^(\s*(?:-\s*)?)(["']?)([A-Za-z_][\w.-]*)\2(\s*:\s*)(.*)$/;
284
+ function redactYaml(text) {
285
+ let blockSecret = false;
286
+ const result = redactByLine(text, (line) => {
287
+ const match = YAML_PAIR.exec(line);
288
+ if (!match)
289
+ return { text: line, changed: false };
290
+ const kind = (0, redaction_js_1.keyMatch)(match[3]);
291
+ if (!kind)
292
+ return { text: line, changed: false };
293
+ if (/^[>|]/.test(match[5].trim()))
294
+ blockSecret = true;
295
+ return {
296
+ text: `${match[1]}${match[2]}${match[3]}${match[2]}${match[4]}${(0, redaction_js_1.maskForMatch)(kind, match[5].trim().replace(/^(["'])(.*)\1$/, "$2"))}`,
297
+ changed: true,
298
+ };
299
+ });
300
+ return blockSecret ? { text: constants_js_1.REDACTED_VALUE, changed: true } : result;
301
+ }
302
+ function redactPlainText(text) {
303
+ const trimmed = text.trimStart();
304
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
305
+ const json = redactJson(text);
306
+ if (json.changed)
307
+ return json;
308
+ }
309
+ const yaml = redactYaml(text);
310
+ if (yaml.changed)
311
+ return yaml;
312
+ const redacted = (0, redaction_js_1.redactString)(text);
313
+ return redacted === text
314
+ ? { text, changed: false }
315
+ : { text: redacted, changed: true };
316
+ }
317
+ function contentDispositionName(headers) {
318
+ const line = headers
319
+ .split(/\r\n|\n|\r/)
320
+ .find((header) => /^content-disposition\s*:/i.test(header));
321
+ return line ? /\bname\s*=\s*(?:"([^"]*)"|([^;\s]*))/i.exec(line)?.slice(1).find(Boolean) : undefined;
322
+ }
323
+ function partContentType(headers) {
324
+ return headers
325
+ .split(/\r\n|\n|\r/)
326
+ .find((header) => /^content-type\s*:/i.test(header))
327
+ ?.replace(/^[^:]+:\s*/, "");
328
+ }
329
+ function redactMultipart(text, boundary) {
330
+ if (!boundary || boundary.length > 70)
331
+ return { text, changed: false };
332
+ const marker = `--${boundary}`;
333
+ if (!text.includes(marker))
334
+ return { text, changed: false };
335
+ let changed = false;
336
+ const parts = text.split(marker);
337
+ for (let index = 1; index < parts.length; index += 1) {
338
+ const part = parts[index];
339
+ if (part.startsWith("--"))
340
+ continue;
341
+ const leading = part.match(/^(?:\r\n|\n|\r)/)?.[0] ?? "";
342
+ const content = leading ? part.slice(leading.length) : part;
343
+ const separator = /\r\n\r\n|\n\n|\r\r/.exec(content);
344
+ if (!separator?.index)
345
+ continue;
346
+ const headers = content.slice(0, separator.index);
347
+ const bodyWithEnding = content.slice(separator.index + separator[0].length);
348
+ const ending = bodyWithEnding.match(/(?:\r\n|\n|\r)$/)?.[0] ?? "";
349
+ const body = ending
350
+ ? bodyWithEnding.slice(0, -ending.length)
351
+ : bodyWithEnding;
352
+ const name = contentDispositionName(headers);
353
+ let result;
354
+ if (name && ((0, redaction_js_1.keyMatch)(name) || (0, redaction_js_1.isSensitiveQueryKey)(name))) {
355
+ result = { text: constants_js_1.REDACTED_VALUE, changed: true };
356
+ }
357
+ else if (/\bfilename\s*=/i.test(headers)) {
358
+ result = { text: body, changed: false };
359
+ }
360
+ else {
361
+ result = redactBodyText(body, partContentType(headers) ?? "text/plain");
362
+ }
363
+ if (!result.changed)
364
+ continue;
365
+ parts[index] = `${leading}${headers}${separator[0]}${result.text}${ending}`;
366
+ changed = true;
367
+ }
368
+ return changed
369
+ ? { text: parts.join(marker), changed: true }
370
+ : { text, changed: false };
371
+ }
372
+ function redactBodyText(text, contentType) {
373
+ const { media, params } = parseContentType(contentType);
374
+ if (media === "application/x-www-form-urlencoded")
375
+ return redactForm(text);
376
+ if (media === "text/event-stream")
377
+ return redactSse(text);
378
+ if (isJsonSequenceMediaType(media))
379
+ return redactJsonSequence(text);
380
+ if (isNdjsonMediaType(media))
381
+ return redactNdjson(text);
382
+ if (media.endsWith("/json") || media.endsWith("+json"))
383
+ return redactJson(text);
384
+ if (media.startsWith("multipart/")) {
385
+ return redactMultipart(text, params.get("boundary") ?? "");
386
+ }
387
+ if (XML_MEDIA_TYPES.has(media) ||
388
+ media.endsWith("/xml") ||
389
+ media.endsWith("+xml")) {
390
+ return redactXml(text);
391
+ }
392
+ if (YAML_MEDIA_TYPES.has(media) ||
393
+ media.endsWith("/yaml") ||
394
+ media.endsWith("+yaml")) {
395
+ return redactYaml(text);
396
+ }
397
+ if (media.startsWith("text/") || params.has("charset")) {
398
+ return redactPlainText(text);
399
+ }
400
+ return { text, changed: false };
401
+ }
@@ -1,8 +1,4 @@
1
1
  import type { RequestHookFunction, ResponseHookFunction } from "@opentelemetry/instrumentation-undici";
2
- /**
3
- * Builds OTel Undici/fetch hooks. Headers use the same allowlist as Node HTTP.
4
- * Bodies are copied from diagnostics_channel chunks — never from fetch streams.
5
- */
6
2
  export declare function createUndiciNetworkCaptureHooks(bodyCaptureEnabled?: boolean): {
7
3
  requestHook: RequestHookFunction;
8
4
  responseHook: ResponseHookFunction;