@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.
- package/README.md +393 -214
- package/dist/before-send.d.ts +16 -0
- package/dist/before-send.js +41 -0
- package/dist/constants.d.ts +10 -8
- package/dist/constants.js +19 -6
- package/dist/endpoint.d.ts +2 -0
- package/dist/endpoint.js +11 -0
- package/dist/exporters.d.ts +11 -3
- package/dist/exporters.js +127 -53
- package/dist/index.d.ts +7 -2
- package/dist/index.js +8 -1
- package/dist/ingest.d.ts +8 -2
- package/dist/ingest.js +28 -16
- package/dist/init.d.ts +7 -3
- package/dist/init.js +85 -37
- package/dist/instrumentations.js +25 -3
- package/dist/logs.d.ts +10 -0
- package/dist/logs.js +61 -0
- package/dist/network-capture/collector.js +168 -69
- package/dist/network-capture/http.d.ts +0 -1
- package/dist/network-capture/http.js +23 -25
- package/dist/network-capture/index.js +10 -0
- package/dist/network-capture/redact.d.ts +9 -0
- package/dist/network-capture/redact.js +401 -0
- package/dist/network-capture/undici.d.ts +0 -4
- package/dist/network-capture/undici.js +55 -22
- package/dist/otlp.d.ts +8 -0
- package/dist/otlp.js +46 -0
- package/dist/propagation.d.ts +1 -1
- package/dist/propagation.js +6 -4
- package/dist/redaction-keys.d.ts +3 -0
- package/dist/redaction-keys.js +421 -0
- package/dist/redaction.d.ts +24 -0
- package/dist/redaction.js +270 -0
- package/dist/report.d.ts +9 -0
- package/dist/report.js +56 -0
- package/dist/state.d.ts +11 -4
- package/dist/state.js +26 -9
- package/dist/traces.d.ts +1 -0
- package/dist/traces.js +21 -0
- package/dist/utils.js +1 -1
- package/package.json +1 -1
- package/dist/diagnostics.d.ts +0 -13
- package/dist/diagnostics.js +0 -83
|
@@ -46,7 +46,7 @@ function observeDirectHeaders(args, headers) {
|
|
|
46
46
|
append(rawKey, value);
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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
|
-
|
|
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;
|