@tracemarketplace/shared 0.0.1 → 0.0.2

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/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export * from "./hash.js";
3
3
  export * from "./scoring.js";
4
4
  export * from "./utils.js";
5
5
  export * from "./validators.js";
6
+ export * from "./redact.js";
6
7
  export { extractClaudeCode } from "./extractors/claude-code.js";
7
8
  export { extractCodex } from "./extractors/codex.js";
8
9
  export { extractCursor } from "./extractors/cursor.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC"}
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ export * from "./hash.js";
3
3
  export * from "./scoring.js";
4
4
  export * from "./utils.js";
5
5
  export * from "./validators.js";
6
+ export * from "./redact.js";
6
7
  export { extractClaudeCode } from "./extractors/claude-code.js";
7
8
  export { extractCodex } from "./extractors/codex.js";
8
9
  export { extractCursor } from "./extractors/cursor.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC"}
@@ -0,0 +1,13 @@
1
+ import type { NormalizedTrace } from "./types.js";
2
+ export interface RedactOptions {
3
+ /** Pass os.homedir() from the CLI — strips absolute home paths from all strings. */
4
+ homeDir?: string;
5
+ }
6
+ /**
7
+ * Redact PII and secrets from a trace before it leaves the user's machine.
8
+ *
9
+ * Pass `homeDir: os.homedir()` from the CLI so absolute paths are stripped.
10
+ * The server runs a second pass (without homeDir) as a safety net.
11
+ */
12
+ export declare function redactTrace(trace: NormalizedTrace, opts?: RedactOptions): NormalizedTrace;
13
+ //# sourceMappingURL=redact.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redact.d.ts","sourceRoot":"","sources":["../src/redact.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAsB,MAAM,YAAY,CAAC;AAEtE,MAAM,WAAW,aAAa;IAC5B,oFAAoF;IACpF,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAoGD;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,eAAe,EACtB,IAAI,GAAE,aAAkB,GACvB,eAAe,CAuBjB"}
package/dist/redact.js ADDED
@@ -0,0 +1,114 @@
1
+ // ─── Secret patterns ────────────────────────────────────────────────────────
2
+ // Ordered from specific → generic to avoid partial matches being swallowed.
3
+ const SECRET_PATTERNS = [
4
+ // Anthropic
5
+ { re: /sk-ant-[a-zA-Z0-9\-_]{20,}/g, label: "ANTHROPIC_KEY" },
6
+ // OpenAI (must come before generic sk- catch-all)
7
+ { re: /sk-proj-[a-zA-Z0-9\-_]{20,}/g, label: "OPENAI_KEY" },
8
+ { re: /sk-[a-zA-Z0-9]{20,}/g, label: "OPENAI_KEY" },
9
+ // AWS
10
+ { re: /AKIA[0-9A-Z]{16}/g, label: "AWS_ACCESS_KEY" },
11
+ { re: /(aws_secret_access_key\s*[=:]\s*)[A-Za-z0-9/+]{40}/gi, label: "AWS_SECRET_KEY" },
12
+ // GitHub
13
+ { re: /github_pat_[a-zA-Z0-9_]{82}/g, label: "GITHUB_PAT" },
14
+ { re: /ghp_[a-zA-Z0-9]{36}/g, label: "GITHUB_TOKEN" },
15
+ { re: /ghs_[a-zA-Z0-9]{36}/g, label: "GITHUB_TOKEN" },
16
+ // Stripe
17
+ { re: /sk_live_[a-zA-Z0-9]{24,}/g, label: "STRIPE_SECRET_KEY" },
18
+ { re: /rk_live_[a-zA-Z0-9]{24,}/g, label: "STRIPE_RESTRICTED_KEY" },
19
+ // Resend
20
+ { re: /re_[a-zA-Z0-9]{32,}/g, label: "RESEND_KEY" },
21
+ // JWTs — eyJ<base64>.<base64>.<base64>
22
+ { re: /eyJ[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]*/g, label: "JWT" },
23
+ // Bearer tokens in Authorization headers
24
+ { re: /(Bearer\s+)[a-zA-Z0-9\-._~+/]+=*/gi, label: "BEARER_TOKEN" },
25
+ // Passwords in URLs: https://user:PASSWORD@host
26
+ { re: /(https?:\/\/[^:@\s]+:)[^:@\s]+(@)/g, label: "URL_PASSWORD" },
27
+ // Database DSNs: postgres://user:PASSWORD@host
28
+ { re: /((?:postgres(?:ql)?|mysql|redis):\/\/[^:]+:)[^@\s]+(@)/g, label: "DB_PASSWORD" },
29
+ // Generic key/secret assignments: API_KEY=abc123... or secret: "abc123..."
30
+ {
31
+ re: /((?:api[_-]?key|api[_-]?secret|access[_-]?token|auth[_-]?token|private[_-]?key|client[_-]?secret)\s*[=:]\s*["']?)[a-zA-Z0-9\-_.+/]{16,}(["']?)/gi,
32
+ label: "SECRET_VALUE",
33
+ },
34
+ ];
35
+ // ─── Core string transforms ──────────────────────────────────────────────────
36
+ function stripHome(s, home) {
37
+ return home ? s.replaceAll(home, "~") : s;
38
+ }
39
+ function stripSecrets(s) {
40
+ let out = s;
41
+ for (const { re, label } of SECRET_PATTERNS) {
42
+ // Patterns with capture groups: preserve group 1 (key name), replace group 2 (value)
43
+ if (re.source.includes("(")) {
44
+ out = out.replace(re, (...args) => {
45
+ // Replace only the non-group parts; keep named prefixes intact
46
+ const groups = args.slice(1, -2);
47
+ if (groups.length === 1)
48
+ return `${groups[0]}[${label}]`;
49
+ if (groups.length === 2)
50
+ return `${groups[0]}[${label}]${groups[1]}`;
51
+ return `[${label}]`;
52
+ });
53
+ }
54
+ else {
55
+ out = out.replace(re, `[${label}]`);
56
+ }
57
+ re.lastIndex = 0; // reset stateful global regexes
58
+ }
59
+ return out;
60
+ }
61
+ function redactString(s, home) {
62
+ return stripSecrets(stripHome(s, home));
63
+ }
64
+ // ─── Content block traversal ─────────────────────────────────────────────────
65
+ function redactToolInput(input, home) {
66
+ return Object.fromEntries(Object.entries(input).map(([k, v]) => [
67
+ k,
68
+ typeof v === "string" ? redactString(v, home) : v,
69
+ ]));
70
+ }
71
+ function redactBlock(block, home) {
72
+ switch (block.type) {
73
+ case "text":
74
+ case "thinking":
75
+ return { ...block, text: redactString(block.text, home) };
76
+ case "tool_use":
77
+ return { ...block, tool_input: redactToolInput(block.tool_input, home) };
78
+ case "tool_result":
79
+ return {
80
+ ...block,
81
+ result_content: block.result_content
82
+ ? redactString(block.result_content, home)
83
+ : null,
84
+ };
85
+ default:
86
+ return block;
87
+ }
88
+ }
89
+ // ─── Public API ───────────────────────────────────────────────────────────────
90
+ /**
91
+ * Redact PII and secrets from a trace before it leaves the user's machine.
92
+ *
93
+ * Pass `homeDir: os.homedir()` from the CLI so absolute paths are stripped.
94
+ * The server runs a second pass (without homeDir) as a safety net.
95
+ */
96
+ export function redactTrace(trace, opts = {}) {
97
+ const home = opts.homeDir ?? "";
98
+ return {
99
+ ...trace,
100
+ turns: trace.turns.map((turn) => ({
101
+ ...turn,
102
+ content: turn.content.map((b) => redactBlock(b, home)),
103
+ })),
104
+ env_state: trace.env_state
105
+ ? {
106
+ ...trace.env_state,
107
+ inferred_file_tree: trace.env_state.inferred_file_tree?.map((p) => stripHome(p, home)) ?? null,
108
+ inferred_changed_files: trace.env_state.inferred_changed_files?.map((p) => stripHome(p, home)) ?? null,
109
+ inferred_error_files: trace.env_state.inferred_error_files?.map((p) => stripHome(p, home)) ?? null,
110
+ }
111
+ : null,
112
+ };
113
+ }
114
+ //# sourceMappingURL=redact.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redact.js","sourceRoot":"","sources":["../src/redact.ts"],"names":[],"mappings":"AAOA,+EAA+E;AAC/E,4EAA4E;AAE5E,MAAM,eAAe,GAAyC;IAC5D,YAAY;IACZ,EAAE,EAAE,EAAE,6BAA6B,EAA2B,KAAK,EAAE,eAAe,EAAE;IACtF,kDAAkD;IAClD,EAAE,EAAE,EAAE,8BAA8B,EAA0B,KAAK,EAAE,YAAY,EAAE;IACnF,EAAE,EAAE,EAAE,sBAAsB,EAAmC,KAAK,EAAE,YAAY,EAAE;IACpF,MAAM;IACN,EAAE,EAAE,EAAE,mBAAmB,EAAsC,KAAK,EAAE,gBAAgB,EAAE;IACxF,EAAE,EAAE,EAAE,sDAAsD,EAAE,KAAK,EAAE,gBAAgB,EAAE;IACvF,SAAS;IACT,EAAE,EAAE,EAAE,8BAA8B,EAA0B,KAAK,EAAE,YAAY,EAAE;IACnF,EAAE,EAAE,EAAE,sBAAsB,EAAmC,KAAK,EAAE,cAAc,EAAE;IACtF,EAAE,EAAE,EAAE,sBAAsB,EAAmC,KAAK,EAAE,cAAc,EAAE;IACtF,SAAS;IACT,EAAE,EAAE,EAAE,2BAA2B,EAA8B,KAAK,EAAE,mBAAmB,EAAE;IAC3F,EAAE,EAAE,EAAE,2BAA2B,EAA8B,KAAK,EAAE,uBAAuB,EAAE;IAC/F,SAAS;IACT,EAAE,EAAE,EAAE,sBAAsB,EAAmC,KAAK,EAAE,YAAY,EAAE;IACpF,uCAAuC;IACvC,EAAE,EAAE,EAAE,uDAAuD,EAAE,KAAK,EAAE,KAAK,EAAE;IAC7E,yCAAyC;IACzC,EAAE,EAAE,EAAE,oCAAoC,EAAoB,KAAK,EAAE,cAAc,EAAE;IACrF,iDAAiD;IACjD,EAAE,EAAE,EAAE,oCAAoC,EAAoB,KAAK,EAAE,cAAc,EAAE;IACrF,gDAAgD;IAChD,EAAE,EAAE,EAAE,yDAAyD,EAAE,KAAK,EAAE,aAAa,EAAE;IACvF,8EAA8E;IAC9E;QACE,EAAE,EAAE,kJAAkJ;QACtJ,KAAK,EAAE,cAAc;KACtB;CACF,CAAC;AAEF,gFAAgF;AAEhF,SAAS,SAAS,CAAC,CAAS,EAAE,IAAY;IACxC,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC5C,CAAC;AAED,SAAS,YAAY,CAAC,CAAS;IAC7B,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,KAAK,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,eAAe,EAAE,CAAC;QAC5C,qFAAqF;QACrF,IAAI,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YAC5B,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,EAAE;gBAChC,+DAA+D;gBAC/D,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAa,CAAC;gBAC7C,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;oBAAE,OAAO,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,KAAK,GAAG,CAAC;gBACzD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;oBAAE,OAAO,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;gBACrE,OAAO,IAAI,KAAK,GAAG,CAAC;YACtB,CAAC,CAAC,CAAC;QACL,CAAC;aAAM,CAAC;YACN,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,EAAE,EAAE,IAAI,KAAK,GAAG,CAAC,CAAC;QACtC,CAAC;QACD,EAAE,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC,gCAAgC;IACpD,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,YAAY,CAAC,CAAS,EAAE,IAAY;IAC3C,OAAO,YAAY,CAAC,SAAS,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;AAC1C,CAAC;AAED,gFAAgF;AAEhF,SAAS,eAAe,CAAC,KAA8B,EAAE,IAAY;IACnE,OAAO,MAAM,CAAC,WAAW,CACvB,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC;QACpC,CAAC;QACD,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;KAClD,CAAC,CACH,CAAC;AACJ,CAAC;AAED,SAAS,WAAW,CAAC,KAAmB,EAAE,IAAY;IACpD,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;QACnB,KAAK,MAAM,CAAC;QACZ,KAAK,UAAU;YACb,OAAO,EAAE,GAAG,KAAK,EAAE,IAAI,EAAE,YAAY,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC;QAC5D,KAAK,UAAU;YACb,OAAO,EAAE,GAAG,KAAK,EAAE,UAAU,EAAE,eAAe,CAAC,KAAK,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,CAAC;QAC3E,KAAK,aAAa;YAChB,OAAO;gBACL,GAAG,KAAK;gBACR,cAAc,EAAE,KAAK,CAAC,cAAc;oBAClC,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,cAAc,EAAE,IAAI,CAAC;oBAC1C,CAAC,CAAC,IAAI;aACT,CAAC;QACJ;YACE,OAAO,KAAK,CAAC;IACjB,CAAC;AACH,CAAC;AAED,iFAAiF;AAEjF;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CACzB,KAAsB,EACtB,OAAsB,EAAE;IAExB,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC;IAEhC,OAAO;QACL,GAAG,KAAK;QACR,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,GAAG,CACpB,CAAC,IAAI,EAAQ,EAAE,CAAC,CAAC;YACf,GAAG,IAAI;YACP,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;SACvD,CAAC,CACH;QACD,SAAS,EAAE,KAAK,CAAC,SAAS;YACxB,CAAC,CAAC;gBACE,GAAG,KAAK,CAAC,SAAS;gBAClB,kBAAkB,EAChB,KAAK,CAAC,SAAS,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,IAAI,IAAI;gBAC5E,sBAAsB,EACpB,KAAK,CAAC,SAAS,CAAC,sBAAsB,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,IAAI,IAAI;gBAChF,oBAAoB,EAClB,KAAK,CAAC,SAAS,CAAC,oBAAoB,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,IAAI,IAAI;aAC/E;YACH,CAAC,CAAC,IAAI;KACT,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tracemarketplace/shared",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
package/src/index.ts CHANGED
@@ -3,6 +3,7 @@ export * from "./hash.js";
3
3
  export * from "./scoring.js";
4
4
  export * from "./utils.js";
5
5
  export * from "./validators.js";
6
+ export * from "./redact.js";
6
7
  export { extractClaudeCode } from "./extractors/claude-code.js";
7
8
  export { extractCodex } from "./extractors/codex.js";
8
9
  export { extractCursor } from "./extractors/cursor.js";
package/src/redact.ts ADDED
@@ -0,0 +1,138 @@
1
+ import type { NormalizedTrace, ContentBlock, Turn } from "./types.js";
2
+
3
+ export interface RedactOptions {
4
+ /** Pass os.homedir() from the CLI — strips absolute home paths from all strings. */
5
+ homeDir?: string;
6
+ }
7
+
8
+ // ─── Secret patterns ────────────────────────────────────────────────────────
9
+ // Ordered from specific → generic to avoid partial matches being swallowed.
10
+
11
+ const SECRET_PATTERNS: Array<{ re: RegExp; label: string }> = [
12
+ // Anthropic
13
+ { re: /sk-ant-[a-zA-Z0-9\-_]{20,}/g, label: "ANTHROPIC_KEY" },
14
+ // OpenAI (must come before generic sk- catch-all)
15
+ { re: /sk-proj-[a-zA-Z0-9\-_]{20,}/g, label: "OPENAI_KEY" },
16
+ { re: /sk-[a-zA-Z0-9]{20,}/g, label: "OPENAI_KEY" },
17
+ // AWS
18
+ { re: /AKIA[0-9A-Z]{16}/g, label: "AWS_ACCESS_KEY" },
19
+ { re: /(aws_secret_access_key\s*[=:]\s*)[A-Za-z0-9/+]{40}/gi, label: "AWS_SECRET_KEY" },
20
+ // GitHub
21
+ { re: /github_pat_[a-zA-Z0-9_]{82}/g, label: "GITHUB_PAT" },
22
+ { re: /ghp_[a-zA-Z0-9]{36}/g, label: "GITHUB_TOKEN" },
23
+ { re: /ghs_[a-zA-Z0-9]{36}/g, label: "GITHUB_TOKEN" },
24
+ // Stripe
25
+ { re: /sk_live_[a-zA-Z0-9]{24,}/g, label: "STRIPE_SECRET_KEY" },
26
+ { re: /rk_live_[a-zA-Z0-9]{24,}/g, label: "STRIPE_RESTRICTED_KEY" },
27
+ // Resend
28
+ { re: /re_[a-zA-Z0-9]{32,}/g, label: "RESEND_KEY" },
29
+ // JWTs — eyJ<base64>.<base64>.<base64>
30
+ { re: /eyJ[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]*/g, label: "JWT" },
31
+ // Bearer tokens in Authorization headers
32
+ { re: /(Bearer\s+)[a-zA-Z0-9\-._~+/]+=*/gi, label: "BEARER_TOKEN" },
33
+ // Passwords in URLs: https://user:PASSWORD@host
34
+ { re: /(https?:\/\/[^:@\s]+:)[^:@\s]+(@)/g, label: "URL_PASSWORD" },
35
+ // Database DSNs: postgres://user:PASSWORD@host
36
+ { re: /((?:postgres(?:ql)?|mysql|redis):\/\/[^:]+:)[^@\s]+(@)/g, label: "DB_PASSWORD" },
37
+ // Generic key/secret assignments: API_KEY=abc123... or secret: "abc123..."
38
+ {
39
+ re: /((?:api[_-]?key|api[_-]?secret|access[_-]?token|auth[_-]?token|private[_-]?key|client[_-]?secret)\s*[=:]\s*["']?)[a-zA-Z0-9\-_.+/]{16,}(["']?)/gi,
40
+ label: "SECRET_VALUE",
41
+ },
42
+ ];
43
+
44
+ // ─── Core string transforms ──────────────────────────────────────────────────
45
+
46
+ function stripHome(s: string, home: string): string {
47
+ return home ? s.replaceAll(home, "~") : s;
48
+ }
49
+
50
+ function stripSecrets(s: string): string {
51
+ let out = s;
52
+ for (const { re, label } of SECRET_PATTERNS) {
53
+ // Patterns with capture groups: preserve group 1 (key name), replace group 2 (value)
54
+ if (re.source.includes("(")) {
55
+ out = out.replace(re, (...args) => {
56
+ // Replace only the non-group parts; keep named prefixes intact
57
+ const groups = args.slice(1, -2) as string[];
58
+ if (groups.length === 1) return `${groups[0]}[${label}]`;
59
+ if (groups.length === 2) return `${groups[0]}[${label}]${groups[1]}`;
60
+ return `[${label}]`;
61
+ });
62
+ } else {
63
+ out = out.replace(re, `[${label}]`);
64
+ }
65
+ re.lastIndex = 0; // reset stateful global regexes
66
+ }
67
+ return out;
68
+ }
69
+
70
+ function redactString(s: string, home: string): string {
71
+ return stripSecrets(stripHome(s, home));
72
+ }
73
+
74
+ // ─── Content block traversal ─────────────────────────────────────────────────
75
+
76
+ function redactToolInput(input: Record<string, unknown>, home: string): Record<string, unknown> {
77
+ return Object.fromEntries(
78
+ Object.entries(input).map(([k, v]) => [
79
+ k,
80
+ typeof v === "string" ? redactString(v, home) : v,
81
+ ])
82
+ );
83
+ }
84
+
85
+ function redactBlock(block: ContentBlock, home: string): ContentBlock {
86
+ switch (block.type) {
87
+ case "text":
88
+ case "thinking":
89
+ return { ...block, text: redactString(block.text, home) };
90
+ case "tool_use":
91
+ return { ...block, tool_input: redactToolInput(block.tool_input, home) };
92
+ case "tool_result":
93
+ return {
94
+ ...block,
95
+ result_content: block.result_content
96
+ ? redactString(block.result_content, home)
97
+ : null,
98
+ };
99
+ default:
100
+ return block;
101
+ }
102
+ }
103
+
104
+ // ─── Public API ───────────────────────────────────────────────────────────────
105
+
106
+ /**
107
+ * Redact PII and secrets from a trace before it leaves the user's machine.
108
+ *
109
+ * Pass `homeDir: os.homedir()` from the CLI so absolute paths are stripped.
110
+ * The server runs a second pass (without homeDir) as a safety net.
111
+ */
112
+ export function redactTrace(
113
+ trace: NormalizedTrace,
114
+ opts: RedactOptions = {}
115
+ ): NormalizedTrace {
116
+ const home = opts.homeDir ?? "";
117
+
118
+ return {
119
+ ...trace,
120
+ turns: trace.turns.map(
121
+ (turn): Turn => ({
122
+ ...turn,
123
+ content: turn.content.map((b) => redactBlock(b, home)),
124
+ })
125
+ ),
126
+ env_state: trace.env_state
127
+ ? {
128
+ ...trace.env_state,
129
+ inferred_file_tree:
130
+ trace.env_state.inferred_file_tree?.map((p) => stripHome(p, home)) ?? null,
131
+ inferred_changed_files:
132
+ trace.env_state.inferred_changed_files?.map((p) => stripHome(p, home)) ?? null,
133
+ inferred_error_files:
134
+ trace.env_state.inferred_error_files?.map((p) => stripHome(p, home)) ?? null,
135
+ }
136
+ : null,
137
+ };
138
+ }