@runtime-digital-twin/sdk 1.0.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 (56) hide show
  1. package/README.md +214 -0
  2. package/dist/constants.d.ts +11 -0
  3. package/dist/constants.d.ts.map +1 -0
  4. package/dist/constants.js +13 -0
  5. package/dist/db-wrapper.d.ts +258 -0
  6. package/dist/db-wrapper.d.ts.map +1 -0
  7. package/dist/db-wrapper.js +636 -0
  8. package/dist/event-envelope.d.ts +35 -0
  9. package/dist/event-envelope.d.ts.map +1 -0
  10. package/dist/event-envelope.js +101 -0
  11. package/dist/fastify-plugin.d.ts +29 -0
  12. package/dist/fastify-plugin.d.ts.map +1 -0
  13. package/dist/fastify-plugin.js +243 -0
  14. package/dist/http-sentinels.d.ts +39 -0
  15. package/dist/http-sentinels.d.ts.map +1 -0
  16. package/dist/http-sentinels.js +169 -0
  17. package/dist/http-wrapper.d.ts +25 -0
  18. package/dist/http-wrapper.d.ts.map +1 -0
  19. package/dist/http-wrapper.js +477 -0
  20. package/dist/index.d.ts +19 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +93 -0
  23. package/dist/invariants.d.ts +58 -0
  24. package/dist/invariants.d.ts.map +1 -0
  25. package/dist/invariants.js +192 -0
  26. package/dist/multi-service-edge-builder.d.ts +80 -0
  27. package/dist/multi-service-edge-builder.d.ts.map +1 -0
  28. package/dist/multi-service-edge-builder.js +107 -0
  29. package/dist/outbound-matcher.d.ts +192 -0
  30. package/dist/outbound-matcher.d.ts.map +1 -0
  31. package/dist/outbound-matcher.js +457 -0
  32. package/dist/peer-service-resolver.d.ts +22 -0
  33. package/dist/peer-service-resolver.d.ts.map +1 -0
  34. package/dist/peer-service-resolver.js +85 -0
  35. package/dist/redaction.d.ts +111 -0
  36. package/dist/redaction.d.ts.map +1 -0
  37. package/dist/redaction.js +487 -0
  38. package/dist/replay-logger.d.ts +438 -0
  39. package/dist/replay-logger.d.ts.map +1 -0
  40. package/dist/replay-logger.js +434 -0
  41. package/dist/root-cause-analyzer.d.ts +45 -0
  42. package/dist/root-cause-analyzer.d.ts.map +1 -0
  43. package/dist/root-cause-analyzer.js +606 -0
  44. package/dist/shape-digest-utils.d.ts +45 -0
  45. package/dist/shape-digest-utils.d.ts.map +1 -0
  46. package/dist/shape-digest-utils.js +154 -0
  47. package/dist/trace-bundle-writer.d.ts +52 -0
  48. package/dist/trace-bundle-writer.d.ts.map +1 -0
  49. package/dist/trace-bundle-writer.js +267 -0
  50. package/dist/trace-loader.d.ts +69 -0
  51. package/dist/trace-loader.d.ts.map +1 -0
  52. package/dist/trace-loader.js +146 -0
  53. package/dist/trace-uploader.d.ts +25 -0
  54. package/dist/trace-uploader.d.ts.map +1 -0
  55. package/dist/trace-uploader.js +132 -0
  56. package/package.json +63 -0
@@ -0,0 +1,457 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OutboundMatcher = void 0;
4
+ exports.normalizeUrl = normalizeUrl;
5
+ exports.normalizeHeaders = normalizeHeaders;
6
+ exports.computeBodyHash = computeBodyHash;
7
+ exports.computeMatchSignature = computeMatchSignature;
8
+ exports.compareOutboundCalls = compareOutboundCalls;
9
+ exports.parseRecordedOutboundCalls = parseRecordedOutboundCalls;
10
+ exports.formatOutboundMismatch = formatOutboundMismatch;
11
+ exports.formatUnexpectedOutboundCall = formatUnexpectedOutboundCall;
12
+ exports.formatMissingOutboundCall = formatMissingOutboundCall;
13
+ exports.formatOutboundCallFailure = formatOutboundCallFailure;
14
+ const crypto_1 = require("crypto");
15
+ /**
16
+ * Headers to include in matching (case-insensitive)
17
+ */
18
+ const MATCHING_HEADERS = ["content-type", "accept", "authorization"];
19
+ /**
20
+ * Normalize URL for matching: pathname + sorted query params
21
+ */
22
+ function normalizeUrl(url) {
23
+ try {
24
+ const urlObj = new URL(url);
25
+ const searchParams = new URLSearchParams(urlObj.search);
26
+ const sortedParams = Array.from(searchParams.entries()).sort(([a], [b]) => a.localeCompare(b));
27
+ const normalizedSearch = new URLSearchParams(sortedParams).toString();
28
+ return `${urlObj.pathname}${normalizedSearch ? "?" + normalizedSearch : ""}`;
29
+ }
30
+ catch {
31
+ // If URL parsing fails, try to extract path+query manually
32
+ const match = url.match(/^[^:]+:\/\/[^/]+(\/.*)$/);
33
+ if (match) {
34
+ return match[1];
35
+ }
36
+ return url;
37
+ }
38
+ }
39
+ /**
40
+ * Normalize headers for matching: lowercase keys, filter to matching headers
41
+ */
42
+ function normalizeHeaders(headers) {
43
+ const normalized = {};
44
+ for (const [key, value] of Object.entries(headers)) {
45
+ const lowerKey = key.toLowerCase();
46
+ if (MATCHING_HEADERS.includes(lowerKey)) {
47
+ normalized[lowerKey] = value;
48
+ }
49
+ }
50
+ return normalized;
51
+ }
52
+ /**
53
+ * Compute body hash from content
54
+ */
55
+ function computeBodyHash(body) {
56
+ if (!body)
57
+ return null;
58
+ let bodyStr;
59
+ if (Buffer.isBuffer(body)) {
60
+ bodyStr = body.toString("utf8");
61
+ }
62
+ else if (typeof body === "object") {
63
+ bodyStr = JSON.stringify(body);
64
+ }
65
+ else {
66
+ bodyStr = String(body);
67
+ }
68
+ const hash = (0, crypto_1.createHash)("sha256").update(bodyStr).digest("hex");
69
+ return `sha256:${hash}`;
70
+ }
71
+ /**
72
+ * Compute match signature for comparison
73
+ */
74
+ function computeMatchSignature(call) {
75
+ const normalizedUrl = normalizeUrl(call.url);
76
+ const normalizedHeaders = normalizeHeaders(call.headers);
77
+ const headerStr = Object.entries(normalizedHeaders)
78
+ .sort(([a], [b]) => a.localeCompare(b))
79
+ .map(([k, v]) => `${k}=${v}`)
80
+ .join("&");
81
+ return `${call.method}:${normalizedUrl}:${call.bodyHash || "null"}:${headerStr}`;
82
+ }
83
+ /**
84
+ * Compare two calls and return differences
85
+ */
86
+ function compareOutboundCalls(expected, actual) {
87
+ const diffs = [];
88
+ // Method
89
+ if (expected.method !== actual.method) {
90
+ diffs.push({
91
+ field: "method",
92
+ expected: expected.method,
93
+ actual: actual.method,
94
+ });
95
+ }
96
+ // URL (normalized)
97
+ const expectedUrl = expected.normalizedUrl;
98
+ const actualUrl = normalizeUrl(actual.url);
99
+ if (expectedUrl !== actualUrl) {
100
+ diffs.push({ field: "url", expected: expectedUrl, actual: actualUrl });
101
+ }
102
+ // Body hash
103
+ if (expected.bodyHash !== actual.bodyHash) {
104
+ diffs.push({
105
+ field: "bodyHash",
106
+ expected: expected.bodyHash,
107
+ actual: actual.bodyHash,
108
+ });
109
+ }
110
+ // Headers (only check headers that were RECORDED - if recorded headers are empty, skip comparison)
111
+ const expectedHeaders = normalizeHeaders(expected.headers);
112
+ const actualHeaders = normalizeHeaders(actual.headers);
113
+ // Only compare headers that exist in the recorded trace
114
+ // Headers in actual but not in expected are ignored (caller may add headers we don't care about)
115
+ for (const key of Object.keys(expectedHeaders)) {
116
+ const expectedVal = expectedHeaders[key];
117
+ const actualVal = actualHeaders[key] || null;
118
+ if (expectedVal !== actualVal) {
119
+ diffs.push({
120
+ field: `header:${key}`,
121
+ expected: expectedVal,
122
+ actual: actualVal,
123
+ });
124
+ }
125
+ }
126
+ return { matches: diffs.length === 0, diffs };
127
+ }
128
+ /**
129
+ * Ordered outbound call matcher for replay
130
+ */
131
+ class OutboundMatcher {
132
+ recordedCalls = [];
133
+ currentIndex = 0;
134
+ mode;
135
+ traceDir;
136
+ divergences = [];
137
+ logger = null;
138
+ constructor(recordedCalls, mode, traceDir, logger) {
139
+ this.recordedCalls = recordedCalls;
140
+ this.mode = mode;
141
+ this.traceDir = traceDir;
142
+ this.logger = logger || null;
143
+ }
144
+ /**
145
+ * Set the logger for structured replay logging
146
+ */
147
+ setLogger(logger) {
148
+ this.logger = logger;
149
+ }
150
+ /**
151
+ * Get all recorded divergences (for explain mode)
152
+ */
153
+ getDivergences() {
154
+ return [...this.divergences];
155
+ }
156
+ /**
157
+ * Get current call index
158
+ */
159
+ getCurrentIndex() {
160
+ return this.currentIndex;
161
+ }
162
+ /**
163
+ * Get total recorded calls
164
+ */
165
+ getTotalCalls() {
166
+ return this.recordedCalls.length;
167
+ }
168
+ /**
169
+ * Match the next outbound call against the recorded sequence
170
+ *
171
+ * In strict mode: throws OutboundRequestMismatch on any mismatch
172
+ * In explain mode: emits divergence and returns best-effort match
173
+ */
174
+ matchNext(actual) {
175
+ const callIndex = this.currentIndex;
176
+ const actualNormalizedUrl = normalizeUrl(actual.url);
177
+ // Check if we've exceeded recorded calls - this is an unexpected call
178
+ if (callIndex >= this.recordedCalls.length) {
179
+ const failure = {
180
+ type: "unexpected_outbound_call",
181
+ location: `${this.traceDir}/events.jsonl:call[${callIndex}]`,
182
+ expected: `<end of recorded calls after ${this.recordedCalls.length} call(s)>`,
183
+ actual: `${actual.method} ${actualNormalizedUrl}`,
184
+ hint: "An outbound HTTP call occurred during replay that was not recorded in the original trace. This may indicate non-deterministic behavior or code changes.",
185
+ callIndex,
186
+ service: this.traceDir,
187
+ spanId: null,
188
+ call: actual,
189
+ };
190
+ // Log unexpected call
191
+ this.logger?.httpUnexpected({
192
+ callIndex,
193
+ method: actual.method,
194
+ url: actual.url,
195
+ bodyHash: actual.bodyHash,
196
+ expectedCallCount: this.recordedCalls.length,
197
+ });
198
+ if (this.mode === "strict") {
199
+ throw failure;
200
+ }
201
+ // Explain mode: emit divergence and fail (no recorded response to return)
202
+ this.divergences.push({
203
+ type: "divergence",
204
+ severity: "error",
205
+ callIndex,
206
+ message: `Unexpected outbound call: ${actual.method} ${actual.url}`,
207
+ expected: null,
208
+ actual,
209
+ recoverable: false,
210
+ });
211
+ throw failure; // Can't continue without a response
212
+ }
213
+ const expected = this.recordedCalls[callIndex];
214
+ const { matches, diffs } = compareOutboundCalls(expected, actual);
215
+ if (matches) {
216
+ // Log successful match
217
+ this.logger?.httpMatch({
218
+ callIndex,
219
+ method: actual.method,
220
+ url: actual.url,
221
+ normalizedUrl: actualNormalizedUrl,
222
+ bodyHash: actual.bodyHash,
223
+ matchedSpanId: expected.spanId,
224
+ });
225
+ this.currentIndex++;
226
+ return expected;
227
+ }
228
+ // Mismatch detected
229
+ const failure = {
230
+ type: "outbound_request_mismatch",
231
+ location: `${this.traceDir}/events.jsonl:call[${callIndex}]`,
232
+ expected: `${expected.method} ${expected.normalizedUrl}`,
233
+ actual: `${actual.method} ${actualNormalizedUrl}`,
234
+ hint: "Outbound call does not match recorded trace. Check for non-determinism or code changes.",
235
+ callIndex,
236
+ diff: diffs,
237
+ };
238
+ // Explain mode: check if divergence is recoverable
239
+ // Recoverable if only headers differ (method, URL, body match)
240
+ const criticalDiffs = diffs.filter((d) => d.field === "method" || d.field === "url" || d.field === "bodyHash");
241
+ const recoverable = criticalDiffs.length === 0;
242
+ // Log mismatch
243
+ this.logger?.httpMismatch({
244
+ callIndex,
245
+ expected: {
246
+ method: expected.method,
247
+ url: expected.normalizedUrl,
248
+ bodyHash: expected.bodyHash,
249
+ },
250
+ actual: {
251
+ method: actual.method,
252
+ url: actualNormalizedUrl,
253
+ bodyHash: actual.bodyHash,
254
+ },
255
+ differences: diffs,
256
+ recoverable,
257
+ });
258
+ if (this.mode === "strict") {
259
+ throw failure;
260
+ }
261
+ this.divergences.push({
262
+ type: "divergence",
263
+ severity: recoverable ? "warning" : "error",
264
+ callIndex,
265
+ message: `Outbound call mismatch: ${diffs
266
+ .map((d) => `${d.field}: ${d.expected} → ${d.actual}`)
267
+ .join(", ")}`,
268
+ expected,
269
+ actual,
270
+ recoverable,
271
+ });
272
+ // Emit to stderr for CLI visibility
273
+ console.warn(`[Replay] Divergence at call ${callIndex}: ${failure.expected} vs ${failure.actual}`);
274
+ for (const diff of diffs) {
275
+ console.warn(` ${diff.field}: expected="${diff.expected}" actual="${diff.actual}"`);
276
+ }
277
+ if (!recoverable) {
278
+ throw failure;
279
+ }
280
+ // Continue with the recorded response despite header differences
281
+ this.currentIndex++;
282
+ return expected;
283
+ }
284
+ /**
285
+ * Check if all recorded calls have been matched
286
+ */
287
+ isComplete() {
288
+ return this.currentIndex >= this.recordedCalls.length;
289
+ }
290
+ /**
291
+ * Get summary of unmatched calls (calls that were recorded but not replayed)
292
+ */
293
+ getUnmatchedCalls() {
294
+ return this.recordedCalls.slice(this.currentIndex);
295
+ }
296
+ /**
297
+ * Finalize replay and check for missing outbound calls.
298
+ * Should be called at the end of replay to detect any recorded calls that never happened.
299
+ *
300
+ * In strict mode: throws MissingOutboundCall[] if any calls are missing
301
+ * In explain mode: emits divergences and returns the missing calls
302
+ *
303
+ * @returns Array of MissingOutboundCall failures (empty if all calls matched)
304
+ */
305
+ finalize() {
306
+ const missingCalls = [];
307
+ const unmatchedCalls = this.getUnmatchedCalls();
308
+ for (let i = 0; i < unmatchedCalls.length; i++) {
309
+ const recordedCall = unmatchedCalls[i];
310
+ const callIndex = this.currentIndex + i;
311
+ const failure = {
312
+ type: "missing_outbound_call",
313
+ location: `${this.traceDir}/events.jsonl:call[${callIndex}]`,
314
+ expected: `${recordedCall.method} ${recordedCall.normalizedUrl}`,
315
+ actual: "<call never made>",
316
+ hint: "A recorded outbound HTTP call was never made during replay. This may indicate code path divergence, conditional logic changes, or early termination.",
317
+ callIndex,
318
+ service: this.traceDir,
319
+ spanId: recordedCall.spanId,
320
+ recordedCall,
321
+ };
322
+ missingCalls.push(failure);
323
+ // Log missing call
324
+ this.logger?.httpMissing({
325
+ callIndex,
326
+ method: recordedCall.method,
327
+ url: recordedCall.normalizedUrl,
328
+ spanId: recordedCall.spanId,
329
+ bodyHash: recordedCall.bodyHash,
330
+ });
331
+ // Emit divergence
332
+ this.divergences.push({
333
+ type: "divergence",
334
+ severity: "error",
335
+ callIndex,
336
+ message: `Missing outbound call: ${recordedCall.method} ${recordedCall.normalizedUrl}`,
337
+ expected: recordedCall,
338
+ actual: { method: "", url: "", headers: {}, bodyHash: null },
339
+ recoverable: false,
340
+ });
341
+ // Emit to stderr for CLI visibility
342
+ console.warn(`[Replay] Missing call ${callIndex}: expected ${recordedCall.method} ${recordedCall.normalizedUrl}`);
343
+ }
344
+ if (missingCalls.length > 0 && this.mode === "strict") {
345
+ // In strict mode, throw the first missing call
346
+ // (caller can access all via the returned array if caught)
347
+ throw missingCalls[0];
348
+ }
349
+ return missingCalls;
350
+ }
351
+ }
352
+ exports.OutboundMatcher = OutboundMatcher;
353
+ /**
354
+ * Parse recorded outbound calls from trace events
355
+ */
356
+ function parseRecordedOutboundCalls(events) {
357
+ const requestsBySpan = new Map();
358
+ for (const event of events) {
359
+ if (event.type === "http.request.outbound") {
360
+ requestsBySpan.set(event.spanId, {
361
+ spanId: event.spanId,
362
+ method: event.method,
363
+ url: event.url,
364
+ normalizedUrl: normalizeUrl(event.url),
365
+ headers: event.headers || {},
366
+ bodyHash: event.bodyHash || null,
367
+ response: null,
368
+ });
369
+ }
370
+ else if (event.type === "http.response.outbound") {
371
+ const request = requestsBySpan.get(event.spanId);
372
+ if (request) {
373
+ request.response = {
374
+ statusCode: event.statusCode,
375
+ headers: event.headers || {},
376
+ bodyHash: event.bodyHash || null,
377
+ bodyBlob: event.bodyBlob || null,
378
+ };
379
+ }
380
+ }
381
+ }
382
+ // Return calls in order (by their appearance in events)
383
+ // Filter out requests without responses
384
+ return Array.from(requestsBySpan.values()).filter((call) => call.response !== null);
385
+ }
386
+ /**
387
+ * Format OutboundRequestMismatch for CLI output
388
+ */
389
+ function formatOutboundMismatch(failure) {
390
+ const lines = [
391
+ `[Replay Error] ${failure.type}`,
392
+ ` Location: ${failure.location}`,
393
+ ` Call #${failure.callIndex}`,
394
+ ` Expected: ${failure.expected}`,
395
+ ` Actual: ${failure.actual}`,
396
+ ` Differences:`,
397
+ ];
398
+ for (const diff of failure.diff) {
399
+ lines.push(` ${diff.field}: "${diff.expected}" → "${diff.actual}"`);
400
+ }
401
+ lines.push(` Hint: ${failure.hint}`);
402
+ return lines.join("\n");
403
+ }
404
+ /**
405
+ * Format UnexpectedOutboundCall for CLI output
406
+ */
407
+ function formatUnexpectedOutboundCall(failure) {
408
+ const lines = [
409
+ `[Replay Error] ${failure.type}`,
410
+ ` Location: ${failure.location}`,
411
+ ` Service: ${failure.service}`,
412
+ ` Call #${failure.callIndex}`,
413
+ ` Expected: ${failure.expected}`,
414
+ ` Actual: ${failure.actual}`,
415
+ ` Call Details:`,
416
+ ` Method: ${failure.call.method}`,
417
+ ` URL: ${failure.call.url}`,
418
+ ` Body Hash: ${failure.call.bodyHash || "<none>"}`,
419
+ ` Hint: ${failure.hint}`,
420
+ ];
421
+ return lines.join("\n");
422
+ }
423
+ /**
424
+ * Format MissingOutboundCall for CLI output
425
+ */
426
+ function formatMissingOutboundCall(failure) {
427
+ const lines = [
428
+ `[Replay Error] ${failure.type}`,
429
+ ` Location: ${failure.location}`,
430
+ ` Service: ${failure.service}`,
431
+ ` Span ID: ${failure.spanId}`,
432
+ ` Call #${failure.callIndex}`,
433
+ ` Expected: ${failure.expected}`,
434
+ ` Actual: ${failure.actual}`,
435
+ ` Recorded Call:`,
436
+ ` Method: ${failure.recordedCall.method}`,
437
+ ` URL: ${failure.recordedCall.url}`,
438
+ ` Body Hash: ${failure.recordedCall.bodyHash || "<none>"}`,
439
+ ` Hint: ${failure.hint}`,
440
+ ];
441
+ return lines.join("\n");
442
+ }
443
+ /**
444
+ * Format any outbound call failure for CLI output
445
+ */
446
+ function formatOutboundCallFailure(failure) {
447
+ switch (failure.type) {
448
+ case "outbound_request_mismatch":
449
+ return formatOutboundMismatch(failure);
450
+ case "unexpected_outbound_call":
451
+ return formatUnexpectedOutboundCall(failure);
452
+ case "missing_outbound_call":
453
+ return formatMissingOutboundCall(failure);
454
+ default:
455
+ return `[Replay Error] Unknown failure type`;
456
+ }
457
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Peer service resolution for outbound HTTP calls
3
+ * Maps hostnames to service names for multi-service graph building
4
+ */
5
+ /**
6
+ * Resolve peer service name from hostname using mapping
7
+ *
8
+ * @param hostname - The hostname from the URL (e.g., "pricing.internal", "api.example.com")
9
+ * @returns Object with peerService (string | null) and peerHost (string | null)
10
+ */
11
+ export declare function resolvePeerService(hostname: string): {
12
+ peerService: string | null;
13
+ peerHost: string | null;
14
+ };
15
+ /**
16
+ * Extract URL template from URL (path without query params)
17
+ *
18
+ * @param url - Full URL string
19
+ * @returns URL template (path only, no query params) or null if parsing fails
20
+ */
21
+ export declare function extractUrlTemplate(url: string): string | null;
22
+ //# sourceMappingURL=peer-service-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"peer-service-resolver.d.ts","sourceRoot":"","sources":["../src/peer-service-resolver.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG;IACpD,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,CAiDA;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAgB7D"}
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ /**
3
+ * Peer service resolution for outbound HTTP calls
4
+ * Maps hostnames to service names for multi-service graph building
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.resolvePeerService = resolvePeerService;
8
+ exports.extractUrlTemplate = extractUrlTemplate;
9
+ /**
10
+ * Resolve peer service name from hostname using mapping
11
+ *
12
+ * @param hostname - The hostname from the URL (e.g., "pricing.internal", "api.example.com")
13
+ * @returns Object with peerService (string | null) and peerHost (string | null)
14
+ */
15
+ function resolvePeerService(hostname) {
16
+ // Parse peer mapping from environment variable
17
+ const peerMapJson = process.env.WRAITH_PEER_MAP_JSON;
18
+ if (!peerMapJson) {
19
+ // No mapping configured - return hostname as peerHost
20
+ return {
21
+ peerService: null,
22
+ peerHost: hostname,
23
+ };
24
+ }
25
+ let peerMap;
26
+ try {
27
+ peerMap = JSON.parse(peerMapJson);
28
+ }
29
+ catch (error) {
30
+ console.warn(`[SDK] Failed to parse WRAITH_PEER_MAP_JSON: ${error}`);
31
+ return {
32
+ peerService: null,
33
+ peerHost: hostname,
34
+ };
35
+ }
36
+ // Check for exact hostname match
37
+ if (peerMap[hostname]) {
38
+ return {
39
+ peerService: peerMap[hostname],
40
+ peerHost: hostname,
41
+ };
42
+ }
43
+ // Check for domain match (e.g., "*.internal" matches "pricing.internal")
44
+ for (const [pattern, serviceName] of Object.entries(peerMap)) {
45
+ if (pattern.startsWith('*.')) {
46
+ const domain = pattern.substring(2); // Remove "*."
47
+ if (hostname.endsWith('.' + domain) || hostname === domain) {
48
+ return {
49
+ peerService: serviceName,
50
+ peerHost: hostname,
51
+ };
52
+ }
53
+ }
54
+ }
55
+ // No match found - return hostname as peerHost
56
+ return {
57
+ peerService: null,
58
+ peerHost: hostname,
59
+ };
60
+ }
61
+ /**
62
+ * Extract URL template from URL (path without query params)
63
+ *
64
+ * @param url - Full URL string
65
+ * @returns URL template (path only, no query params) or null if parsing fails
66
+ */
67
+ function extractUrlTemplate(url) {
68
+ try {
69
+ const urlObj = new URL(url);
70
+ return urlObj.pathname;
71
+ }
72
+ catch (error) {
73
+ // If URL parsing fails, try to extract path manually
74
+ try {
75
+ const pathMatch = url.match(/^https?:\/\/[^\/]+(\/[^?]*)/);
76
+ if (pathMatch) {
77
+ return pathMatch[1];
78
+ }
79
+ }
80
+ catch {
81
+ // Ignore
82
+ }
83
+ return null;
84
+ }
85
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Redaction Module
3
+ *
4
+ * Provides privacy guardrails for trace capture by:
5
+ * - Masking sensitive headers (auth, tokens, cookies)
6
+ * - Redacting passwords and secrets from request/response bodies
7
+ * - Supporting custom redaction rules via config file
8
+ * - Using deterministic hashing for redacted values (stable across runs)
9
+ */
10
+ /**
11
+ * Redaction rule types
12
+ */
13
+ export type RedactionRuleType = "header" | "body_field" | "body_pattern" | "query_param";
14
+ /**
15
+ * A single redaction rule
16
+ */
17
+ export interface RedactionRule {
18
+ type: RedactionRuleType;
19
+ /** For header/query_param: exact name or pattern */
20
+ name?: string;
21
+ /** For body_pattern: regex pattern */
22
+ pattern?: string;
23
+ /** Whether to use regex matching for name */
24
+ regex?: boolean;
25
+ /** Custom replacement (default: [REDACTED:hash]) */
26
+ replacement?: string;
27
+ /** Whether to include a deterministic hash suffix */
28
+ hash?: boolean;
29
+ }
30
+ /**
31
+ * Redaction configuration
32
+ */
33
+ export interface RedactionConfig {
34
+ /** Enable/disable redaction (default: true) */
35
+ enabled: boolean;
36
+ /** Custom rules to add to defaults */
37
+ rules: RedactionRule[];
38
+ /** Override default rules entirely */
39
+ overrideDefaults?: boolean;
40
+ /** Salt for deterministic hashing (for privacy, use a secret) */
41
+ hashSalt?: string;
42
+ }
43
+ /**
44
+ * Compute a deterministic hash for a redacted value
45
+ * The hash is stable across runs given the same input and salt
46
+ */
47
+ export declare function computeRedactionHash(value: string, salt?: string): string;
48
+ /**
49
+ * Create a redacted placeholder
50
+ */
51
+ export declare function createRedactedValue(originalValue: string, includeHash?: boolean, salt?: string): string;
52
+ /**
53
+ * Load redaction config from file
54
+ */
55
+ export declare function loadRedactionConfig(configPath?: string): RedactionConfig;
56
+ /**
57
+ * Set global redaction config
58
+ */
59
+ export declare function setRedactionConfig(config: Partial<RedactionConfig>): void;
60
+ /**
61
+ * Get current redaction config
62
+ */
63
+ export declare function getRedactionConfig(): RedactionConfig;
64
+ /**
65
+ * Reset redaction config to defaults
66
+ */
67
+ export declare function resetRedactionConfig(): void;
68
+ /**
69
+ * Check if a header name should be redacted
70
+ */
71
+ export declare function shouldRedactHeader(headerName: string): boolean;
72
+ /**
73
+ * Check if a query param should be redacted
74
+ */
75
+ export declare function shouldRedactQueryParam(paramName: string): boolean;
76
+ /**
77
+ * Redact headers in a headers object
78
+ */
79
+ export declare function redactHeaders(headers: Record<string, string | string[] | undefined>): Record<string, string | string[] | undefined>;
80
+ /**
81
+ * Redact query parameters
82
+ */
83
+ export declare function redactQueryParams(query: Record<string, any>): Record<string, any>;
84
+ /**
85
+ * Redact sensitive fields from a body object (recursive)
86
+ */
87
+ export declare function redactBodyObject(body: any): any;
88
+ /**
89
+ * Redact patterns from a body string
90
+ */
91
+ export declare function redactBodyPatterns(body: string): string;
92
+ /**
93
+ * Redact a body (handles both string and object)
94
+ */
95
+ export declare function redactBody(body: string | object | null | undefined): string | object | null | undefined;
96
+ /**
97
+ * Redact a URL (query params)
98
+ */
99
+ export declare function redactUrl(url: string): string;
100
+ /**
101
+ * Check if a value looks like it contains sensitive data
102
+ * (heuristic-based, for additional protection)
103
+ */
104
+ export declare function containsSensitivePatterns(value: string): boolean;
105
+ /**
106
+ * Export default sensitive lists for testing
107
+ */
108
+ export declare const SENSITIVE_HEADERS: string[];
109
+ export declare const SENSITIVE_QUERY_PARAMS: string[];
110
+ export declare const SENSITIVE_BODY_FIELDS: string[];
111
+ //# sourceMappingURL=redaction.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redaction.d.ts","sourceRoot":"","sources":["../src/redaction.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH;;GAEG;AACH,MAAM,MAAM,iBAAiB,GACzB,QAAQ,GACR,YAAY,GACZ,cAAc,GACd,aAAa,CAAC;AAElB;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,iBAAiB,CAAC;IACxB,oDAAoD;IACpD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sCAAsC;IACtC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,oDAAoD;IACpD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qDAAqD;IACrD,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,+CAA+C;IAC/C,OAAO,EAAE,OAAO,CAAC;IACjB,sCAAsC;IACtC,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,sCAAsC;IACtC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,iEAAiE;IACjE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAkJD;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,MAAoC,GACzC,MAAM,CAMR;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,aAAa,EAAE,MAAM,EACrB,WAAW,GAAE,OAAc,EAC3B,IAAI,CAAC,EAAE,MAAM,GACZ,MAAM,CAMR;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,eAAe,CAwCxE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,OAAO,CAAC,eAAe,CAAC,GAAG,IAAI,CAQzE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,eAAe,CAEpD;AAED;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAE3C;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAiB9D;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAiBjE;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,GACrD,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAwB/C;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GACzB,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAsBrB;AAsBD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,GAAG,GAAG,GAAG,CAkC/C;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAwBvD;AAED;;GAEG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,GACvC,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CA6BpC;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA6B7C;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAYhE;AAED;;GAEG;AACH,eAAO,MAAM,iBAAiB,UAA4B,CAAC;AAC3D,eAAO,MAAM,sBAAsB,UAAiC,CAAC;AACrE,eAAO,MAAM,qBAAqB,UAAgC,CAAC"}