@optimuslabs/pii-scrub-staging 0.0.0-stage → 0.1.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.
package/README.md CHANGED
@@ -1,3 +1,22 @@
1
- # Temporary Holding Version
1
+ # @optimuslabs/pii-scrub
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Redacts secrets, and optionally personal data (email names, home-directory usernames), from what an
4
+ Optimus endpoint sends, before the request leaves the machine. Used by `@optimuslabs/runtime`
5
+ (tool calls), `@optimuslabs/harness-map` (config file inventory) and `@optimuslabs/discovery`
6
+ (which sends the version). Design: `docs/pii-scrubbing/DESIGN.md`.
7
+
8
+ ```ts
9
+ import { SCRUBBER_VERSION, scrub, scrubText, scrubValue } from "@optimuslabs/pii-scrub";
10
+
11
+ scrubValue({ env: { BRAVE_API_KEY: "BSA..." } }); // { env: { BRAVE_API_KEY: "[OL_REDACTED:CREDENTIAL_ENV_VAR]" } }
12
+ scrubText("key sk-ant-api03-...", { fingerprintKey }); // "key sk-ant-api03-[OL_REDACTED:ANTHROPIC_KEY:<24 hex>]"
13
+ scrubText("ada@gmail.com", { personalData: true }); // "[OL_REDACTED:EMAIL]@gmail.com"
14
+ scrub(payload).hits; // { ANTHROPIC_KEY: 1, ... }; placeholders already there are not hits
15
+ ```
16
+
17
+ The rules are in `rules.json`, which the backend reads too; every pattern works in both the
18
+ JavaScript and Python regex engines. Only vendor-format secrets are fingerprinted (HMAC-SHA256
19
+ under the org key, 96 bits), and only when the caller passes the key.
20
+
21
+ The staging build publishes as `@optimuslabs/pii-scrub-staging`, the production build as
22
+ `@optimuslabs/pii-scrub`.
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Redact secrets, and optionally personal data, from what an endpoint sends to Optimus.
3
+ *
4
+ * Runs on the endpoint, just before a request is built, so the value never leaves the machine.
5
+ * A value is replaced with a typed placeholder rather than dropped, so the backend still knows a
6
+ * credential was there and of what kind:
7
+ *
8
+ * sk-ant-api03-[OL_REDACTED:ANTHROPIC_KEY:<fp>] a vendor key keeps its published prefix
9
+ * [OL_REDACTED:CREDENTIAL_ENV_VAR] a value under a credential-named key
10
+ * [OL_REDACTED:ENV_VALUE] any other value in an MCP server's env block
11
+ * [OL_REDACTED:EMAIL]@gmail.com an email inside content keeps its domain
12
+ * /Users/[OL_REDACTED:USERNAME]/src a home directory keeps its path
13
+ *
14
+ * `<fp>` is a 96-bit fingerprint (24 hex characters, HMAC-SHA256 under the org's key), on
15
+ * vendor-format secrets only, so the same key is recognisable across configs and machines
16
+ * without being held. A credential-named value with no known format could be a guessable
17
+ * password, so it gets none. Without a key, nothing gets one.
18
+ *
19
+ * Structure first, then text: a 29-character API key matches no format, but nobody names a
20
+ * variable `BRAVE_API_KEY` unless it holds one. A placeholder already in the input is left as it
21
+ * is and is not a hit, so scrubbing twice changes nothing and counts nothing.
22
+ */
23
+ export { SECRET_FORMATS } from "./rules.js";
24
+ /** This scrubber's version (the package's), which every sender includes with what it scrubbed. */
25
+ export declare const SCRUBBER_VERSION: string;
26
+ export declare const PLACEHOLDER_PREFIX = "[OL_REDACTED:";
27
+ export interface ScrubOptions {
28
+ /**
29
+ * Also redact personal data: the name part of emails and the username in home directories. On
30
+ * for tool-call content; off for config files, where account emails are identity (agent owners,
31
+ * tenant approval, the personal-email finding) and stay readable.
32
+ */
33
+ personalData?: boolean;
34
+ /** The org's fingerprint key, hex. Absent until the endpoint has received it: no fingerprints. */
35
+ fingerprintKey?: string;
36
+ }
37
+ /** How many of each type were redacted. Placeholders already in the input are not counted. */
38
+ export type ScrubHits = Record<string, number>;
39
+ export interface ScrubResult<T> {
40
+ value: T;
41
+ hits: ScrubHits;
42
+ }
43
+ export declare function placeholder(type: string, fingerprint?: string): string;
44
+ /**
45
+ * A deep copy of `value` with every string scrubbed and every value under a credential-named key
46
+ * redacted, and how many of each type were redacted. Non-string leaves are returned as they are.
47
+ */
48
+ export declare function scrub<T>(value: T, options?: ScrubOptions): ScrubResult<T>;
49
+ /** `scrub(value).value`. */
50
+ export declare function scrubValue<T>(value: T, options?: ScrubOptions): T;
51
+ /** A copy of `text` with every secret, and with `personalData` emails and usernames, redacted. */
52
+ export declare function scrubText(text: string, options?: ScrubOptions): string;
53
+ /**
54
+ * A copy of `record` with each of `fields` that is a string scrubbed as text, the rest untouched:
55
+ * for log lines and run records (`command`, `message`, `detail`), where the fields are free text.
56
+ */
57
+ export declare function scrubFields<T extends Record<string, unknown>>(record: T, fields: readonly string[], options?: ScrubOptions): T;
58
+ /** Whether `value` holds a placeholder anywhere (to refuse writing scrubbed data back to disk). */
59
+ export declare function containsPlaceholder(value: unknown): boolean;
60
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAmBH,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE5C,kGAAkG;AAClG,eAAO,MAAM,gBAAgB,EAAE,MAA2F,CAAC;AAE3H,eAAO,MAAM,kBAAkB,kBAAkB,CAAC;AAElD,MAAM,WAAW,YAAY;IAC3B;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,kGAAkG;IAClG,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,8FAA8F;AAC9F,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAE/C,MAAM,WAAW,WAAW,CAAC,CAAC;IAC5B,KAAK,EAAE,CAAC,CAAC;IACT,IAAI,EAAE,SAAS,CAAC;CACjB;AAWD,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAEtE;AAgID;;;GAGG;AACH,wBAAgB,KAAK,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,GAAE,YAAiB,GAAG,WAAW,CAAC,CAAC,CAAC,CAG7E;AAED,4BAA4B;AAC5B,wBAAgB,UAAU,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,GAAE,YAAiB,GAAG,CAAC,CAErE;AAED,kGAAkG;AAClG,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,MAAM,CAE1E;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3D,MAAM,EAAE,CAAC,EACT,MAAM,EAAE,SAAS,MAAM,EAAE,EACzB,OAAO,GAAE,YAAiB,GACzB,CAAC,CAMH;AAED,mGAAmG;AACnG,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAO3D"}
package/dist/index.js ADDED
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Redact secrets, and optionally personal data, from what an endpoint sends to Optimus.
3
+ *
4
+ * Runs on the endpoint, just before a request is built, so the value never leaves the machine.
5
+ * A value is replaced with a typed placeholder rather than dropped, so the backend still knows a
6
+ * credential was there and of what kind:
7
+ *
8
+ * sk-ant-api03-[OL_REDACTED:ANTHROPIC_KEY:<fp>] a vendor key keeps its published prefix
9
+ * [OL_REDACTED:CREDENTIAL_ENV_VAR] a value under a credential-named key
10
+ * [OL_REDACTED:ENV_VALUE] any other value in an MCP server's env block
11
+ * [OL_REDACTED:EMAIL]@gmail.com an email inside content keeps its domain
12
+ * /Users/[OL_REDACTED:USERNAME]/src a home directory keeps its path
13
+ *
14
+ * `<fp>` is a 96-bit fingerprint (24 hex characters, HMAC-SHA256 under the org's key), on
15
+ * vendor-format secrets only, so the same key is recognisable across configs and machines
16
+ * without being held. A credential-named value with no known format could be a guessable
17
+ * password, so it gets none. Without a key, nothing gets one.
18
+ *
19
+ * Structure first, then text: a 29-character API key matches no format, but nobody names a
20
+ * variable `BRAVE_API_KEY` unless it holds one. A placeholder already in the input is left as it
21
+ * is and is not a hit, so scrubbing twice changes nothing and counts nothing.
22
+ */
23
+ import { createHmac } from "node:crypto";
24
+ import { createRequire } from "node:module";
25
+ import { CONTAINS_REFERENCE, CREDENTIAL_ASSIGNMENT, CREDENTIAL_FLAG, CREDENTIAL_KEY, EMAIL, ENV_REFERENCE, HOME_DIRECTORY, MCP_SERVER_KEYS, MCP_VALUE_BLOCKS, MIN_CREDENTIAL_LENGTH, RESERVED_EMAIL_DOMAIN, SECRET_FORMATS, } from "./rules.js";
26
+ export { SECRET_FORMATS } from "./rules.js";
27
+ /** This scrubber's version (the package's), which every sender includes with what it scrubbed. */
28
+ export const SCRUBBER_VERSION = createRequire(import.meta.url)("../package.json").version;
29
+ export const PLACEHOLDER_PREFIX = "[OL_REDACTED:";
30
+ /** How deep a payload may nest before the scrubber stops descending (and redacts the rest). */
31
+ const MAX_DEPTH = 32;
32
+ const FINGERPRINT_HEX_LENGTH = 24;
33
+ export function placeholder(type, fingerprint) {
34
+ return fingerprint ? `${PLACEHOLDER_PREFIX}${type}:${fingerprint}]` : `${PLACEHOLDER_PREFIX}${type}]`;
35
+ }
36
+ function redacted(pass, type, fingerprint) {
37
+ pass.hits[type] = (pass.hits[type] ?? 0) + 1;
38
+ return placeholder(type, fingerprint);
39
+ }
40
+ function fingerprintOf(pass, secret) {
41
+ const key = pass.options.fingerprintKey;
42
+ if (!key)
43
+ return undefined;
44
+ return createHmac("sha256", Buffer.from(key, "hex")).update(secret).digest("hex").slice(0, FINGERPRINT_HEX_LENGTH);
45
+ }
46
+ /** A value worth redacting under a credential-named key: not a flag, a reference or a placeholder. */
47
+ function isCredentialValue(value) {
48
+ const trimmed = value.trim();
49
+ return (trimmed.length >= MIN_CREDENTIAL_LENGTH &&
50
+ !/\s/.test(trimmed) &&
51
+ !ENV_REFERENCE.test(trimmed) &&
52
+ // A placeholder, or the front of one that a text rule cut at its closing `]`.
53
+ !trimmed.includes(PLACEHOLDER_PREFIX));
54
+ }
55
+ function redactFormats(text, pass) {
56
+ let out = text;
57
+ for (const format of SECRET_FORMATS) {
58
+ out = out.replace(format.pattern, (match, prefix) => {
59
+ const fingerprint = format.fingerprint ? fingerprintOf(pass, match) : undefined;
60
+ return `${typeof prefix === "string" ? prefix : ""}${redacted(pass, format.type, fingerprint)}`;
61
+ });
62
+ }
63
+ return out;
64
+ }
65
+ function redactAssignments(text, pass) {
66
+ return text.replace(CREDENTIAL_ASSIGNMENT, (match, name, value) => isCredentialValue(value) ? `${name}${redacted(pass, "CREDENTIAL_ENV_VAR")}` : match);
67
+ }
68
+ function redactPersonalData(text, pass) {
69
+ // `git@github.com:` is an SSH remote, and a reserved domain is documentation: neither is a person.
70
+ const emailsDone = text.replace(EMAIL, (match, domain) => match.startsWith("git@") || RESERVED_EMAIL_DOMAIN.test(domain)
71
+ ? match
72
+ : `${redacted(pass, "EMAIL")}@${domain}`);
73
+ return emailsDone.replace(HOME_DIRECTORY, (_match, root) => `${root}${redacted(pass, "USERNAME")}`);
74
+ }
75
+ function scrubString(text, pass) {
76
+ if (!text)
77
+ return text;
78
+ const out = redactAssignments(redactFormats(text, pass), pass);
79
+ return pass.options.personalData ? redactPersonalData(out, pass) : out;
80
+ }
81
+ function scrubLeaf(value, pass, key) {
82
+ if (key !== undefined && CREDENTIAL_KEY.test(key) && isCredentialValue(value)) {
83
+ // A recognised format keeps its prefix, type and fingerprint; anything else is just a credential.
84
+ const formatted = redactFormats(value, pass);
85
+ return formatted !== value ? formatted : redacted(pass, "CREDENTIAL_ENV_VAR");
86
+ }
87
+ return scrubString(value, pass);
88
+ }
89
+ function isPlainObject(value) {
90
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
91
+ }
92
+ /**
93
+ * A value in an MCP server's env or headers block. Every one is redacted, because a server's
94
+ * secrets sit there under whatever name its author chose: a known format keeps its prefix, type
95
+ * and fingerprint; a credential-named one is a credential; anything else is typed by the block
96
+ * (`ENV_VALUE`, `HEADER_VALUE`), which the backend does not read as a secret. A reference to a
97
+ * secret stored elsewhere (`${TOKEN}`, `Bearer ${TOKEN}`) stays readable.
98
+ */
99
+ function redactBlockValue(value, name, ordinaryType, pass) {
100
+ if (!value.trim() || CONTAINS_REFERENCE.test(value) || ENV_REFERENCE.test(value.trim()))
101
+ return value;
102
+ if (value.includes(PLACEHOLDER_PREFIX))
103
+ return value;
104
+ const formatted = redactFormats(value, pass);
105
+ if (formatted !== value)
106
+ return formatted;
107
+ return redacted(pass, CREDENTIAL_KEY.test(name) ? "CREDENTIAL_ENV_VAR" : ordinaryType);
108
+ }
109
+ function walkValueBlock(block, ordinaryType, pass, depth) {
110
+ const out = {};
111
+ for (const [name, v] of Object.entries(block)) {
112
+ out[name] = typeof v === "string" ? redactBlockValue(v, name, ordinaryType, pass) : walk(v, pass, name, depth + 1);
113
+ }
114
+ return out;
115
+ }
116
+ /** `["--api-key", "BSA..."]`: the argument after a credential flag is the credential. */
117
+ function walkArgs(args, pass, depth) {
118
+ return args.map((arg, i) => {
119
+ const flag = typeof args[i - 1] === "string" ? CREDENTIAL_FLAG.exec(args[i - 1]) : null;
120
+ if (flag && CREDENTIAL_KEY.test(flag[1]) && typeof arg === "string" && isCredentialValue(arg)) {
121
+ const formatted = redactFormats(arg, pass);
122
+ return formatted !== arg ? formatted : redacted(pass, "CREDENTIAL_ENV_VAR");
123
+ }
124
+ return walk(arg, pass, "args", depth + 1);
125
+ });
126
+ }
127
+ /** One child of an object: an MCP value block and `args` get their structural rule. */
128
+ function walkChild(key, value, inServer, pass, depth) {
129
+ const blockType = inServer ? MCP_VALUE_BLOCKS[key] : undefined;
130
+ if (blockType && isPlainObject(value))
131
+ return walkValueBlock(value, blockType, pass, depth);
132
+ if (key === "args" && Array.isArray(value))
133
+ return walkArgs(value, pass, depth);
134
+ return walk(value, pass, key, depth + 1);
135
+ }
136
+ function walkObject(obj, pass, depth) {
137
+ const inServer = Object.keys(obj).some((k) => MCP_SERVER_KEYS.has(k));
138
+ const out = {};
139
+ for (const [k, v] of Object.entries(obj))
140
+ out[k] = walkChild(k, v, inServer, pass, depth);
141
+ return out;
142
+ }
143
+ function walk(value, pass, key, depth) {
144
+ if (depth > MAX_DEPTH)
145
+ return redacted(pass, "TOO_DEEP");
146
+ if (Array.isArray(value))
147
+ return value.map((item) => walk(item, pass, key, depth + 1));
148
+ if (isPlainObject(value))
149
+ return walkObject(value, pass, depth);
150
+ return typeof value === "string" ? scrubLeaf(value, pass, key) : value;
151
+ }
152
+ /**
153
+ * A deep copy of `value` with every string scrubbed and every value under a credential-named key
154
+ * redacted, and how many of each type were redacted. Non-string leaves are returned as they are.
155
+ */
156
+ export function scrub(value, options = {}) {
157
+ const pass = { options, hits: {} };
158
+ return { value: walk(value, pass, undefined, 0), hits: pass.hits };
159
+ }
160
+ /** `scrub(value).value`. */
161
+ export function scrubValue(value, options = {}) {
162
+ return scrub(value, options).value;
163
+ }
164
+ /** A copy of `text` with every secret, and with `personalData` emails and usernames, redacted. */
165
+ export function scrubText(text, options = {}) {
166
+ return scrubString(text, { options, hits: {} });
167
+ }
168
+ /**
169
+ * A copy of `record` with each of `fields` that is a string scrubbed as text, the rest untouched:
170
+ * for log lines and run records (`command`, `message`, `detail`), where the fields are free text.
171
+ */
172
+ export function scrubFields(record, fields, options = {}) {
173
+ const out = { ...record };
174
+ for (const field of fields) {
175
+ if (typeof out[field] === "string")
176
+ out[field] = scrubText(out[field], options);
177
+ }
178
+ return out;
179
+ }
180
+ /** Whether `value` holds a placeholder anywhere (to refuse writing scrubbed data back to disk). */
181
+ export function containsPlaceholder(value) {
182
+ if (typeof value === "string")
183
+ return value.includes(PLACEHOLDER_PREFIX);
184
+ try {
185
+ return JSON.stringify(value ?? null).includes(PLACEHOLDER_PREFIX);
186
+ }
187
+ catch {
188
+ return false;
189
+ }
190
+ }
191
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The scrubber's rules, loaded from `rules.json` at the package root.
3
+ *
4
+ * The rules are data, checked in, so the backend's safety net reads the very same file (and a
5
+ * Python test compiles every pattern in it). Each pattern is written for both the JavaScript and
6
+ * Python regex engines: no lookbehind, no named groups, no Unicode property escapes, and every
7
+ * repeat that can start a match is bounded, so a long run of letters scans in linear time.
8
+ *
9
+ * Formats run in file order, most specific first (Anthropic before the generic `sk-` OpenAI
10
+ * shape, vendor tokens before `Bearer`), so a value is typed by the most precise rule that
11
+ * matches. Group 1 of a format, when present, is the published vendor prefix that stays.
12
+ */
13
+ export interface SecretFormat {
14
+ type: string;
15
+ fingerprint: boolean;
16
+ /** Global, for `replace`. */
17
+ pattern: RegExp;
18
+ }
19
+ export declare const SECRET_FORMATS: readonly SecretFormat[];
20
+ export declare const CREDENTIAL_KEY: RegExp;
21
+ export declare const CREDENTIAL_ASSIGNMENT: RegExp;
22
+ export declare const ENV_REFERENCE: RegExp;
23
+ export declare const CONTAINS_REFERENCE: RegExp;
24
+ export declare const CREDENTIAL_FLAG: RegExp;
25
+ /** Block key (`env`, `headers`) to the placeholder type for an ordinary value in it. */
26
+ export declare const MCP_VALUE_BLOCKS: Readonly<Record<string, string>>;
27
+ export declare const MCP_SERVER_KEYS: ReadonlySet<string>;
28
+ export declare const EMAIL: RegExp;
29
+ export declare const RESERVED_EMAIL_DOMAIN: RegExp;
30
+ export declare const HOME_DIRECTORY: RegExp;
31
+ export declare const MIN_CREDENTIAL_LENGTH: number;
32
+ //# sourceMappingURL=rules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../src/rules.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AA6BH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,OAAO,CAAC;IACrB,6BAA6B;IAC7B,OAAO,EAAE,MAAM,CAAC;CACjB;AAWD,eAAO,MAAM,cAAc,EAAE,SAAS,YAAY,EAI/C,CAAC;AACJ,eAAO,MAAM,cAAc,QAAuC,CAAC;AACnE,eAAO,MAAM,qBAAqB,QAA6C,CAAC;AAChF,eAAO,MAAM,aAAa,QAAsC,CAAC;AACjE,eAAO,MAAM,kBAAkB,QAA2C,CAAC;AAC3E,eAAO,MAAM,eAAe,QAAwC,CAAC;AACrE,wFAAwF;AACxF,eAAO,MAAM,gBAAgB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAiC,CAAC;AAChG,eAAO,MAAM,eAAe,EAAE,WAAW,CAAC,MAAM,CAA+C,CAAC;AAChG,eAAO,MAAM,KAAK,QAA6B,CAAC;AAChD,eAAO,MAAM,qBAAqB,QAA8C,CAAC;AACjF,eAAO,MAAM,cAAc,QAAsC,CAAC;AAClE,eAAO,MAAM,qBAAqB,QAA8B,CAAC"}
package/dist/rules.js ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The scrubber's rules, loaded from `rules.json` at the package root.
3
+ *
4
+ * The rules are data, checked in, so the backend's safety net reads the very same file (and a
5
+ * Python test compiles every pattern in it). Each pattern is written for both the JavaScript and
6
+ * Python regex engines: no lookbehind, no named groups, no Unicode property escapes, and every
7
+ * repeat that can start a match is bounded, so a long run of letters scans in linear time.
8
+ *
9
+ * Formats run in file order, most specific first (Anthropic before the generic `sk-` OpenAI
10
+ * shape, vendor tokens before `Bearer`), so a value is typed by the most precise rule that
11
+ * matches. Group 1 of a format, when present, is the published vendor prefix that stays.
12
+ */
13
+ import { createRequire } from "node:module";
14
+ // dist/rules.js → ../rules.json, the file at the package root (shipped in `files`). Loaded with
15
+ // Node's JSON loader rather than `fs`, which a consumer's tests may mock.
16
+ const RULES = createRequire(import.meta.url)("../rules.json");
17
+ /** `global` for patterns used with `replace`; never for `test`, whose `lastIndex` would stick. */
18
+ function compile(spec, global) {
19
+ return new RegExp(spec.pattern, `${global ? "g" : ""}${spec.ignore_case ? "i" : ""}`);
20
+ }
21
+ export const SECRET_FORMATS = RULES.formats.map((f) => ({
22
+ type: f.type,
23
+ fingerprint: f.fingerprint,
24
+ pattern: compile(f, true),
25
+ }));
26
+ export const CREDENTIAL_KEY = compile(RULES.credential_key, false);
27
+ export const CREDENTIAL_ASSIGNMENT = compile(RULES.credential_assignment, true);
28
+ export const ENV_REFERENCE = compile(RULES.env_reference, false);
29
+ export const CONTAINS_REFERENCE = compile(RULES.contains_reference, false);
30
+ export const CREDENTIAL_FLAG = compile(RULES.credential_flag, false);
31
+ /** Block key (`env`, `headers`) to the placeholder type for an ordinary value in it. */
32
+ export const MCP_VALUE_BLOCKS = RULES.mcp_value_blocks.blocks;
33
+ export const MCP_SERVER_KEYS = new Set(RULES.mcp_value_blocks.server_keys);
34
+ export const EMAIL = compile(RULES.email, true);
35
+ export const RESERVED_EMAIL_DOMAIN = compile(RULES.reserved_email_domain, false);
36
+ export const HOME_DIRECTORY = compile(RULES.home_directory, true);
37
+ export const MIN_CREDENTIAL_LENGTH = RULES.min_credential_length;
38
+ //# sourceMappingURL=rules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rules.js","sourceRoot":"","sources":["../src/rules.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAkC5C,gGAAgG;AAChG,0EAA0E;AAC1E,MAAM,KAAK,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,eAAe,CAAc,CAAC;AAE3E,kGAAkG;AAClG,SAAS,OAAO,CAAC,IAAiB,EAAE,MAAe;IACjD,OAAO,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AACxF,CAAC;AAED,MAAM,CAAC,MAAM,cAAc,GAA4B,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAC/E,IAAI,EAAE,CAAC,CAAC,IAAI;IACZ,WAAW,EAAE,CAAC,CAAC,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC,CAAC,EAAE,IAAI,CAAC;CAC1B,CAAC,CAAC,CAAC;AACJ,MAAM,CAAC,MAAM,cAAc,GAAG,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,KAAK,CAAC,CAAC;AACnE,MAAM,CAAC,MAAM,qBAAqB,GAAG,OAAO,CAAC,KAAK,CAAC,qBAAqB,EAAE,IAAI,CAAC,CAAC;AAChF,MAAM,CAAC,MAAM,aAAa,GAAG,OAAO,CAAC,KAAK,CAAC,aAAa,EAAE,KAAK,CAAC,CAAC;AACjE,MAAM,CAAC,MAAM,kBAAkB,GAAG,OAAO,CAAC,KAAK,CAAC,kBAAkB,EAAE,KAAK,CAAC,CAAC;AAC3E,MAAM,CAAC,MAAM,eAAe,GAAG,OAAO,CAAC,KAAK,CAAC,eAAe,EAAE,KAAK,CAAC,CAAC;AACrE,wFAAwF;AACxF,MAAM,CAAC,MAAM,gBAAgB,GAAqC,KAAK,CAAC,gBAAgB,CAAC,MAAM,CAAC;AAChG,MAAM,CAAC,MAAM,eAAe,GAAwB,IAAI,GAAG,CAAC,KAAK,CAAC,gBAAgB,CAAC,WAAW,CAAC,CAAC;AAChG,MAAM,CAAC,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;AAChD,MAAM,CAAC,MAAM,qBAAqB,GAAG,OAAO,CAAC,KAAK,CAAC,qBAAqB,EAAE,KAAK,CAAC,CAAC;AACjF,MAAM,CAAC,MAAM,cAAc,GAAG,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,IAAI,CAAC,CAAC;AAClE,MAAM,CAAC,MAAM,qBAAqB,GAAG,KAAK,CAAC,qBAAqB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,32 @@
1
1
  {
2
2
  "name": "@optimuslabs/pii-scrub-staging",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Redacts secrets and personal data from what an Optimus endpoint sends, before it leaves the machine.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "files": [
9
+ "dist/**/*",
10
+ "!dist/test/**",
11
+ "rules.json",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "build": "rm -rf dist && tsc -p tsconfig.json",
16
+ "pretest": "npm run build",
17
+ "test": "node --test dist/test/*.test.js",
18
+ "prepack": "npm run build"
19
+ },
20
+ "engines": {
21
+ "node": ">=18"
22
+ },
23
+ "license": "UNLICENSED",
24
+ "dependencies": {},
25
+ "devDependencies": {
26
+ "@types/node": "^20.19.39",
27
+ "typescript": "^5.4.5"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ }
32
+ }
package/rules.json ADDED
@@ -0,0 +1,154 @@
1
+ {
2
+ "description": "Rules for @optimuslabs/pii-scrub, read by the endpoint scrubber and the backend. Every pattern is valid in both the JavaScript and Python regex engines; see docs/pii-scrubbing/DESIGN.md.",
3
+ "placeholder": {
4
+ "description": "A placeholder the scrubber wrote: left unchanged and never counted as a hit.",
5
+ "pattern": "\\[OL_REDACTED:[A-Z_]+(?::[0-9a-f]{24})?\\]"
6
+ },
7
+ "formats": [
8
+ {
9
+ "type": "PRIVATE_KEY",
10
+ "pattern": "-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY-----[\\s\\S]*?-----END (?:[A-Z0-9]+ )*PRIVATE KEY-----",
11
+ "fingerprint": true
12
+ },
13
+ {
14
+ "type": "URL_CREDENTIALS",
15
+ "pattern": "\\b([a-z][a-z0-9+.-]{0,31}:\\/\\/[^\\s:@/\"'`]{1,256}:)[^\\s@/\"'`]{1,256}(?=@)",
16
+ "ignore_case": true,
17
+ "fingerprint": false
18
+ },
19
+ {
20
+ "type": "ANTHROPIC_KEY",
21
+ "pattern": "\\b(sk-ant-[a-z]+\\d{2}-)[A-Za-z0-9_-]{20,}",
22
+ "fingerprint": true
23
+ },
24
+ {
25
+ "type": "OPENAI_KEY",
26
+ "pattern": "\\b(sk-(?:proj|svcacct|admin)-)[A-Za-z0-9_-]{20,}",
27
+ "fingerprint": true
28
+ },
29
+ {
30
+ "type": "OPENAI_KEY",
31
+ "pattern": "\\b(sk-)[A-Za-z0-9]{20,}",
32
+ "fingerprint": true
33
+ },
34
+ {
35
+ "type": "GITHUB_TOKEN",
36
+ "pattern": "\\b(gh[pousr]_)[A-Za-z0-9]{36,}",
37
+ "fingerprint": true
38
+ },
39
+ {
40
+ "type": "GITHUB_TOKEN",
41
+ "pattern": "\\b(github_pat_)[A-Za-z0-9_]{22,}",
42
+ "fingerprint": true
43
+ },
44
+ {
45
+ "type": "AWS_ACCESS_KEY",
46
+ "pattern": "\\b(AKIA|ASIA)[0-9A-Z]{16}\\b",
47
+ "fingerprint": true
48
+ },
49
+ {
50
+ "type": "STRIPE_KEY",
51
+ "pattern": "\\b((?:sk|rk)_(?:live|test)_)[A-Za-z0-9]{16,}",
52
+ "fingerprint": true
53
+ },
54
+ {
55
+ "type": "SLACK_TOKEN",
56
+ "pattern": "\\b(xox[abposr]-|xapp-)[A-Za-z0-9-]{10,}",
57
+ "fingerprint": true
58
+ },
59
+ {
60
+ "type": "GOOGLE_API_KEY",
61
+ "pattern": "\\b(AIza)[0-9A-Za-z_-]{35}",
62
+ "fingerprint": true
63
+ },
64
+ {
65
+ "type": "NPM_TOKEN",
66
+ "pattern": "\\b(npm_)[A-Za-z0-9]{36}\\b",
67
+ "fingerprint": true
68
+ },
69
+ {
70
+ "type": "PYPI_TOKEN",
71
+ "pattern": "\\b(pypi-)AgEIcHlwaS5vcmc[A-Za-z0-9_-]{50,}",
72
+ "fingerprint": true
73
+ },
74
+ {
75
+ "type": "SENDGRID_KEY",
76
+ "pattern": "\\b(SG\\.)[A-Za-z0-9_-]{22}\\.[A-Za-z0-9_-]{43}",
77
+ "fingerprint": true
78
+ },
79
+ {
80
+ "type": "HUGGINGFACE_TOKEN",
81
+ "pattern": "\\b(hf_)[A-Za-z0-9]{34,}",
82
+ "fingerprint": true
83
+ },
84
+ {
85
+ "type": "PERPLEXITY_KEY",
86
+ "pattern": "\\b(pplx-)[A-Za-z0-9]{32,}",
87
+ "fingerprint": true
88
+ },
89
+ {
90
+ "type": "JWT",
91
+ "pattern": "\\beyJ[A-Za-z0-9_-]{8,}\\.eyJ[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}",
92
+ "fingerprint": true
93
+ },
94
+ {
95
+ "type": "BEARER_TOKEN",
96
+ "pattern": "\\b(Bearer\\s+)[A-Za-z0-9._~+/=-]{16,}",
97
+ "ignore_case": true,
98
+ "fingerprint": false
99
+ },
100
+ {
101
+ "type": "BASIC_AUTH",
102
+ "pattern": "\\b(Basic\\s+)[A-Za-z0-9+/=]{12,}",
103
+ "fingerprint": false
104
+ }
105
+ ],
106
+ "credential_key": {
107
+ "pattern": "(?:[_.-]key|api[_-]?key|apikey|secret|token|password|passwd|credential|private[_-]?key|access[_-]?key|authorization|token[_-]?cache(?:v\\d+)?)$",
108
+ "ignore_case": true
109
+ },
110
+ "credential_assignment": {
111
+ "pattern": "((?:api[_-]?key|apikey|secret|token|password|passwd|credential|private[_-]?key|access[_-]?key)[\"']?\\s*[:=]\\s*[\"']?)([^\\s\"'`,;&)}\\]]{8,})",
112
+ "ignore_case": true
113
+ },
114
+ "env_reference": {
115
+ "pattern": "^(?:\\$|\\{\\{|process\\.env\\.)|^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$"
116
+ },
117
+ "contains_reference": {
118
+ "description": "A reference to a secret stored elsewhere anywhere in a value (`Bearer ${TOKEN}`): the value stays readable.",
119
+ "pattern": "\\$\\{[^}]{1,128}\\}|\\$env:|process\\.env\\."
120
+ },
121
+ "credential_flag": {
122
+ "description": "A command-line flag whose next argument is a credential (`--api-key`, `--token`); group 1 is the name, tested against credential_key.",
123
+ "pattern": "^-{1,2}([A-Za-z0-9_.-]{1,64})$"
124
+ },
125
+ "mcp_value_blocks": {
126
+ "description": "Blocks inside an MCP server definition whose every value is redacted: block key to the placeholder type for a value that is neither a known format nor under a credential-named key. A server definition is an object with one of server_keys.",
127
+ "blocks": {
128
+ "env": "ENV_VALUE",
129
+ "environment": "ENV_VALUE",
130
+ "headers": "HEADER_VALUE",
131
+ "http_headers": "HEADER_VALUE"
132
+ },
133
+ "server_keys": [
134
+ "command",
135
+ "args",
136
+ "url",
137
+ "serverUrl",
138
+ "httpUrl",
139
+ "transport",
140
+ "type"
141
+ ]
142
+ },
143
+ "email": {
144
+ "pattern": "\\b[A-Za-z0-9._%+-]{1,64}@([A-Za-z0-9-]{1,63}(?:\\.[A-Za-z0-9-]{1,63})*\\.[A-Za-z]{2,24})\\b"
145
+ },
146
+ "reserved_email_domain": {
147
+ "pattern": "(?:^|\\.)(?:example\\.(?:com|org|net)|example|test|invalid|localhost)$",
148
+ "ignore_case": true
149
+ },
150
+ "home_directory": {
151
+ "pattern": "(\\/(?:Users|home)\\/)([A-Za-z0-9._-]{1,64})"
152
+ },
153
+ "min_credential_length": 8
154
+ }