@runtime-digital-twin/sdk 1.0.0 → 1.0.3

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/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 Wraith On-Call Engineer Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md CHANGED
@@ -91,6 +91,8 @@ await fastify.register(fastifyTracing, {
91
91
  headerAllowlist: [], // Optional: Headers to capture
92
92
  maxBodySize: 10 * 1024 * 1024, // Optional: Max body size to capture (default: 10MB)
93
93
  enabled: true, // Optional: Enable/disable tracing (default: true)
94
+ traceSampleRate: 1, // Optional: 0–1; 1 = always, 0.1 = 10% of requests (default: 1)
95
+ alwaysTraceErrors: true, // Optional: always capture trace for requests that error (default: true)
94
96
  // Upload options (optional)
95
97
  uploadUrl: process.env.WRAITH_API_URL + '/api/traces/ingest',
96
98
  apiKey: process.env.WRAITH_API_KEY,
@@ -136,6 +138,15 @@ WRAITH_API_KEY=your-api-key-here
136
138
 
137
139
  Traces are uploaded asynchronously after each request completes, so they don't slow down your application.
138
140
 
141
+ ### Sampling and errors
142
+
143
+ - **traceSampleRate** (0–1): When set below 1, only that fraction of requests are traced (e.g. 0.1 = 10%). Reduces volume and cost while keeping a representative sample.
144
+ - **alwaysTraceErrors**: When true (default), any request that ends in an error (Fastify `onError`) is traced and uploaded even if it was not sampled. Ensures failures are never missed.
145
+
146
+ ### Rate limits and 429
147
+
148
+ If the ingest endpoint returns **429 Too Many Requests**, the uploader retries with exponential backoff (same as for 5xx). Use the **Retry-After** response header when present; the SDK respects backoff between attempts. To avoid hitting limits, reduce volume with **traceSampleRate** or increase **TRACE_INGEST_RATE_LIMIT_REQUESTS_PER_MIN** on the server.
149
+
139
150
  ## Modes
140
151
 
141
152
  ### Record Mode
@@ -209,6 +220,21 @@ import {
209
220
 
210
221
  See the [Quickstart Guide](../../QUICKSTART.md) for complete examples.
211
222
 
223
+ ## Changelog
224
+
225
+ ### v1.1.0
226
+ - Enhanced trace upload with automatic retry
227
+ - Improved CORS support for browser-based services
228
+ - Better error handling and logging
229
+ - Support for demo mode autofix integration
230
+
231
+ ### v1.0.0
232
+ - Initial release
233
+ - Fastify tracing plugin
234
+ - Database query capture
235
+ - HTTP client wrapping
236
+ - Trace bundle writing
237
+
212
238
  ## License
213
239
 
214
240
  MIT
@@ -8,6 +8,10 @@ export interface FastifyTracingOptions {
8
8
  headerAllowlist?: string[];
9
9
  maxBodySize?: number;
10
10
  enabled?: boolean;
11
+ /** Sample rate 0–1. 1 = always trace, 0 = never, 0.1 = 10% of requests. Default 1. */
12
+ traceSampleRate?: number;
13
+ /** When true, always capture a trace for requests that end in error (onError), even if not sampled. Default true. */
14
+ alwaysTraceErrors?: boolean;
11
15
  uploadUrl?: string;
12
16
  apiKey?: string;
13
17
  uploadOnComplete?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"fastify-plugin.d.ts","sourceRoot":"","sources":["../src/fastify-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAgC,MAAM,SAAS,CAAC;AAE3E,OAAO,EAKL,WAAW,EACZ,MAAM,uBAAuB,CAAC;AAY/B,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,cAAc;QACtB,WAAW,CAAC,EAAE,WAAW,CAAC;QAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,oBAAoB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KACtC;CACF;AAID;;GAEG;AACH,eAAO,MAAM,cAAc,EAAE,kBAAkB,CAAC,qBAAqB,CAsRpE,CAAC;;AAGF,wBAGG"}
1
+ {"version":3,"file":"fastify-plugin.d.ts","sourceRoot":"","sources":["../src/fastify-plugin.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAgC,MAAM,SAAS,CAAC;AAE3E,OAAO,EAKL,WAAW,EACZ,MAAM,uBAAuB,CAAC;AAY/B,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,sFAAsF;IACtF,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,qHAAqH;IACrH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAE5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,cAAc;QACtB,WAAW,CAAC,EAAE,WAAW,CAAC;QAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,oBAAoB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KACtC;CACF;AAID;;GAEG;AACH,eAAO,MAAM,cAAc,EAAE,kBAAkB,CAAC,qBAAqB,CAyVpE,CAAC;;AAGF,wBAGG"}
@@ -4,6 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.fastifyTracing = void 0;
7
+ const promises_1 = require("fs/promises");
8
+ const path_1 = require("path");
7
9
  const fastify_plugin_1 = __importDefault(require("fastify-plugin"));
8
10
  const trace_bundle_writer_1 = require("./trace-bundle-writer");
9
11
  const http_wrapper_1 = require("./http-wrapper");
@@ -15,7 +17,7 @@ const DEFAULT_MAX_BODY_SIZE = 10 * 1024 * 1024; // 10MB
15
17
  * Fastify plugin for capturing HTTP request/response traces
16
18
  */
17
19
  const fastifyTracing = async (fastify, options) => {
18
- const { serviceName, serviceVersion, environment, traceDir, headerAllowlist, maxBodySize = DEFAULT_MAX_BODY_SIZE, enabled = true, uploadUrl, apiKey, uploadOnComplete = true, } = options;
20
+ const { serviceName, serviceVersion, environment, traceDir, headerAllowlist, maxBodySize = DEFAULT_MAX_BODY_SIZE, enabled = true, traceSampleRate = 1, alwaysTraceErrors = true, uploadUrl, apiKey, uploadOnComplete = true, } = options;
19
21
  if (!enabled) {
20
22
  fastify.log.info("[SDK] Tracing is disabled");
21
23
  return;
@@ -27,30 +29,36 @@ const fastifyTracing = async (fastify, options) => {
27
29
  // Hook: onRequest - initialize trace but don't write request event yet (body not available)
28
30
  fastify.addHook("onRequest", async (request, reply) => {
29
31
  try {
32
+ const sampledIn = traceSampleRate >= 1 || (traceSampleRate > 0 && Math.random() < traceSampleRate);
33
+ request.traceSampledIn = sampledIn;
34
+ if (!sampledIn) {
35
+ request.traceBundle = null;
36
+ request.traceSpanId = null;
37
+ request.traceStartTime = null;
38
+ request.traceRequestBodyHash = null;
39
+ request.traceIncomingParentSpanId = null;
40
+ return;
41
+ }
30
42
  // Check for trace context propagation headers
31
43
  const incomingTraceId = request.headers['x-wraith-trace-id'];
32
44
  const incomingParentSpanId = request.headers['x-wraith-parent-span-id'];
33
45
  // Create trace bundle for this request
34
- // Use incoming traceId if present (cross-service propagation), otherwise generate new one
35
46
  const traceBundle = await (0, trace_bundle_writer_1.createTrace)({
36
47
  serviceName,
37
48
  serviceVersion,
38
49
  environment,
39
50
  traceDir,
40
- traceId: incomingTraceId, // Will be undefined if header not present, triggering new traceId generation
51
+ traceId: incomingTraceId,
41
52
  });
42
53
  const spanId = (0, trace_bundle_writer_1.generateSpanId)();
43
54
  const startTime = Date.now();
44
- // Store on request for later use
45
55
  request.traceBundle = traceBundle;
46
56
  request.traceSpanId = spanId;
47
57
  request.traceStartTime = startTime;
48
- request.traceRequestBodyHash = null; // Will be set in preHandler
49
- // Store incoming parent span ID for use in preHandler
58
+ request.traceRequestBodyHash = null;
50
59
  request.traceIncomingParentSpanId = incomingParentSpanId || null;
51
60
  }
52
61
  catch (error) {
53
- // Fail-open: log error but don't crash
54
62
  fastify.log.error(`[SDK] Failed to initialize trace: ${error}`);
55
63
  }
56
64
  });
@@ -179,8 +187,8 @@ const fastifyTracing = async (fastify, options) => {
179
187
  });
180
188
  // Auto-upload trace if configured
181
189
  if (uploadUrl && apiKey && uploadOnComplete !== false) {
182
- // Upload asynchronously (don't block response)
183
190
  const actualTraceDir = traceDir || './traces';
191
+ const tracePath = (0, path_1.join)(actualTraceDir, traceBundle.traceId);
184
192
  (0, trace_uploader_1.uploadTrace)({
185
193
  uploadUrl,
186
194
  apiKey,
@@ -189,15 +197,22 @@ const fastifyTracing = async (fastify, options) => {
189
197
  serviceName,
190
198
  serviceVersion,
191
199
  environment,
192
- }).then(result => {
200
+ }).then(async (result) => {
193
201
  if (result.success) {
194
- fastify.log.info(`[SDK] Trace uploaded: ${result.traceId}`);
202
+ fastify.log.info({ traceId: result.traceId }, '[SDK] Trace uploaded');
203
+ // Delete local trace dir after successful upload to avoid re-upload and unbounded disk growth
204
+ try {
205
+ await (0, promises_1.rm)(tracePath, { recursive: true, force: true });
206
+ }
207
+ catch (err) {
208
+ fastify.log.debug({ traceId: traceBundle.traceId, err: err?.message }, '[SDK] Could not delete trace dir after upload');
209
+ }
195
210
  }
196
211
  else {
197
- fastify.log.warn(`[SDK] Trace upload failed: ${result.error}`);
212
+ fastify.log.warn({ traceId: traceBundle.traceId, error: result.error }, '[SDK] Trace upload failed');
198
213
  }
199
214
  }).catch(error => {
200
- fastify.log.error(`[SDK] Trace upload error: ${error}`);
215
+ fastify.log.error({ traceId: traceBundle.traceId, error: error?.message }, '[SDK] Trace upload error');
201
216
  });
202
217
  }
203
218
  // Clear trace context
@@ -210,13 +225,67 @@ const fastifyTracing = async (fastify, options) => {
210
225
  (0, http_wrapper_1.setTraceContext)(null, null);
211
226
  }
212
227
  });
213
- // Hook: onError - capture errors
228
+ // Hook: onError - capture errors (always capture if alwaysTraceErrors and no bundle yet)
214
229
  fastify.addHook("onError", async (request, reply, error) => {
215
- const traceBundle = request.traceBundle;
216
- const spanId = request.traceSpanId;
217
- if (!traceBundle || !spanId) {
230
+ let traceBundle = request.traceBundle;
231
+ let spanId = request.traceSpanId;
232
+ const wasSampledOut = !request.traceSampledIn;
233
+ if (wasSampledOut && alwaysTraceErrors) {
234
+ try {
235
+ const bundle = await (0, trace_bundle_writer_1.createTrace)({
236
+ serviceName,
237
+ serviceVersion,
238
+ environment,
239
+ traceDir,
240
+ });
241
+ const sid = (0, trace_bundle_writer_1.generateSpanId)();
242
+ traceBundle = bundle;
243
+ spanId = sid;
244
+ await bundle.writeEvent({
245
+ type: "error",
246
+ timestamp: Date.now(),
247
+ spanId: sid,
248
+ parentSpanId: null,
249
+ error: {
250
+ name: error.name || "Error",
251
+ message: error.message || String(error),
252
+ stack: error.stack || undefined,
253
+ },
254
+ });
255
+ await bundle.complete({
256
+ method: 'ERROR',
257
+ path: '/error',
258
+ headers: {},
259
+ });
260
+ if (uploadUrl && apiKey) {
261
+ const actualTraceDir = traceDir || './traces';
262
+ const tracePath = (0, path_1.join)(actualTraceDir, bundle.traceId);
263
+ (0, trace_uploader_1.uploadTrace)({
264
+ uploadUrl,
265
+ apiKey,
266
+ traceDir: actualTraceDir,
267
+ traceId: bundle.traceId,
268
+ serviceName,
269
+ serviceVersion,
270
+ environment,
271
+ }).then(async (result) => {
272
+ if (result.success) {
273
+ try {
274
+ // Delete local trace dir after successful upload to prevent re-upload and unbounded disk growth
275
+ await (0, promises_1.rm)(tracePath, { recursive: true, force: true });
276
+ }
277
+ catch (_) { }
278
+ }
279
+ }).catch(() => { });
280
+ }
281
+ }
282
+ catch (err) {
283
+ fastify.log.error(`[SDK] Failed to capture error trace: ${err}`);
284
+ }
218
285
  return;
219
286
  }
287
+ if (!traceBundle || !spanId)
288
+ return;
220
289
  try {
221
290
  await traceBundle.writeEvent({
222
291
  type: "error",
@@ -1 +1 @@
1
- {"version":3,"file":"http-wrapper.d.ts","sourceRoot":"","sources":["../src/http-wrapper.ts"],"names":[],"mappings":"AAwBA;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAAC,SAAS,EAAE,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;CAAE,GAAG,IAAI,EACvI,MAAM,EAAE,MAAM,GAAG,IAAI,QAQtB;AAED;;GAEG;AACH,wBAAgB,eAAe,IAAI;IACjC,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;QAAC,SAAS,EAAE,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;KAAE,GAAG,IAAI,CAAC;IACxI,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB,CAMA;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,aAAa,EAAE,OAAO,KAAK,GAAG,OAAO,KAAK,CA2dnE"}
1
+ {"version":3,"file":"http-wrapper.d.ts","sourceRoot":"","sources":["../src/http-wrapper.ts"],"names":[],"mappings":"AAwBA;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAAC,SAAS,EAAE,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;CAAE,GAAG,IAAI,EACvI,MAAM,EAAE,MAAM,GAAG,IAAI,QAQtB;AAED;;GAEG;AACH,wBAAgB,eAAe,IAAI;IACjC,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;QAAC,SAAS,EAAE,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;KAAE,GAAG,IAAI,CAAC;IACxI,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB,CAMA;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,aAAa,EAAE,OAAO,KAAK,GAAG,OAAO,KAAK,CA0gBnE"}
@@ -64,10 +64,53 @@ function wrapFetch(originalFetch) {
64
64
  // - If peerService is null → route to mock server (stub)
65
65
  if (peerService) {
66
66
  if (subgraphServices.includes(peerService)) {
67
- // Call is to a service in subgraph - should route to actual service
68
- // TODO: Implement actual service routing (requires service discovery/URL mapping)
69
- // For now, route to mock server as fallback
70
- shouldRouteToMock = true;
67
+ // Call is to a service in subgraph - route to actual service
68
+ // Check for service URL mapping via environment variables
69
+ const serviceUrlEnv = process.env[`${peerService.toUpperCase().replace(/-/g, '_')}_URL`];
70
+ const serviceUrls = process.env.SERVICE_URLS || '';
71
+ // Parse SERVICE_URLS: "service-a:http://localhost:3001,service-b:http://localhost:3002"
72
+ // Note: Split on first ':' only to handle URLs with colons
73
+ const urlMap = new Map();
74
+ if (serviceUrls) {
75
+ for (const mapping of serviceUrls.split(',')) {
76
+ const colonIndex = mapping.indexOf(':');
77
+ if (colonIndex > 0) {
78
+ const service = mapping.substring(0, colonIndex).trim();
79
+ const url = mapping.substring(colonIndex + 1).trim();
80
+ if (service && url) {
81
+ urlMap.set(service, url);
82
+ }
83
+ }
84
+ }
85
+ }
86
+ const serviceUrl = serviceUrlEnv || urlMap.get(peerService);
87
+ if (serviceUrl) {
88
+ // Route to actual service
89
+ try {
90
+ const path = urlObj.pathname + urlObj.search;
91
+ const fullUrl = new URL(path, serviceUrl).toString();
92
+ // Get trace context for propagation
93
+ const { bundle, spanId } = getTraceContext();
94
+ // Make request to actual service with trace headers
95
+ const enhancedInit = {
96
+ ...init,
97
+ headers: {
98
+ ...(init?.headers || {}),
99
+ ...(bundle?.traceId ? { 'x-trace-id': bundle.traceId } : {}),
100
+ ...(spanId ? { 'x-span-id': spanId } : {}),
101
+ },
102
+ };
103
+ return await originalFetch(fullUrl, enhancedInit);
104
+ }
105
+ catch (error) {
106
+ console.warn(`[SDK] Failed to route to ${peerService}: ${error}`);
107
+ shouldRouteToMock = true;
108
+ }
109
+ }
110
+ else {
111
+ // No service URL configured, route to mock
112
+ shouldRouteToMock = true;
113
+ }
71
114
  }
72
115
  else if (stubServices.includes(peerService)) {
73
116
  // Call is to a stubbed service - route to mock server
@@ -55,6 +55,7 @@ export declare function createRedactedValue(originalValue: string, includeHash?:
55
55
  export declare function loadRedactionConfig(configPath?: string): RedactionConfig;
56
56
  /**
57
57
  * Set global redaction config
58
+ * SECURITY: Redaction cannot be disabled - any attempt to set enabled: false will throw an error
58
59
  */
59
60
  export declare function setRedactionConfig(config: Partial<RedactionConfig>): void;
60
61
  /**
@@ -63,38 +64,47 @@ export declare function setRedactionConfig(config: Partial<RedactionConfig>): vo
63
64
  export declare function getRedactionConfig(): RedactionConfig;
64
65
  /**
65
66
  * Reset redaction config to defaults
67
+ * SECURITY: Always ensures enabled = true
66
68
  */
67
69
  export declare function resetRedactionConfig(): void;
68
70
  /**
69
71
  * Check if a header name should be redacted
72
+ * SECURITY: Redaction is always enabled - enabled check removed for enforcement
70
73
  */
71
74
  export declare function shouldRedactHeader(headerName: string): boolean;
72
75
  /**
73
76
  * Check if a query param should be redacted
77
+ * SECURITY: Redaction is always enabled - enabled check removed for enforcement
74
78
  */
75
79
  export declare function shouldRedactQueryParam(paramName: string): boolean;
76
80
  /**
77
81
  * Redact headers in a headers object
82
+ * SECURITY: Redaction is always enforced - enabled check removed
78
83
  */
79
84
  export declare function redactHeaders(headers: Record<string, string | string[] | undefined>): Record<string, string | string[] | undefined>;
80
85
  /**
81
86
  * Redact query parameters
87
+ * SECURITY: Redaction is always enforced - enabled check removed
82
88
  */
83
89
  export declare function redactQueryParams(query: Record<string, any>): Record<string, any>;
84
90
  /**
85
91
  * Redact sensitive fields from a body object (recursive)
92
+ * SECURITY: Redaction is always enforced - enabled check removed
86
93
  */
87
94
  export declare function redactBodyObject(body: any): any;
88
95
  /**
89
96
  * Redact patterns from a body string
97
+ * SECURITY: Redaction is always enforced - enabled check removed
90
98
  */
91
99
  export declare function redactBodyPatterns(body: string): string;
92
100
  /**
93
101
  * Redact a body (handles both string and object)
102
+ * SECURITY: Redaction is always enforced - enabled check removed
94
103
  */
95
104
  export declare function redactBody(body: string | object | null | undefined): string | object | null | undefined;
96
105
  /**
97
106
  * Redact a URL (query params)
107
+ * SECURITY: Redaction is always enforced - enabled check removed
98
108
  */
99
109
  export declare function redactUrl(url: string): string;
100
110
  /**
@@ -1 +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"}
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;AAoOD;;;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,CAiDxE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,OAAO,CAAC,eAAe,CAAC,GAAG,IAAI,CAiBzE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,eAAe,CAEpD;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAM3C;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAkB9D;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAkBjE;AAED;;;GAGG;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,CAyB/C;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GACzB,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAuBrB;AAsBD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,GAAG,GAAG,GAAG,CAmC/C;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAyBvD;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,GACvC,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CA8BpC;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA8B7C;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"}
package/dist/redaction.js CHANGED
@@ -66,8 +66,10 @@ const DEFAULT_SENSITIVE_QUERY_PARAMS = [
66
66
  ];
67
67
  /**
68
68
  * Default sensitive body field names (case-insensitive)
69
+ * Includes PII (Personally Identifiable Information) fields for GDPR/compliance
69
70
  */
70
71
  const DEFAULT_SENSITIVE_BODY_FIELDS = [
72
+ // Authentication & Secrets
71
73
  "password",
72
74
  "passwd",
73
75
  "pwd",
@@ -84,20 +86,86 @@ const DEFAULT_SENSITIVE_BODY_FIELDS = [
84
86
  "secretKey",
85
87
  "client_secret",
86
88
  "clientSecret",
87
- "ssn",
88
- "social_security",
89
+ "pin",
90
+ // Financial Information
89
91
  "credit_card",
90
92
  "creditCard",
91
93
  "card_number",
92
94
  "cardNumber",
93
95
  "cvv",
94
96
  "cvc",
95
- "pin",
97
+ "bank_account",
98
+ "bankAccount",
99
+ "routing_number",
100
+ "routingNumber",
101
+ // PII - Personal Identifiers
102
+ "ssn",
103
+ "social_security",
104
+ "social_security_number",
105
+ "tax_id",
106
+ "taxId",
107
+ "driver_license",
108
+ "driverLicense",
109
+ "passport",
110
+ "national_id",
111
+ "nationalId",
112
+ // PII - Contact Information
113
+ "email",
114
+ "email_address",
115
+ "emailAddress",
116
+ "phone",
117
+ "phone_number",
118
+ "phoneNumber",
119
+ "mobile",
120
+ "mobile_number",
121
+ "mobileNumber",
122
+ "telephone",
123
+ "address",
124
+ "street_address",
125
+ "streetAddress",
126
+ "home_address",
127
+ "homeAddress",
128
+ "billing_address",
129
+ "billingAddress",
130
+ "shipping_address",
131
+ "shippingAddress",
132
+ // PII - Personal Details
133
+ "first_name",
134
+ "firstName",
135
+ "last_name",
136
+ "lastName",
137
+ "full_name",
138
+ "fullName",
139
+ "name",
140
+ "date_of_birth",
141
+ "dateOfBirth",
142
+ "dob",
143
+ "birth_date",
144
+ "birthDate",
145
+ "age",
146
+ "gender",
147
+ "race",
148
+ "ethnicity",
149
+ // PII - Location
150
+ "city",
151
+ "state",
152
+ "province",
153
+ "zip",
154
+ "zip_code",
155
+ "zipCode",
156
+ "postal_code",
157
+ "postalCode",
158
+ "country",
159
+ "latitude",
160
+ "longitude",
161
+ "coordinates",
96
162
  ];
97
163
  /**
98
164
  * Default body patterns to redact (regex)
165
+ * Includes PII pattern detection for comprehensive data protection
99
166
  */
100
167
  const DEFAULT_SENSITIVE_BODY_PATTERNS = [
168
+ // Authentication tokens
101
169
  // Bearer tokens
102
170
  /Bearer\s+[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]*/g,
103
171
  // JWT tokens
@@ -106,8 +174,21 @@ const DEFAULT_SENSITIVE_BODY_PATTERNS = [
106
174
  /(?:api[_-]?key|apikey)[=:]["']?[A-Za-z0-9\-_]{20,}["']?/gi,
107
175
  // AWS access keys
108
176
  /AKIA[0-9A-Z]{16}/g,
177
+ // Financial Information
109
178
  // Credit card numbers (basic pattern)
110
179
  /\b(?:\d{4}[- ]?){3}\d{4}\b/g,
180
+ // PII - Email addresses (pattern-based detection)
181
+ /\b[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\b/g,
182
+ // PII - Phone numbers (US and international formats)
183
+ /\b\+?[\d\s\-()]{10,}\b/g,
184
+ // PII - Social Security Numbers (US format)
185
+ /\b\d{3}-\d{2}-\d{4}\b/g,
186
+ /\b\d{9}\b/g, // 9 consecutive digits (potential SSN)
187
+ // PII - Dates that might be DOB (MM/DD/YYYY, YYYY-MM-DD, etc.)
188
+ /\b(0[1-9]|1[0-2])[\/\-](0[1-9]|[12]\d|3[01])[\/\-]\d{4}\b/g,
189
+ /\b\d{4}[\/\-](0[1-9]|1[0-2])[\/\-](0[1-9]|[12]\d|3[01])\b/g,
190
+ // PII - IP addresses (may contain sensitive location data)
191
+ /\b(?:\d{1,3}\.){3}\d{1,3}\b/g,
111
192
  ];
112
193
  /**
113
194
  * Build default redaction rules
@@ -150,11 +231,12 @@ function buildDefaultRules() {
150
231
  }
151
232
  /**
152
233
  * Default redaction configuration
234
+ * SECURITY: Redaction is ALWAYS enabled and cannot be disabled for compliance
153
235
  */
154
236
  const DEFAULT_CONFIG = {
155
- enabled: true,
237
+ enabled: true, // Always true - cannot be changed
156
238
  rules: buildDefaultRules(),
157
- hashSalt: "rdt-redaction-salt-v1",
239
+ hashSalt: process.env.REDACTION_SALT || process.env.REDACTION_HASH_SALT || "rdt-redaction-salt-v1",
158
240
  };
159
241
  /**
160
242
  * Global redaction config (can be overridden)
@@ -201,14 +283,20 @@ function loadRedactionConfig(configPath) {
201
283
  if (config.redaction) {
202
284
  const userConfig = config.redaction;
203
285
  // Merge with defaults
286
+ // SECURITY: Always enforce enabled = true, ignore user attempts to disable
204
287
  const mergedConfig = {
205
- enabled: userConfig.enabled ?? true,
288
+ enabled: true, // Always true - cannot be disabled
206
289
  rules: userConfig.overrideDefaults
207
290
  ? userConfig.rules || []
208
291
  : [...buildDefaultRules(), ...(userConfig.rules || [])],
209
292
  overrideDefaults: userConfig.overrideDefaults,
210
293
  hashSalt: userConfig.hashSalt || DEFAULT_CONFIG.hashSalt,
211
294
  };
295
+ // Warn if user tried to disable redaction
296
+ if (userConfig.enabled === false) {
297
+ console.warn('[Redaction] Attempted to disable redaction in config file. ' +
298
+ 'Redaction is always enabled for security compliance. Ignoring enabled: false.');
299
+ }
212
300
  return mergedConfig;
213
301
  }
214
302
  }
@@ -221,11 +309,18 @@ function loadRedactionConfig(configPath) {
221
309
  }
222
310
  /**
223
311
  * Set global redaction config
312
+ * SECURITY: Redaction cannot be disabled - any attempt to set enabled: false will throw an error
224
313
  */
225
314
  function setRedactionConfig(config) {
315
+ // SECURITY: Prevent disabling redaction for compliance
316
+ if (config.enabled === false) {
317
+ throw new Error('Redaction cannot be disabled for security and compliance requirements. ' +
318
+ 'All sensitive data must be redacted before storage.');
319
+ }
226
320
  globalConfig = {
227
321
  ...DEFAULT_CONFIG,
228
322
  ...config,
323
+ enabled: true, // Always enforce enabled = true
229
324
  rules: config.overrideDefaults
230
325
  ? config.rules || []
231
326
  : [...buildDefaultRules(), ...(config.rules || [])],
@@ -239,16 +334,22 @@ function getRedactionConfig() {
239
334
  }
240
335
  /**
241
336
  * Reset redaction config to defaults
337
+ * SECURITY: Always ensures enabled = true
242
338
  */
243
339
  function resetRedactionConfig() {
244
- globalConfig = { ...DEFAULT_CONFIG, rules: buildDefaultRules() };
340
+ globalConfig = {
341
+ ...DEFAULT_CONFIG,
342
+ enabled: true, // Always enforce enabled
343
+ rules: buildDefaultRules()
344
+ };
245
345
  }
246
346
  /**
247
347
  * Check if a header name should be redacted
348
+ * SECURITY: Redaction is always enabled - enabled check removed for enforcement
248
349
  */
249
350
  function shouldRedactHeader(headerName) {
250
- if (!globalConfig.enabled)
251
- return false;
351
+ // SECURITY: Redaction is always enabled - removed bypass check
352
+ // if (!globalConfig.enabled) return false; // REMOVED - cannot bypass
252
353
  const lowerName = headerName.toLowerCase();
253
354
  for (const rule of globalConfig.rules) {
254
355
  if (rule.type !== "header")
@@ -267,10 +368,11 @@ function shouldRedactHeader(headerName) {
267
368
  }
268
369
  /**
269
370
  * Check if a query param should be redacted
371
+ * SECURITY: Redaction is always enabled - enabled check removed for enforcement
270
372
  */
271
373
  function shouldRedactQueryParam(paramName) {
272
- if (!globalConfig.enabled)
273
- return false;
374
+ // SECURITY: Redaction is always enabled - removed bypass check
375
+ // if (!globalConfig.enabled) return false; // REMOVED - cannot bypass
274
376
  const lowerName = paramName.toLowerCase();
275
377
  for (const rule of globalConfig.rules) {
276
378
  if (rule.type !== "query_param")
@@ -289,10 +391,11 @@ function shouldRedactQueryParam(paramName) {
289
391
  }
290
392
  /**
291
393
  * Redact headers in a headers object
394
+ * SECURITY: Redaction is always enforced - enabled check removed
292
395
  */
293
396
  function redactHeaders(headers) {
294
- if (!globalConfig.enabled)
295
- return headers;
397
+ // SECURITY: Redaction is always enabled - removed bypass check
398
+ // if (!globalConfig.enabled) return headers; // REMOVED - cannot bypass
296
399
  const redacted = {};
297
400
  for (const [key, value] of Object.entries(headers)) {
298
401
  if (value === undefined) {
@@ -311,10 +414,11 @@ function redactHeaders(headers) {
311
414
  }
312
415
  /**
313
416
  * Redact query parameters
417
+ * SECURITY: Redaction is always enforced - enabled check removed
314
418
  */
315
419
  function redactQueryParams(query) {
316
- if (!globalConfig.enabled)
317
- return query;
420
+ // SECURITY: Redaction is always enabled - removed bypass check
421
+ // if (!globalConfig.enabled) return query; // REMOVED - cannot bypass
318
422
  const redacted = {};
319
423
  for (const [key, value] of Object.entries(query)) {
320
424
  if (shouldRedactQueryParam(key)) {
@@ -352,10 +456,11 @@ function shouldRedactBodyField(fieldName) {
352
456
  }
353
457
  /**
354
458
  * Redact sensitive fields from a body object (recursive)
459
+ * SECURITY: Redaction is always enforced - enabled check removed
355
460
  */
356
461
  function redactBodyObject(body) {
357
- if (!globalConfig.enabled)
358
- return body;
462
+ // SECURITY: Redaction is always enabled - removed bypass check
463
+ // if (!globalConfig.enabled) return body; // REMOVED - cannot bypass
359
464
  if (body === null || body === undefined) {
360
465
  return body;
361
466
  }
@@ -382,10 +487,11 @@ function redactBodyObject(body) {
382
487
  }
383
488
  /**
384
489
  * Redact patterns from a body string
490
+ * SECURITY: Redaction is always enforced - enabled check removed
385
491
  */
386
492
  function redactBodyPatterns(body) {
387
- if (!globalConfig.enabled)
388
- return body;
493
+ // SECURITY: Redaction is always enabled - removed bypass check
494
+ // if (!globalConfig.enabled) return body; // REMOVED - cannot bypass
389
495
  let result = body;
390
496
  for (const rule of globalConfig.rules) {
391
497
  if (rule.type !== "body_pattern" || !rule.pattern)
@@ -405,10 +511,11 @@ function redactBodyPatterns(body) {
405
511
  }
406
512
  /**
407
513
  * Redact a body (handles both string and object)
514
+ * SECURITY: Redaction is always enforced - enabled check removed
408
515
  */
409
516
  function redactBody(body) {
410
- if (!globalConfig.enabled)
411
- return body;
517
+ // SECURITY: Redaction is always enabled - removed bypass check
518
+ // if (!globalConfig.enabled) return body; // REMOVED - cannot bypass
412
519
  if (body === null || body === undefined) {
413
520
  return body;
414
521
  }
@@ -436,10 +543,11 @@ function redactBody(body) {
436
543
  }
437
544
  /**
438
545
  * Redact a URL (query params)
546
+ * SECURITY: Redaction is always enforced - enabled check removed
439
547
  */
440
548
  function redactUrl(url) {
441
- if (!globalConfig.enabled)
442
- return url;
549
+ // SECURITY: Redaction is always enabled - removed bypass check
550
+ // if (!globalConfig.enabled) return url; // REMOVED - cannot bypass
443
551
  try {
444
552
  const urlObj = new URL(url, "http://placeholder");
445
553
  const redactedParams = new URLSearchParams();
@@ -32,19 +32,17 @@ export declare function createTrace(options: CreateTraceOptions): Promise<TraceB
32
32
  * Helper to process body content and store as blob
33
33
  * Applies redaction to sensitive fields before storing
34
34
  * NOTE: Always stores bodies as blobs so they can be retrieved during replay
35
+ * SECURITY: Redaction is ALWAYS enforced - skipRedaction option removed
35
36
  */
36
- export declare function processBody(body: string | Buffer | object | null | undefined, writeBlobFn: (content: string | object) => Promise<string>, options?: {
37
- skipRedaction?: boolean;
38
- }): Promise<{
37
+ export declare function processBody(body: string | Buffer | object | null | undefined, writeBlobFn: (content: string | object) => Promise<string>): Promise<{
39
38
  bodyHash: string | null;
40
39
  bodyBlob: string | null;
41
40
  }>;
42
41
  /**
43
42
  * Filter headers based on allowlist and apply redaction
43
+ * SECURITY: Redaction is ALWAYS enforced - skipRedaction option removed
44
44
  */
45
- export declare function filterHeaders(headers: Record<string, string | string[] | undefined>, allowlist?: string[], options?: {
46
- skipRedaction?: boolean;
47
- }): Record<string, string>;
45
+ export declare function filterHeaders(headers: Record<string, string | string[] | undefined>, allowlist?: string[]): Record<string, string>;
48
46
  /**
49
47
  * Generate span ID (exported for use in middleware)
50
48
  */
@@ -1 +1 @@
1
- {"version":3,"file":"trace-bundle-writer.d.ts","sourceRoot":"","sources":["../src/trace-bundle-writer.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,SAAS,EAAE,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACzD,QAAQ,EAAE,CAAC,cAAc,CAAC,EAAE;QAC1B,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACrB;AAED,OAAO,EAAE,4BAA4B,EAAE,MAAM,aAAa,CAAC;AAM3D,OAAO,EAAE,4BAA4B,EAAE,CAAC;AAWxC;;GAEG;AACH,iBAAS,cAAc,IAAI,MAAM,CAEhC;AAgBD;;GAEG;AACH,wBAAsB,WAAW,CAC/B,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,WAAW,CAAC,CAkLtB;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,EACjD,WAAW,EAAE,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,EAC1D,OAAO,CAAC,EAAE;IAAE,aAAa,CAAC,EAAE,OAAO,CAAA;CAAE,GACpC,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,CAiC7D;AAEH;;GAEG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,EACtD,SAAS,CAAC,EAAE,MAAM,EAAE,EACpB,OAAO,CAAC,EAAE;IAAE,aAAa,CAAC,EAAE,OAAO,CAAA;CAAE,GACpC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAyCxB;AAED;;GAEG;AACH,OAAO,EAAE,cAAc,EAAE,CAAC"}
1
+ {"version":3,"file":"trace-bundle-writer.d.ts","sourceRoot":"","sources":["../src/trace-bundle-writer.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,SAAS,EAAE,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACzD,QAAQ,EAAE,CAAC,cAAc,CAAC,EAAE;QAC1B,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACrB;AAED,OAAO,EAAE,4BAA4B,EAAE,MAAM,aAAa,CAAC;AAM3D,OAAO,EAAE,4BAA4B,EAAE,CAAC;AAWxC;;GAEG;AACH,iBAAS,cAAc,IAAI,MAAM,CAEhC;AAgBD;;GAEG;AACH,wBAAsB,WAAW,CAC/B,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,WAAW,CAAC,CAkLtB;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,EACjD,WAAW,EAAE,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAEzD,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,CA+B7D;AAEH;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,EACtD,SAAS,CAAC,EAAE,MAAM,EAAE,GAEnB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAqCxB;AAED;;GAEG;AACH,OAAO,EAAE,cAAc,EAAE,CAAC"}
@@ -190,8 +190,11 @@ async function createTrace(options) {
190
190
  * Helper to process body content and store as blob
191
191
  * Applies redaction to sensitive fields before storing
192
192
  * NOTE: Always stores bodies as blobs so they can be retrieved during replay
193
+ * SECURITY: Redaction is ALWAYS enforced - skipRedaction option removed
193
194
  */
194
- function processBody(body, writeBlobFn, options) {
195
+ function processBody(body, writeBlobFn
196
+ // SECURITY: Removed skipRedaction option - redaction is always enforced
197
+ ) {
195
198
  if (!body) {
196
199
  return Promise.resolve({ bodyHash: null, bodyBlob: null });
197
200
  }
@@ -205,15 +208,13 @@ function processBody(body, writeBlobFn, options) {
205
208
  else {
206
209
  bodyStr = String(body);
207
210
  }
208
- // Apply redaction unless explicitly skipped
209
- if (!options?.skipRedaction) {
210
- const redacted = (0, redaction_1.redactBody)(bodyStr);
211
- if (typeof redacted === "string") {
212
- bodyStr = redacted;
213
- }
214
- else if (redacted !== null && redacted !== undefined) {
215
- bodyStr = JSON.stringify(redacted);
216
- }
211
+ // SECURITY: Redaction is ALWAYS applied - no bypass option
212
+ const redacted = (0, redaction_1.redactBody)(bodyStr);
213
+ if (typeof redacted === "string") {
214
+ bodyStr = redacted;
215
+ }
216
+ else if (redacted !== null && redacted !== undefined) {
217
+ bodyStr = JSON.stringify(redacted);
217
218
  }
218
219
  const hash = computeHash(bodyStr);
219
220
  const hashFormatted = formatHash(hash);
@@ -226,8 +227,11 @@ function processBody(body, writeBlobFn, options) {
226
227
  }
227
228
  /**
228
229
  * Filter headers based on allowlist and apply redaction
230
+ * SECURITY: Redaction is ALWAYS enforced - skipRedaction option removed
229
231
  */
230
- function filterHeaders(headers, allowlist, options) {
232
+ function filterHeaders(headers, allowlist
233
+ // SECURITY: Removed skipRedaction option - redaction is always enforced
234
+ ) {
231
235
  if (!allowlist || allowlist.length === 0) {
232
236
  // Default allowlist: common headers that are safe to capture
233
237
  const defaultAllowlist = [
@@ -241,7 +245,7 @@ function filterHeaders(headers, allowlist, options) {
241
245
  "x-request-id",
242
246
  "x-correlation-id",
243
247
  ];
244
- return filterHeaders(headers, defaultAllowlist, options);
248
+ return filterHeaders(headers, defaultAllowlist);
245
249
  }
246
250
  const filtered = {};
247
251
  const lowerAllowlist = allowlist.map((h) => h.toLowerCase());
@@ -252,16 +256,13 @@ function filterHeaders(headers, allowlist, options) {
252
256
  filtered[key] = Array.isArray(value) ? value.join(", ") : String(value);
253
257
  }
254
258
  }
255
- // Apply redaction unless explicitly skipped
256
- if (!options?.skipRedaction) {
257
- const redacted = (0, redaction_1.redactHeaders)(filtered);
258
- const result = {};
259
- for (const [key, value] of Object.entries(redacted)) {
260
- if (value !== undefined) {
261
- result[key] = Array.isArray(value) ? value.join(", ") : String(value);
262
- }
259
+ // SECURITY: Redaction is ALWAYS applied - no bypass option
260
+ const redacted = (0, redaction_1.redactHeaders)(filtered);
261
+ const result = {};
262
+ for (const [key, value] of Object.entries(redacted)) {
263
+ if (value !== undefined) {
264
+ result[key] = Array.isArray(value) ? value.join(", ") : String(value);
263
265
  }
264
- return result;
265
266
  }
266
- return filtered;
267
+ return result;
267
268
  }
@@ -19,7 +19,8 @@ export interface TraceUploadResult {
19
19
  error?: string;
20
20
  }
21
21
  /**
22
- * Upload trace to ingestion endpoint
22
+ * Upload trace to ingestion endpoint with retries (5xx, 429, network errors).
23
+ * Exponential backoff: 1s, 2s, 4s (capped at 8s).
23
24
  */
24
25
  export declare function uploadTrace(options: TraceUploadOptions): Promise<TraceUploadResult>;
25
26
  //# sourceMappingURL=trace-uploader.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"trace-uploader.d.ts","sourceRoot":"","sources":["../src/trace-uploader.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA+CD;;GAEG;AACH,wBAAsB,WAAW,CAC/B,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,iBAAiB,CAAC,CAiF5B"}
1
+ {"version":3,"file":"trace-uploader.d.ts","sourceRoot":"","sources":["../src/trace-uploader.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAmED;;;GAGG;AACH,wBAAsB,WAAW,CAC/B,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,iBAAiB,CAAC,CAwI5B"}
@@ -53,8 +53,26 @@ async function readTraceMeta(metaPath) {
53
53
  throw new Error(`Failed to read meta.json: ${error.message}`);
54
54
  }
55
55
  }
56
+ const MAX_UPLOAD_ATTEMPTS = 3;
57
+ const INITIAL_BACKOFF_MS = 1000;
58
+ const MAX_BACKOFF_MS = 8000;
59
+ function isRetryableStatus(status) {
60
+ return status >= 500 || status === 429;
61
+ }
62
+ function sleep(ms) {
63
+ return new Promise(resolve => setTimeout(resolve, ms));
64
+ }
65
+ function logUpload(event) {
66
+ try {
67
+ console.warn('[SDK]', JSON.stringify({ event: 'trace_upload', ...event }));
68
+ }
69
+ catch {
70
+ console.warn('[SDK] trace_upload', event);
71
+ }
72
+ }
56
73
  /**
57
- * Upload trace to ingestion endpoint
74
+ * Upload trace to ingestion endpoint with retries (5xx, 429, network errors).
75
+ * Exponential backoff: 1s, 2s, 4s (capped at 8s).
58
76
  */
59
77
  async function uploadTrace(options) {
60
78
  const { uploadUrl, apiKey, traceDir, traceId, serviceName, serviceVersion, environment } = options;
@@ -89,7 +107,6 @@ async function uploadTrace(options) {
89
107
  correlationIds: meta.correlationIds || [],
90
108
  },
91
109
  events: events.map(event => {
92
- // Ensure all events have required base envelope fields
93
110
  return {
94
111
  ...event,
95
112
  traceId: event.traceId || traceId,
@@ -98,31 +115,77 @@ async function uploadTrace(options) {
98
115
  };
99
116
  }),
100
117
  };
101
- // Upload to ingestion endpoint
102
- const response = await fetch(uploadUrl, {
103
- method: 'POST',
104
- headers: {
105
- 'Content-Type': 'application/json',
106
- 'Authorization': `Bearer ${apiKey}`,
107
- },
108
- body: JSON.stringify(batch),
109
- });
110
- if (!response.ok) {
111
- const errorText = await response.text();
112
- return {
113
- success: false,
114
- traceId,
115
- error: `Upload failed: ${response.status} ${response.statusText} - ${errorText}`,
116
- };
118
+ let lastError;
119
+ let lastStatus;
120
+ for (let attempt = 1; attempt <= MAX_UPLOAD_ATTEMPTS; attempt++) {
121
+ logUpload({ traceId, attempt });
122
+ try {
123
+ const response = await fetch(uploadUrl, {
124
+ method: 'POST',
125
+ headers: {
126
+ 'Content-Type': 'application/json',
127
+ 'Authorization': `Bearer ${apiKey}`,
128
+ },
129
+ body: JSON.stringify(batch),
130
+ });
131
+ lastStatus = response.status;
132
+ logUpload({ traceId, attempt, statusCode: response.status });
133
+ if (response.ok) {
134
+ const result = await response.json();
135
+ logUpload({ traceId, attempt, statusCode: response.status, success: true });
136
+ return {
137
+ success: true,
138
+ traceId: result.traceId || traceId,
139
+ blobRoot: result.blobRoot,
140
+ };
141
+ }
142
+ const errorText = await response.text();
143
+ lastError = `${response.status} ${response.statusText} - ${errorText.substring(0, 200)}`;
144
+ if (!isRetryableStatus(response.status) || attempt === MAX_UPLOAD_ATTEMPTS) {
145
+ logUpload({ traceId, attempt, statusCode: response.status, success: false, error: lastError?.substring(0, 200) });
146
+ return {
147
+ success: false,
148
+ traceId,
149
+ error: lastError,
150
+ };
151
+ }
152
+ let backoffMs = Math.min(INITIAL_BACKOFF_MS * Math.pow(2, attempt - 1), MAX_BACKOFF_MS);
153
+ if (response.status === 429) {
154
+ const retryAfter = response.headers.get('Retry-After');
155
+ if (retryAfter != null) {
156
+ const retryAfterMs = /^\d+$/.test(retryAfter.trim())
157
+ ? parseInt(retryAfter.trim(), 10) * 1000
158
+ : 0;
159
+ if (retryAfterMs > 0) {
160
+ backoffMs = Math.min(backoffMs, retryAfterMs);
161
+ }
162
+ }
163
+ }
164
+ await sleep(backoffMs);
165
+ }
166
+ catch (networkError) {
167
+ lastError = networkError.message || String(networkError);
168
+ logUpload({ traceId, attempt, success: false, error: lastError?.substring(0, 200) });
169
+ if (attempt === MAX_UPLOAD_ATTEMPTS) {
170
+ return {
171
+ success: false,
172
+ traceId,
173
+ error: lastError,
174
+ };
175
+ }
176
+ const backoffMs = Math.min(INITIAL_BACKOFF_MS * Math.pow(2, attempt - 1), MAX_BACKOFF_MS);
177
+ await sleep(backoffMs);
178
+ }
117
179
  }
118
- const result = await response.json();
180
+ logUpload({ traceId, attempt: MAX_UPLOAD_ATTEMPTS, statusCode: lastStatus, success: false, error: lastError?.substring(0, 200) });
119
181
  return {
120
- success: true,
121
- traceId: result.traceId || traceId,
122
- blobRoot: result.blobRoot,
182
+ success: false,
183
+ traceId,
184
+ error: lastError || 'Upload failed after retries',
123
185
  };
124
186
  }
125
187
  catch (error) {
188
+ logUpload({ traceId, success: false, error: error.message?.substring(0, 200) });
126
189
  return {
127
190
  success: false,
128
191
  traceId,
package/package.json CHANGED
@@ -1,35 +1,28 @@
1
1
  {
2
2
  "name": "@runtime-digital-twin/sdk",
3
- "version": "1.0.0",
4
- "description": "SDK for capturing runtime behavior - automatic incident response and debugging",
3
+ "version": "1.0.3",
4
+ "description": "SDK for capturing runtime behavior - automatic incident response and debugging with enhanced autofix support",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "exports": {
8
8
  ".": {
9
9
  "types": "./dist/index.d.ts",
10
- "default": "./dist/index.js"
10
+ "default": "./src/index.ts"
11
11
  },
12
12
  "./src/*": "./src/*",
13
13
  "./dist/*": "./dist/*"
14
14
  },
15
- "scripts": {
16
- "build": "tsc",
17
- "dev": "tsc --watch",
18
- "test": "jest",
19
- "test:watch": "jest --watch",
20
- "prepublishOnly": "pnpm build"
21
- },
22
15
  "dependencies": {
23
- "@runtime-digital-twin/core": "^1.0.0",
24
16
  "fastify": "^4.24.0",
25
- "fastify-plugin": "^4.5.0"
17
+ "fastify-plugin": "^4.5.0",
18
+ "@runtime-digital-twin/core": "1.0.1"
26
19
  },
27
20
  "devDependencies": {
28
- "@jest/globals": "^29.7.0",
29
- "@types/jest": "^29.5.0",
21
+ "@jest/globals": "^30.2.0",
22
+ "@types/jest": "^30.0.0",
30
23
  "@types/node": "^20.0.0",
31
- "jest": "^29.7.0",
32
- "ts-jest": "^29.1.0",
24
+ "jest": "^30.2.0",
25
+ "ts-jest": "^29.4.6",
33
26
  "typescript": "^5.0.0"
34
27
  },
35
28
  "license": "MIT",
@@ -38,7 +31,7 @@
38
31
  },
39
32
  "repository": {
40
33
  "type": "git",
41
- "url": "git+https://github.com/your-org/WraithOnCallEngineer.git",
34
+ "url": "git+https://github.com/usewraith/wraith.git",
42
35
  "directory": "packages/sdk"
43
36
  },
44
37
  "keywords": [
@@ -58,6 +51,11 @@
58
51
  "dist",
59
52
  "README.md",
60
53
  "LICENSE"
61
- ]
62
- }
63
-
54
+ ],
55
+ "scripts": {
56
+ "build": "tsc",
57
+ "dev": "tsc --watch",
58
+ "test": "jest",
59
+ "test:watch": "jest --watch"
60
+ }
61
+ }