@foam-ai/node 0.1.0-alpha.1 → 0.1.0-alpha.11

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 (64) hide show
  1. package/README.md +656 -0
  2. package/dist/before-send.d.ts +27 -0
  3. package/dist/before-send.js +46 -0
  4. package/dist/constants.d.ts +44 -0
  5. package/dist/constants.js +67 -0
  6. package/dist/endpoint.d.ts +2 -0
  7. package/dist/endpoint.js +11 -0
  8. package/dist/exporters.d.ts +34 -0
  9. package/dist/exporters.js +208 -0
  10. package/dist/index.d.ts +10 -0
  11. package/dist/index.js +28 -0
  12. package/dist/ingest.d.ts +12 -0
  13. package/dist/ingest.js +70 -0
  14. package/dist/init.d.ts +26 -0
  15. package/dist/init.js +171 -0
  16. package/dist/instrumentations.d.ts +4 -0
  17. package/dist/instrumentations.js +120 -0
  18. package/dist/library-versions.d.ts +1 -0
  19. package/dist/library-versions.js +59 -0
  20. package/dist/logs.d.ts +10 -0
  21. package/dist/logs.js +61 -0
  22. package/dist/metrics.d.ts +5 -0
  23. package/dist/metrics.js +29 -0
  24. package/dist/network-capture/collector.d.ts +27 -0
  25. package/dist/network-capture/collector.js +411 -0
  26. package/dist/network-capture/http.d.ts +5 -0
  27. package/dist/network-capture/http.js +221 -0
  28. package/dist/network-capture/index.d.ts +3 -0
  29. package/dist/network-capture/index.js +20 -0
  30. package/dist/network-capture/redact.d.ts +9 -0
  31. package/dist/network-capture/redact.js +401 -0
  32. package/dist/network-capture/support.d.ts +2 -0
  33. package/dist/network-capture/support.js +38 -0
  34. package/dist/network-capture/undici.d.ts +5 -0
  35. package/dist/network-capture/undici.js +177 -0
  36. package/dist/otlp.d.ts +8 -0
  37. package/dist/otlp.js +46 -0
  38. package/dist/propagation.d.ts +5 -0
  39. package/dist/propagation.js +47 -0
  40. package/dist/redaction-keys.d.ts +4 -0
  41. package/dist/redaction-keys.js +480 -0
  42. package/dist/redaction.d.ts +24 -0
  43. package/dist/redaction.js +281 -0
  44. package/dist/report.d.ts +9 -0
  45. package/dist/report.js +56 -0
  46. package/dist/resource.d.ts +3 -0
  47. package/dist/resource.js +24 -0
  48. package/dist/state.d.ts +26 -0
  49. package/dist/state.js +61 -0
  50. package/dist/traces.d.ts +1 -0
  51. package/dist/traces.js +21 -0
  52. package/dist/utils.d.ts +4 -0
  53. package/dist/utils.js +20 -0
  54. package/package.json +47 -19
  55. package/dist/node/src/capture-exception.d.ts +0 -9
  56. package/dist/node/src/capture-exception.js +0 -31
  57. package/dist/node/src/index.d.ts +0 -9
  58. package/dist/node/src/index.js +0 -12
  59. package/dist/node/src/init.d.ts +0 -27
  60. package/dist/node/src/init.js +0 -203
  61. package/dist/shared/constants.d.ts +0 -1
  62. package/dist/shared/constants.js +0 -4
  63. package/dist/shared/util.d.ts +0 -9
  64. package/dist/shared/util.js +0 -21
@@ -0,0 +1,411 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.BodyCollector = void 0;
37
+ exports.setHeaderAttributes = setHeaderAttributes;
38
+ const api_1 = require("@opentelemetry/api");
39
+ const api_logs_1 = require("@opentelemetry/api-logs");
40
+ const semantic_conventions_1 = require("@opentelemetry/semantic-conventions");
41
+ const node_crypto_1 = require("node:crypto");
42
+ const zlib = __importStar(require("node:zlib"));
43
+ const constants_js_1 = require("../constants.js");
44
+ const utils_js_1 = require("../utils.js");
45
+ const redact_js_1 = require("./redact.js");
46
+ const ALLOWED_HEADERS = new Set(constants_js_1.SAFE_NETWORK_HEADERS);
47
+ const ATTR = {
48
+ request: {
49
+ header: semantic_conventions_1.ATTR_HTTP_REQUEST_HEADER,
50
+ size: constants_js_1.ATTR_HTTP_REQUEST_BODY_SIZE,
51
+ content: constants_js_1.ATTR_HTTP_REQUEST_BODY_CONTENT,
52
+ captureId: constants_js_1.ATTR_FOAM_HTTP_REQUEST_BODY_CAPTURE_ID,
53
+ complete: constants_js_1.ATTR_FOAM_HTTP_REQUEST_BODY_COMPLETE,
54
+ truncated: constants_js_1.ATTR_FOAM_HTTP_REQUEST_BODY_TRUNCATED,
55
+ },
56
+ response: {
57
+ header: semantic_conventions_1.ATTR_HTTP_RESPONSE_HEADER,
58
+ size: constants_js_1.ATTR_HTTP_RESPONSE_BODY_SIZE,
59
+ content: constants_js_1.ATTR_HTTP_RESPONSE_BODY_CONTENT,
60
+ captureId: constants_js_1.ATTR_FOAM_HTTP_RESPONSE_BODY_CAPTURE_ID,
61
+ complete: constants_js_1.ATTR_FOAM_HTTP_RESPONSE_BODY_COMPLETE,
62
+ truncated: constants_js_1.ATTR_FOAM_HTTP_RESPONSE_BODY_TRUNCATED,
63
+ },
64
+ };
65
+ let activeCaptures = 0;
66
+ function captureLogger() {
67
+ return api_logs_1.logs.getLogger(constants_js_1.FOAM_DISTRO_NAME, constants_js_1.FOAM_DISTRO_VERSION);
68
+ }
69
+ function headerValues(value) {
70
+ return Array.isArray(value) ? value.map(String) : [String(value)];
71
+ }
72
+ function setHeaderAttributes(span, direction, headers) {
73
+ const attributes = {};
74
+ for (const [rawName, rawValue] of Object.entries(headers)) {
75
+ if (rawValue === undefined)
76
+ continue;
77
+ const name = rawName.toLowerCase();
78
+ if (!ALLOWED_HEADERS.has(name))
79
+ continue;
80
+ attributes[ATTR[direction].header(name)] = headerValues(rawValue);
81
+ }
82
+ if (Object.keys(attributes).length > 0)
83
+ span.setAttributes(attributes);
84
+ }
85
+ function stringEncoding(encoding) {
86
+ return typeof encoding === "string" && Buffer.isEncoding(encoding)
87
+ ? encoding
88
+ : "utf8";
89
+ }
90
+ function copyBody(value, encoding) {
91
+ if (typeof value === "string") {
92
+ return Buffer.from(value, stringEncoding(encoding));
93
+ }
94
+ if (Buffer.isBuffer(value))
95
+ return Buffer.from(value);
96
+ if (ArrayBuffer.isView(value)) {
97
+ return Buffer.from(new Uint8Array(value.buffer, value.byteOffset, value.byteLength));
98
+ }
99
+ return undefined;
100
+ }
101
+ // Byte length of a body chunk without copying it; undefined for non-body values.
102
+ function bodyByteLength(value, encoding) {
103
+ if (typeof value === "string") {
104
+ return Buffer.byteLength(value, stringEncoding(encoding));
105
+ }
106
+ if (Buffer.isBuffer(value) || ArrayBuffer.isView(value)) {
107
+ return value.byteLength;
108
+ }
109
+ return undefined;
110
+ }
111
+ function decodeBody(bytes, contentEncoding) {
112
+ if (contentEncoding === undefined)
113
+ return bytes;
114
+ const encodings = headerValues(contentEncoding)
115
+ .flatMap((value) => value.split(","))
116
+ .map((value) => value.trim().toLowerCase())
117
+ .filter((value) => value && value !== "identity");
118
+ if (encodings.length === 0)
119
+ return bytes;
120
+ const limits = { maxOutputLength: constants_js_1.NETWORK_BODY_MAX_DECODE_BYTES };
121
+ let decoded = bytes;
122
+ try {
123
+ for (const encoding of encodings.reverse()) {
124
+ if (encoding === "gzip" || encoding === "x-gzip") {
125
+ decoded = zlib.gunzipSync(decoded, limits);
126
+ }
127
+ else if (encoding === "deflate") {
128
+ try {
129
+ decoded = zlib.inflateSync(decoded, limits);
130
+ }
131
+ catch {
132
+ decoded = zlib.unzipSync(decoded, limits);
133
+ }
134
+ }
135
+ else if (encoding === "br") {
136
+ decoded = zlib.brotliDecompressSync(decoded, limits);
137
+ }
138
+ else if (encoding === "zstd") {
139
+ const decompress = zlib.zstdDecompressSync;
140
+ if (!decompress)
141
+ return undefined;
142
+ decoded = decompress(decoded, limits);
143
+ }
144
+ else {
145
+ return undefined;
146
+ }
147
+ }
148
+ return decoded;
149
+ }
150
+ catch {
151
+ return undefined;
152
+ }
153
+ }
154
+ function textEncoding(contentType) {
155
+ const raw = headerValues(contentType)[0];
156
+ if (raw === undefined)
157
+ return undefined;
158
+ const parts = raw.split(";").map((part) => part.trim());
159
+ const media = (parts[0] ?? "").toLowerCase();
160
+ const charset = parts
161
+ .find((part) => part.toLowerCase().startsWith("charset="))
162
+ ?.slice("charset=".length)
163
+ .trim()
164
+ .replace(/^["']|["']$/g, "")
165
+ .toLowerCase();
166
+ const isText = (0, redact_js_1.isTextualBodyMediaType)(media) || Boolean(charset);
167
+ if (!isText)
168
+ return undefined;
169
+ if (charset && Buffer.isEncoding(charset))
170
+ return charset;
171
+ if (charset === "iso-8859-1" || charset === "windows-1252")
172
+ return "latin1";
173
+ if (media.startsWith("multipart/"))
174
+ return "latin1";
175
+ return "utf8";
176
+ }
177
+ // Last index such that `buf.subarray(0, end)` is complete UTF-8, capped at `max`.
178
+ function utf8End(buf, max) {
179
+ const end = Math.min(max, buf.byteLength);
180
+ if (end === 0)
181
+ return 0;
182
+ let leadAt = end;
183
+ while (leadAt > 0 && (buf[leadAt - 1] & 0xc0) === 0x80)
184
+ leadAt--;
185
+ if (leadAt === 0)
186
+ return 0;
187
+ const lead = buf[leadAt - 1];
188
+ const needed = (lead & 0x80) === 0
189
+ ? 1
190
+ : (lead & 0xe0) === 0xc0
191
+ ? 2
192
+ : (lead & 0xf0) === 0xe0
193
+ ? 3
194
+ : (lead & 0xf8) === 0xf0
195
+ ? 4
196
+ : Number.POSITIVE_INFINITY;
197
+ return end - (leadAt - 1) >= needed ? end : leadAt - 1;
198
+ }
199
+ function cappedBody(buf) {
200
+ if (buf.byteLength <= constants_js_1.NETWORK_BODY_MAX_CAPTURE_BYTES)
201
+ return buf;
202
+ return buf.subarray(0, constants_js_1.NETWORK_BODY_MAX_CAPTURE_BYTES);
203
+ }
204
+ function decodeText(decoded, encoding) {
205
+ const limited = cappedBody(decoded);
206
+ if (encoding === "utf8" || encoding === "utf-8") {
207
+ return limited.subarray(0, utf8End(limited, limited.byteLength)).toString(encoding);
208
+ }
209
+ return limited.toString(encoding);
210
+ }
211
+ class BodyCollector {
212
+ span;
213
+ context;
214
+ side;
215
+ direction;
216
+ headers;
217
+ id = (0, node_crypto_1.randomUUID)();
218
+ attr;
219
+ chunks = [];
220
+ cleanups = [];
221
+ deadline;
222
+ observedBytes = 0;
223
+ capturedBytes = 0;
224
+ truncated = false;
225
+ finalized = false;
226
+ constructor(span, context, side, direction, headers) {
227
+ this.span = span;
228
+ this.context = context;
229
+ this.side = side;
230
+ this.direction = direction;
231
+ this.headers = headers;
232
+ this.attr = ATTR[direction];
233
+ activeCaptures += 1;
234
+ this.deadline = setTimeout(() => this.finalize(false), constants_js_1.NETWORK_CAPTURE_DEADLINE_MS);
235
+ this.deadline.unref();
236
+ this.span.setAttribute(this.attr.captureId, this.id);
237
+ }
238
+ static create(span, side, direction, headers) {
239
+ const context = api_1.trace.setSpanContext(api_1.ROOT_CONTEXT, span.spanContext());
240
+ if (activeCaptures >= constants_js_1.MAX_ACTIVE_CAPTURES ||
241
+ !span.isRecording() ||
242
+ (span.spanContext().traceFlags & api_1.TraceFlags.SAMPLED) !==
243
+ api_1.TraceFlags.SAMPLED ||
244
+ !captureLogger().enabled({
245
+ context,
246
+ eventName: constants_js_1.NETWORK_CAPTURE_EVENT,
247
+ severityNumber: api_logs_1.SeverityNumber.INFO,
248
+ })) {
249
+ return undefined;
250
+ }
251
+ return new BodyCollector(span, context, side, direction, headers);
252
+ }
253
+ addCleanup(cleanup) {
254
+ this.cleanups.push(cleanup);
255
+ }
256
+ observe(value, encoding) {
257
+ if (this.finalized)
258
+ return;
259
+ if (this.capturedBytes >= constants_js_1.NETWORK_BODY_MAX_CAPTURE_BYTES) {
260
+ // Past the cap every chunk is discarded; only its length is needed
261
+ // (for the observed size), so skip the per-chunk copy that would
262
+ // otherwise double the allocation traffic of large transfers.
263
+ const length = bodyByteLength(value, encoding);
264
+ if (length === undefined)
265
+ return;
266
+ this.deadline.refresh();
267
+ this.observedBytes += length;
268
+ this.truncated = true;
269
+ this.span.setAttributes({
270
+ [this.attr.size]: this.observedBytes,
271
+ [this.attr.truncated]: true,
272
+ });
273
+ return;
274
+ }
275
+ const bytes = copyBody(value, encoding);
276
+ if (!bytes)
277
+ return;
278
+ this.deadline.refresh();
279
+ this.observedBytes += bytes.byteLength;
280
+ this.span.setAttribute(this.attr.size, this.observedBytes);
281
+ const room = constants_js_1.NETWORK_BODY_MAX_CAPTURE_BYTES - this.capturedBytes;
282
+ const kept = bytes.byteLength > room ? bytes.subarray(0, room) : bytes;
283
+ this.chunks.push(kept);
284
+ this.capturedBytes += kept.byteLength;
285
+ if (kept.byteLength < bytes.byteLength) {
286
+ this.truncated = true;
287
+ this.span.setAttribute(this.attr.truncated, true);
288
+ }
289
+ }
290
+ finalize(complete) {
291
+ if (this.finalized)
292
+ return;
293
+ this.finalized = true;
294
+ clearTimeout(this.deadline);
295
+ activeCaptures -= 1;
296
+ for (const cleanup of this.cleanups.splice(0))
297
+ (0, utils_js_1.safely)(cleanup);
298
+ // Runs inside stream handlers; header snapshots may throw on destroyed messages.
299
+ (0, utils_js_1.safely)(() => {
300
+ const headers = this.headers();
301
+ setHeaderAttributes(this.span, this.direction, headers);
302
+ this.span.setAttributes({
303
+ [this.attr.complete]: complete,
304
+ [this.attr.truncated]: this.truncated,
305
+ [this.attr.size]: this.observedBytes,
306
+ });
307
+ this.emit(this.chunks.splice(0), headers, complete);
308
+ });
309
+ }
310
+ emit(buffers, headers, complete) {
311
+ let outcome;
312
+ if (this.observedBytes === 0)
313
+ outcome = "empty";
314
+ else if (complete)
315
+ outcome = "captured";
316
+ else
317
+ outcome = "incomplete";
318
+ if (outcome === "empty")
319
+ return;
320
+ let wire;
321
+ if (buffers.length === 0)
322
+ wire = undefined;
323
+ else if (buffers.length === 1)
324
+ wire = buffers[0];
325
+ else
326
+ wire = Buffer.concat(buffers);
327
+ // Span attributes cannot hold bytes (OTel AttributeValue). Text goes on
328
+ // the span; binary uses the same `http.*.body.content` name on the event.
329
+ let binaryContent;
330
+ let chunkSource = wire;
331
+ let chunkSourceIsDecodedText = false;
332
+ if (wire !== undefined) {
333
+ const decoded = decodeBody(wire, headers["content-encoding"]);
334
+ if (decoded === undefined) {
335
+ binaryContent = Uint8Array.from(cappedBody(wire));
336
+ }
337
+ else {
338
+ const encoding = textEncoding(headers["content-type"]);
339
+ if (encoding !== undefined) {
340
+ const redacted = (0, redact_js_1.redactBodyText)(decodeText(decoded, encoding), headers["content-type"]);
341
+ this.span.setAttribute(this.attr.content, redacted.text);
342
+ if (redacted.changed) {
343
+ // pcga11: Chunk events must not re-ship bytes the span content
344
+ // redacted, so the (possibly compressed) wire bytes are replaced
345
+ // with the redacted text.
346
+ chunkSource = Buffer.from(redacted.text, encoding);
347
+ chunkSourceIsDecodedText = true;
348
+ }
349
+ }
350
+ else {
351
+ binaryContent = Uint8Array.from(cappedBody(decoded));
352
+ }
353
+ }
354
+ }
355
+ const chunks = [];
356
+ if (chunkSource) {
357
+ for (let i = 0; i < chunkSource.byteLength; i += constants_js_1.NETWORK_BODY_CHUNK_BYTES) {
358
+ chunks.push(chunkSource.subarray(i, i + constants_js_1.NETWORK_BODY_CHUNK_BYTES));
359
+ }
360
+ }
361
+ const common = {
362
+ [constants_js_1.ATTR_FOAM_HTTP_BODY_ID]: this.id,
363
+ [constants_js_1.ATTR_FOAM_HTTP_BODY_SIDE]: this.side,
364
+ [constants_js_1.ATTR_FOAM_HTTP_BODY_DIRECTION]: this.direction,
365
+ [constants_js_1.ATTR_FOAM_HTTP_BODY_COMPLETE]: complete,
366
+ [constants_js_1.ATTR_FOAM_HTTP_BODY_TRUNCATED]: this.truncated,
367
+ [constants_js_1.ATTR_FOAM_HTTP_BODY_OUTCOME]: outcome,
368
+ [this.attr.size]: this.observedBytes,
369
+ [constants_js_1.ATTR_FOAM_HTTP_BODY_CAPTURED_BYTES]: chunkSourceIsDecodedText
370
+ ? (chunkSource?.byteLength ?? 0)
371
+ : this.capturedBytes,
372
+ [constants_js_1.ATTR_FOAM_HTTP_BODY_CHUNK_COUNT]: chunks.length,
373
+ };
374
+ for (const name of ["content-type", "content-encoding"]) {
375
+ if (name === "content-encoding" &&
376
+ chunkSourceIsDecodedText &&
377
+ headers[name] !== undefined) {
378
+ common[this.attr.header(name)] = ["identity"];
379
+ continue;
380
+ }
381
+ if (headers[name] !== undefined) {
382
+ common[this.attr.header(name)] = headerValues(headers[name]);
383
+ }
384
+ }
385
+ const logger = captureLogger();
386
+ const records = chunks.length === 0 ? [undefined] : chunks;
387
+ records.forEach((chunk, index) => {
388
+ logger.emit({
389
+ eventName: constants_js_1.NETWORK_CAPTURE_EVENT,
390
+ body: constants_js_1.NETWORK_CAPTURE_EVENT_BODY,
391
+ severityNumber: api_logs_1.SeverityNumber.INFO,
392
+ severityText: constants_js_1.SEVERITY_TEXT[api_logs_1.SeverityNumber.INFO],
393
+ attributes: {
394
+ ...common,
395
+ ...(chunk
396
+ ? {
397
+ [constants_js_1.ATTR_FOAM_HTTP_BODY_CHUNK_INDEX]: index,
398
+ [constants_js_1.ATTR_FOAM_HTTP_BODY_CHUNK_BYTES]: chunk.byteLength,
399
+ [constants_js_1.ATTR_FOAM_HTTP_BODY_CHUNK_CONTENT]: Uint8Array.from(chunk),
400
+ }
401
+ : {}),
402
+ ...(index === 0 && binaryContent
403
+ ? { [this.attr.content]: binaryContent }
404
+ : {}),
405
+ },
406
+ context: this.context,
407
+ });
408
+ });
409
+ }
410
+ }
411
+ exports.BodyCollector = BodyCollector;
@@ -0,0 +1,5 @@
1
+ import type { HttpRequestCustomAttributeFunction, HttpResponseCustomAttributeFunction } from "@opentelemetry/instrumentation-http";
2
+ export declare function createNetworkCaptureHooks(bodyCaptureEnabled?: boolean): {
3
+ requestHook: HttpRequestCustomAttributeFunction;
4
+ responseHook: HttpResponseCustomAttributeFunction;
5
+ };
@@ -0,0 +1,221 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createNetworkCaptureHooks = createNetworkCaptureHooks;
4
+ const node_events_1 = require("node:events");
5
+ const utils_js_1 = require("../utils.js");
6
+ const collector_js_1 = require("./collector.js");
7
+ function incomingHeaders(message) {
8
+ return message.headers;
9
+ }
10
+ function outgoingHeaders(message, direct) {
11
+ return {
12
+ ...Object.fromEntries(direct),
13
+ ...message.getHeaders(),
14
+ };
15
+ }
16
+ function observeDirectHeaders(args, headers) {
17
+ const append = (rawKey, value) => {
18
+ const key = String(rawKey).toLowerCase();
19
+ const existing = headers.get(key);
20
+ if (existing === undefined)
21
+ headers.set(key, value);
22
+ else if (Array.isArray(existing))
23
+ existing.push(value);
24
+ else
25
+ headers.set(key, [existing, value]);
26
+ };
27
+ const candidate = args.length >= 3 && typeof args[1] === "string" ? args[2] : args[1];
28
+ if (!candidate || typeof candidate !== "object")
29
+ return;
30
+ if (Array.isArray(candidate)) {
31
+ if (candidate.every(Array.isArray)) {
32
+ for (const entry of candidate) {
33
+ if (entry.length >= 2) {
34
+ append(entry[0], entry[1]);
35
+ }
36
+ }
37
+ }
38
+ else {
39
+ for (let index = 0; index + 1 < candidate.length; index += 2) {
40
+ append(candidate[index], candidate[index + 1]);
41
+ }
42
+ }
43
+ return;
44
+ }
45
+ for (const [rawKey, value] of Object.entries(candidate)) {
46
+ append(rawKey, value);
47
+ }
48
+ }
49
+ // pcga11:Replaces a method with a wrapped version; returns an undo that restores it unless someone else re-patched it.
50
+ function swapMethod(target, key, wrap) {
51
+ const original = target[key];
52
+ const wrapped = wrap(original);
53
+ target[key] = wrapped;
54
+ return () => {
55
+ if (target[key] === wrapped)
56
+ target[key] = original;
57
+ };
58
+ }
59
+ // Wraps writeHead (when present) to record inline headers; returns an undo.
60
+ function swapWriteHead(target, directHeaders, afterWriteHead) {
61
+ const original = target.writeHead;
62
+ if (!original)
63
+ return () => { };
64
+ const wrapped = function (...args) {
65
+ const result = Reflect.apply(original, this, args);
66
+ (0, utils_js_1.safely)(() => {
67
+ observeDirectHeaders(args, directHeaders);
68
+ afterWriteHead?.();
69
+ });
70
+ return result;
71
+ };
72
+ target.writeHead = wrapped;
73
+ return () => {
74
+ if (target.writeHead === wrapped)
75
+ target.writeHead = original;
76
+ };
77
+ }
78
+ // Attaches one-shot listeners; returns an undo that removes all of them.
79
+ function listenOnce(target, listeners) {
80
+ for (const [event, listener] of listeners)
81
+ target.once(event, listener);
82
+ return () => {
83
+ for (const [event, listener] of listeners) {
84
+ target.removeListener(event, listener);
85
+ }
86
+ };
87
+ }
88
+ function observeWritableHeaders(message, span, direction, directHeaders) {
89
+ const target = message;
90
+ const applyHeaders = () => {
91
+ (0, collector_js_1.setHeaderAttributes)(span, direction, outgoingHeaders(message, directHeaders));
92
+ };
93
+ const restoreEnd = swapMethod(target, "end", (originalEnd) => function (...args) {
94
+ const result = Reflect.apply(originalEnd, this, args);
95
+ (0, utils_js_1.safely)(applyHeaders);
96
+ return result;
97
+ });
98
+ const restoreWriteHead = swapWriteHead(target, directHeaders, applyHeaders);
99
+ let removeListeners = () => { };
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)(() => {
103
+ restoreEnd();
104
+ restoreWriteHead();
105
+ removeListeners();
106
+ });
107
+ removeListeners = listenOnce(target, [
108
+ ["finish", cleanup],
109
+ ["close", cleanup],
110
+ ]);
111
+ }
112
+ function observeReadable(message, collector) {
113
+ const target = message;
114
+ const restorePush = swapMethod(target, "push", (originalPush) => function (...args) {
115
+ // The parser's delivery path runs first and its result is returned
116
+ // unconditionally; observation failures must never disturb it.
117
+ const result = Reflect.apply(originalPush, this, args);
118
+ (0, utils_js_1.safely)(() => {
119
+ if (args[0] !== null)
120
+ collector.observe(args[0], args[1]);
121
+ else
122
+ collector.finalize(true);
123
+ });
124
+ return result;
125
+ });
126
+ const onComplete = () => (0, utils_js_1.safely)(() => collector.finalize(true));
127
+ const onIncomplete = () => (0, utils_js_1.safely)(() => collector.finalize(false));
128
+ const removeListeners = listenOnce(target, [
129
+ ["end", onComplete],
130
+ ["aborted", onIncomplete],
131
+ ["close", onIncomplete],
132
+ [node_events_1.errorMonitor, onIncomplete],
133
+ ]);
134
+ collector.addCleanup(() => {
135
+ restorePush();
136
+ removeListeners();
137
+ });
138
+ }
139
+ function observeWritable(message, collector, directHeaders) {
140
+ const target = message;
141
+ // Prevents double-observing a final chunk that end() forwards to write() internally.
142
+ let ending = false;
143
+ const restoreWrite = swapMethod(target, "write", (originalWrite) => function (...args) {
144
+ const result = Reflect.apply(originalWrite, this, args);
145
+ if (!ending)
146
+ (0, utils_js_1.safely)(() => collector.observe(args[0], args[1]));
147
+ return result;
148
+ });
149
+ const restoreEnd = swapMethod(target, "end", (originalEnd) => function (...args) {
150
+ ending = true;
151
+ try {
152
+ const result = Reflect.apply(originalEnd, this, args);
153
+ (0, utils_js_1.safely)(() => collector.observe(args[0], args[1]));
154
+ return result;
155
+ }
156
+ finally {
157
+ ending = false;
158
+ }
159
+ });
160
+ const restoreWriteHead = swapWriteHead(target, directHeaders);
161
+ const onComplete = () => (0, utils_js_1.safely)(() => collector.finalize(true));
162
+ const onIncomplete = () => (0, utils_js_1.safely)(() => collector.finalize(false));
163
+ const removeListeners = listenOnce(target, [
164
+ ["finish", onComplete],
165
+ ["close", onIncomplete],
166
+ [node_events_1.errorMonitor, onIncomplete],
167
+ ]);
168
+ collector.addCleanup(() => {
169
+ restoreWrite();
170
+ restoreEnd();
171
+ restoreWriteHead();
172
+ removeListeners();
173
+ });
174
+ }
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
+ function hookMessage(span, message, direction, bodyCaptureEnabled) {
179
+ // pcga11: Duck-type: only outgoing messages (ClientRequest/ServerResponse) have write/end.
180
+ if ("write" in message && "end" in message) {
181
+ const side = direction === "request" ? "client" : "server";
182
+ const directHeaders = new Map();
183
+ (0, collector_js_1.setHeaderAttributes)(span, direction, outgoingHeaders(message, directHeaders));
184
+ const collector = bodyCaptureEnabled
185
+ ? // Headers are passed as a lazy callback so late-set headers are seen at finalize time.
186
+ collector_js_1.BodyCollector.create(span, side, direction, () => outgoingHeaders(message, directHeaders))
187
+ : undefined;
188
+ // With a collector, patch write/end/writeHead to copy body chunks as the app sends them.
189
+ if (collector)
190
+ observeWritable(message, collector, directHeaders);
191
+ else if (span.isRecording()) {
192
+ observeWritableHeaders(message, span, direction, directHeaders);
193
+ }
194
+ }
195
+ else {
196
+ const side = direction === "request" ? "server" : "client";
197
+ const headers = incomingHeaders(message);
198
+ (0, collector_js_1.setHeaderAttributes)(span, direction, headers);
199
+ const collector = bodyCaptureEnabled
200
+ ? collector_js_1.BodyCollector.create(span, side, direction, () => headers)
201
+ : undefined;
202
+ if (collector)
203
+ observeReadable(message, collector);
204
+ }
205
+ }
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.
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.
213
+ return {
214
+ requestHook(span, request) {
215
+ (0, utils_js_1.safely)(() => hookMessage(span, request, "request", bodyCaptureEnabled));
216
+ },
217
+ responseHook(span, response) {
218
+ (0, utils_js_1.safely)(() => hookMessage(span, response, "response", bodyCaptureEnabled));
219
+ },
220
+ };
221
+ }
@@ -0,0 +1,3 @@
1
+ export { createNetworkCaptureHooks } from "./http.js";
2
+ export { createUndiciNetworkCaptureHooks } from "./undici.js";
3
+ export { isHttpCaptureSupported, isUndiciCaptureSupported } from "./support.js";
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isUndiciCaptureSupported = exports.isHttpCaptureSupported = 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.
14
+ var http_js_1 = require("./http.js");
15
+ Object.defineProperty(exports, "createNetworkCaptureHooks", { enumerable: true, get: function () { return http_js_1.createNetworkCaptureHooks; } });
16
+ var undici_js_1 = require("./undici.js");
17
+ Object.defineProperty(exports, "createUndiciNetworkCaptureHooks", { enumerable: true, get: function () { return undici_js_1.createUndiciNetworkCaptureHooks; } });
18
+ var support_js_1 = require("./support.js");
19
+ Object.defineProperty(exports, "isHttpCaptureSupported", { enumerable: true, get: function () { return support_js_1.isHttpCaptureSupported; } });
20
+ Object.defineProperty(exports, "isUndiciCaptureSupported", { enumerable: true, get: function () { return support_js_1.isUndiciCaptureSupported; } });
@@ -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;