@agentproto/redaction 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jeremy (agentik.net)
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.
package/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # @agentproto/redaction
2
+
3
+ Vendor-neutral **Redactor** port for scrubbing outbound payloads at an egress
4
+ boundary — loggers, telemetry sinks, session tracers — before they leave the
5
+ process.
6
+
7
+ This package is a **leaf**: it owns the port and depends on no runtime.
8
+ Consumers depend on it. It is also dependency-free and has no network access
9
+ or credential handling in v1 — every built-in redactor is a pure, local
10
+ transform. Being "off by default" is the CONSUMER's job; this package only
11
+ provides the transforms.
12
+
13
+ ## The port
14
+
15
+ ```ts
16
+ import type { Redactor, RedactionContext, JsonValue } from "@agentproto/redaction"
17
+
18
+ interface Redactor {
19
+ readonly slug: string
20
+ redact(value: JsonValue, ctx: RedactionContext): JsonValue
21
+ }
22
+ ```
23
+
24
+ `RedactionContext` carries the `field` being redacted (`"prompt"`, `"input"`,
25
+ `"output"`, `"tool-args"`, `"tool-result"`, `"metadata"`) and an optional
26
+ `sessionId`.
27
+
28
+ ## Built-in redactors
29
+
30
+ - `noneRedactor` — passthrough, returns the value unchanged.
31
+ - `denyListRedactor(opts?)` — deep, immutable walk that masks the value of any
32
+ object entry whose key matches a deny pattern (`password`, `token`,
33
+ `secret`, `authorization`, `cookie`, `credential`, etc, case-insensitive).
34
+ Recurses into nested objects and array elements. Never mutates the input.
35
+ - `truncateRedactor(opts?)` — caps long strings (default 2000 chars) and long
36
+ arrays (default 100 elements), leaving a `"…[+N chars]"` / `"…[+N items]"`
37
+ marker. Recurses into nested structures. Never mutates the input.
38
+ - `chainRedactors(redactors, slug?)` — applies each redactor in order, output
39
+ feeding the next.
40
+
41
+ ```ts
42
+ import { chainRedactors, denyListRedactor, truncateRedactor } from "@agentproto/redaction"
43
+
44
+ const redactor = chainRedactors([denyListRedactor(), truncateRedactor()])
45
+ const safe = redactor.redact(payload, { field: "tool-args" })
46
+ ```
47
+
48
+ ## Catalog and resolver
49
+
50
+ `REDACTOR_CATALOG` lists the built-in backends (`none`, `deny-list`,
51
+ `truncate`) by slug, each with a `description`, `needsCreds` flag, and a
52
+ `build(options?)` factory. `resolveRedactor(spec?)` turns a declarative
53
+ `RedactorSpec` into a `Redactor`:
54
+
55
+ ```ts
56
+ import { resolveRedactor } from "@agentproto/redaction"
57
+
58
+ resolveRedactor() // noneRedactor
59
+ resolveRedactor("deny-list")
60
+ resolveRedactor({ slug: "truncate", options: { maxStringLength: 500 } })
61
+ resolveRedactor(["deny-list", "truncate"]) // chained, in order
62
+ ```
63
+
64
+ External/PII-detection providers (e.g. Presidio) are intentionally OUT of
65
+ v1 — they would resolve over the network and carry `needsCreds: true` in a
66
+ future catalog entry.
67
+
68
+ ## License
69
+
70
+ MIT
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Vendor-neutral Redactor port.
3
+ *
4
+ * This package OWNS the redaction contract so any egress boundary (loggers,
5
+ * telemetry sinks, session tracers) can depend on a single interface without
6
+ * pulling in a kernel. It is a LEAF — it imports nothing from a runtime;
7
+ * runtimes depend on IT. No network, no credentials in v1: every built-in
8
+ * redactor here is a pure, local transform.
9
+ *
10
+ * Being "off by default" is the CONSUMER's job — this package only provides
11
+ * the transforms; a tracer or logger decides whether and how to apply one.
12
+ *
13
+ * Future work (OUT of this package for v1): external/PII-detection providers
14
+ * (e.g. Presidio) would resolve over the network and carry `needsCreds: true`
15
+ * in {@link RedactorCatalogEntry}. None are included here.
16
+ */
17
+ /** Recursive JSON value, defined locally so the package stays dependency-free. */
18
+ type JsonValue = string | number | boolean | null | {
19
+ readonly [key: string]: JsonValue;
20
+ } | readonly JsonValue[];
21
+ /** Which part of an outbound payload is being redacted. */
22
+ type RedactionField = "prompt" | "input" | "output" | "tool-args" | "tool-result" | "metadata";
23
+ /** Context passed to a redactor alongside the value it is scrubbing. */
24
+ interface RedactionContext {
25
+ readonly field: RedactionField;
26
+ readonly sessionId?: string;
27
+ }
28
+ /** A pluggable transform applied to an outbound payload before it leaves the process. */
29
+ interface Redactor {
30
+ readonly slug: string;
31
+ redact(value: JsonValue, ctx: RedactionContext): JsonValue;
32
+ }
33
+ /** A catalog entry describing a buildable redactor backend. */
34
+ interface RedactorCatalogEntry {
35
+ readonly slug: string;
36
+ readonly description: string;
37
+ readonly needsCreds: boolean;
38
+ build(options?: JsonValue): Redactor;
39
+ }
40
+ /**
41
+ * Declarative spec resolved to a {@link Redactor} by {@link resolveRedactor}.
42
+ * A string is a bare catalog slug; an object carries per-slug options; an
43
+ * array chains multiple specs in order (each spec's output feeds the next).
44
+ */
45
+ type RedactorSpec = string | {
46
+ readonly slug: string;
47
+ readonly options?: JsonValue;
48
+ } | readonly RedactorSpec[];
49
+
50
+ /** Passthrough redactor — returns the value unchanged. */
51
+ declare const noneRedactor: Redactor;
52
+ interface DenyListRedactorOptions {
53
+ readonly extraKeys?: readonly (string | RegExp)[];
54
+ readonly placeholder?: string;
55
+ }
56
+ /**
57
+ * Deep, immutable walk that masks the VALUE of every object entry whose KEY
58
+ * matches a deny pattern (case-insensitive substring/regex match), regardless
59
+ * of that value's type. Recurses into nested objects and array elements.
60
+ * Never mutates the input — always returns a new structure.
61
+ */
62
+ declare function denyListRedactor(opts?: DenyListRedactorOptions): Redactor;
63
+ interface TruncateRedactorOptions {
64
+ readonly maxStringLength?: number;
65
+ readonly maxArrayLength?: number;
66
+ }
67
+ /**
68
+ * Caps long strings and long arrays so oversized payloads don't leave the
69
+ * process. Recurses into nested objects/arrays. Never mutates the input.
70
+ */
71
+ declare function truncateRedactor(opts?: TruncateRedactorOptions): Redactor;
72
+ interface ValueScanRedactorOptions {
73
+ readonly placeholder?: string;
74
+ /** Extra whole-match secret patterns (use the `g` flag). */
75
+ readonly extraPatterns?: readonly RegExp[];
76
+ }
77
+ /**
78
+ * Deep, immutable walk that scans every STRING VALUE for well-known secret
79
+ * shapes and masks the matched substring — catching a credential leaked in a
80
+ * value whose key ISN'T denied (e.g. `{ note: "use sk-live-abc…" }`), which the
81
+ * key-based {@link denyListRedactor} cannot see. Surrounding text is preserved,
82
+ * and an `Authorization`-style `Bearer`/`Basic` scheme keeps the scheme word
83
+ * while masking only the token. Never mutates the input.
84
+ */
85
+ declare function valueScanRedactor(opts?: ValueScanRedactorOptions): Redactor;
86
+ /** Applies each redactor in order — the output of one feeds the next. */
87
+ declare function chainRedactors(redactors: readonly Redactor[], slug?: string): Redactor;
88
+
89
+ /** Static catalog of built-in redactor backends. All v1 entries are local, dependency-free transforms. */
90
+ declare const REDACTOR_CATALOG: Readonly<Record<string, RedactorCatalogEntry>>;
91
+ /**
92
+ * Resolve a {@link RedactorSpec} to a concrete {@link Redactor}.
93
+ * - `undefined` / `"none"` / `[]` resolve to {@link noneRedactor}.
94
+ * - A slug string resolves via the catalog's `build()`; throws on unknown slugs.
95
+ * - `{ slug, options }` resolves via the catalog's `build(options)`.
96
+ * - An array resolves each member and chains them in order.
97
+ */
98
+ declare function resolveRedactor(spec?: RedactorSpec): Redactor;
99
+
100
+ export { type DenyListRedactorOptions, type JsonValue, REDACTOR_CATALOG, type RedactionContext, type RedactionField, type Redactor, type RedactorCatalogEntry, type RedactorSpec, type TruncateRedactorOptions, type ValueScanRedactorOptions, chainRedactors, denyListRedactor, noneRedactor, resolveRedactor, truncateRedactor, valueScanRedactor };
package/dist/index.mjs ADDED
@@ -0,0 +1,291 @@
1
+ /**
2
+ * @agentproto/redaction v0.1.0
3
+ * Vendor-neutral Redactor port + built-in redactors + catalog/resolver.
4
+ */
5
+
6
+ // src/redactors.ts
7
+ function isJsonArray(value) {
8
+ return Array.isArray(value);
9
+ }
10
+ function isJsonObject(value) {
11
+ return typeof value === "object" && value !== null && !Array.isArray(value);
12
+ }
13
+ var noneRedactor = {
14
+ slug: "none",
15
+ redact(value) {
16
+ return value;
17
+ }
18
+ };
19
+ var DEFAULT_DENY_PATTERNS = [
20
+ "authorization",
21
+ "password",
22
+ "passwd",
23
+ "secret",
24
+ "token",
25
+ "apikey",
26
+ "api[-_]?key",
27
+ "access[-_]?key",
28
+ "client[-_]?secret",
29
+ "bearer",
30
+ "cookie",
31
+ "session[-_]?id",
32
+ "private[-_]?key",
33
+ "credential"
34
+ ];
35
+ function escapeRegExp(literal) {
36
+ return literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
37
+ }
38
+ function toKeyPattern(key) {
39
+ if (key instanceof RegExp) {
40
+ return new RegExp(key.source, key.flags.includes("i") ? key.flags : `${key.flags}i`);
41
+ }
42
+ return new RegExp(escapeRegExp(key), "i");
43
+ }
44
+ function denyListRedactor(opts = {}) {
45
+ const placeholder = opts.placeholder ?? "[redacted]";
46
+ const patterns = [
47
+ ...DEFAULT_DENY_PATTERNS.map((pattern) => new RegExp(pattern, "i")),
48
+ ...(opts.extraKeys ?? []).map(toKeyPattern)
49
+ ];
50
+ function keyIsDenied(key) {
51
+ return patterns.some((pattern) => pattern.test(key));
52
+ }
53
+ function walk(value) {
54
+ if (isJsonArray(value)) {
55
+ return value.map((element) => walk(element));
56
+ }
57
+ if (isJsonObject(value)) {
58
+ const result = {};
59
+ for (const [key, entryValue] of Object.entries(value)) {
60
+ result[key] = keyIsDenied(key) ? placeholder : walk(entryValue);
61
+ }
62
+ return result;
63
+ }
64
+ return value;
65
+ }
66
+ return {
67
+ slug: "deny-list",
68
+ redact(value) {
69
+ return walk(value);
70
+ }
71
+ };
72
+ }
73
+ function truncateRedactor(opts = {}) {
74
+ const maxStringLength = opts.maxStringLength ?? 2e3;
75
+ const maxArrayLength = opts.maxArrayLength ?? 100;
76
+ function walk(value) {
77
+ if (typeof value === "string") {
78
+ if (value.length <= maxStringLength) {
79
+ return value;
80
+ }
81
+ const extra = value.length - maxStringLength;
82
+ return `${value.slice(0, maxStringLength)}\u2026[+${extra} chars]`;
83
+ }
84
+ if (isJsonArray(value)) {
85
+ const walked = value.map((element) => walk(element));
86
+ if (walked.length <= maxArrayLength) {
87
+ return walked;
88
+ }
89
+ const extra = walked.length - maxArrayLength;
90
+ return [...walked.slice(0, maxArrayLength), `\u2026[+${extra} items]`];
91
+ }
92
+ if (isJsonObject(value)) {
93
+ const result = {};
94
+ for (const [key, entryValue] of Object.entries(value)) {
95
+ result[key] = walk(entryValue);
96
+ }
97
+ return result;
98
+ }
99
+ return value;
100
+ }
101
+ return {
102
+ slug: "truncate",
103
+ redact(value) {
104
+ return walk(value);
105
+ }
106
+ };
107
+ }
108
+ var DEFAULT_SECRET_PATTERNS = [
109
+ // PEM private key block (masked whole).
110
+ /-----BEGIN[A-Z ]*PRIVATE KEY-----[\s\S]*?-----END[A-Z ]*PRIVATE KEY-----/g,
111
+ // JWT — header.payload.signature.
112
+ /\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g,
113
+ // OpenAI / Anthropic / Stripe-style prefixed keys: sk-, sk-ant-, sk_live_, …
114
+ /\bsk[-_](?:ant[-_]|proj[-_]|live[-_]|test[-_])?[A-Za-z0-9]{16,}\b/g,
115
+ // AWS access key id.
116
+ /\bAKIA[0-9A-Z]{16}\b/g,
117
+ // GitHub token — ghp_/gho_/ghu_/ghs_/ghr_.
118
+ /\bgh[pousr]_[A-Za-z0-9]{36,}\b/g,
119
+ // Slack token.
120
+ /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g,
121
+ // Google API key.
122
+ /\bAIza[0-9A-Za-z_-]{35}\b/g
123
+ ];
124
+ function valueScanRedactor(opts = {}) {
125
+ const placeholder = opts.placeholder ?? "[redacted]";
126
+ const patterns = [...DEFAULT_SECRET_PATTERNS, ...opts.extraPatterns ?? []];
127
+ function scan(input) {
128
+ let out = input.replace(
129
+ /\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{12,}/gi,
130
+ (_match, scheme) => `${scheme} ${placeholder}`
131
+ );
132
+ for (const pattern of patterns) {
133
+ out = out.replace(pattern, placeholder);
134
+ }
135
+ return out;
136
+ }
137
+ function walk(value) {
138
+ if (typeof value === "string") {
139
+ return scan(value);
140
+ }
141
+ if (isJsonArray(value)) {
142
+ return value.map((element) => walk(element));
143
+ }
144
+ if (isJsonObject(value)) {
145
+ const result = {};
146
+ for (const [key, entryValue] of Object.entries(value)) {
147
+ result[key] = walk(entryValue);
148
+ }
149
+ return result;
150
+ }
151
+ return value;
152
+ }
153
+ return {
154
+ slug: "value-scan",
155
+ redact(value) {
156
+ return walk(value);
157
+ }
158
+ };
159
+ }
160
+ function chainRedactors(redactors, slug) {
161
+ const chainSlug = slug ?? redactors.map((redactor) => redactor.slug).join("+");
162
+ return {
163
+ slug: chainSlug,
164
+ redact(value, ctx) {
165
+ return redactors.reduce((acc, redactor) => redactor.redact(acc, ctx), value);
166
+ }
167
+ };
168
+ }
169
+
170
+ // src/catalog.ts
171
+ function isJsonObject2(value) {
172
+ return typeof value === "object" && value !== null && !Array.isArray(value);
173
+ }
174
+ function toDenyListOptions(options) {
175
+ const placeholderValue = options.placeholder;
176
+ const extraKeysValue = options.extraKeys;
177
+ return {
178
+ placeholder: typeof placeholderValue === "string" ? placeholderValue : void 0,
179
+ extraKeys: Array.isArray(extraKeysValue) ? extraKeysValue.filter((entry) => typeof entry === "string") : void 0
180
+ };
181
+ }
182
+ function toTruncateOptions(options) {
183
+ const maxStringLengthValue = options.maxStringLength;
184
+ const maxArrayLengthValue = options.maxArrayLength;
185
+ return {
186
+ maxStringLength: typeof maxStringLengthValue === "number" ? maxStringLengthValue : void 0,
187
+ maxArrayLength: typeof maxArrayLengthValue === "number" ? maxArrayLengthValue : void 0
188
+ };
189
+ }
190
+ function toValueScanOptions(options) {
191
+ const placeholderValue = options.placeholder;
192
+ return {
193
+ placeholder: typeof placeholderValue === "string" ? placeholderValue : void 0
194
+ };
195
+ }
196
+ var REDACTOR_CATALOG = {
197
+ none: {
198
+ slug: "none",
199
+ description: "Passthrough \u2014 returns the value unchanged.",
200
+ needsCreds: false,
201
+ build() {
202
+ return noneRedactor;
203
+ }
204
+ },
205
+ "deny-list": {
206
+ slug: "deny-list",
207
+ description: "Deep, immutable walk that masks values whose object key matches a deny pattern (credentials, tokens, cookies, etc).",
208
+ needsCreds: false,
209
+ build(options) {
210
+ if (options === void 0) {
211
+ return denyListRedactor();
212
+ }
213
+ if (!isJsonObject2(options)) {
214
+ throw new Error("deny-list redactor options must be a JSON object");
215
+ }
216
+ return denyListRedactor(toDenyListOptions(options));
217
+ }
218
+ },
219
+ truncate: {
220
+ slug: "truncate",
221
+ description: "Caps long strings and long arrays so oversized payloads don't leave the process.",
222
+ needsCreds: false,
223
+ build(options) {
224
+ if (options === void 0) {
225
+ return truncateRedactor();
226
+ }
227
+ if (!isJsonObject2(options)) {
228
+ throw new Error("truncate redactor options must be a JSON object");
229
+ }
230
+ return truncateRedactor(toTruncateOptions(options));
231
+ }
232
+ },
233
+ "value-scan": {
234
+ slug: "value-scan",
235
+ description: "Deep walk that scans string VALUES for well-known secret shapes (prefixed API keys, JWTs, PEM private keys, cloud tokens) and masks the match, regardless of key. Complements deny-list, which only masks by key.",
236
+ needsCreds: false,
237
+ build(options) {
238
+ if (options === void 0) {
239
+ return valueScanRedactor();
240
+ }
241
+ if (!isJsonObject2(options)) {
242
+ throw new Error("value-scan redactor options must be a JSON object");
243
+ }
244
+ return valueScanRedactor(toValueScanOptions(options));
245
+ }
246
+ },
247
+ secrets: {
248
+ slug: "secrets",
249
+ description: "Recommended default for egress: deny-list (mask by key) chained with value-scan (mask secret shapes in values). Catches a credential whether it sits under a sensitive key or is embedded in an innocuous one.",
250
+ needsCreds: false,
251
+ build() {
252
+ return chainRedactors([denyListRedactor(), valueScanRedactor()], "secrets");
253
+ }
254
+ }
255
+ };
256
+ function isRedactorSpecObject(spec) {
257
+ return typeof spec === "object" && spec !== null && !Array.isArray(spec);
258
+ }
259
+ function resolveOne(slug, options) {
260
+ const entry = REDACTOR_CATALOG[slug];
261
+ if (entry === void 0) {
262
+ const known = Object.keys(REDACTOR_CATALOG).join(", ");
263
+ throw new Error(`unknown redactor slug: ${slug} (known slugs: ${known})`);
264
+ }
265
+ return entry.build(options);
266
+ }
267
+ function resolveRedactor(spec) {
268
+ if (spec === void 0) {
269
+ return noneRedactor;
270
+ }
271
+ if (Array.isArray(spec)) {
272
+ if (spec.length === 0) {
273
+ return noneRedactor;
274
+ }
275
+ return chainRedactors(spec.map((member) => resolveRedactor(member)));
276
+ }
277
+ if (typeof spec === "string") {
278
+ if (spec === "none") {
279
+ return noneRedactor;
280
+ }
281
+ return resolveOne(spec);
282
+ }
283
+ if (isRedactorSpecObject(spec)) {
284
+ return resolveOne(spec.slug, spec.options);
285
+ }
286
+ throw new Error("invalid redactor spec");
287
+ }
288
+
289
+ export { REDACTOR_CATALOG, chainRedactors, denyListRedactor, noneRedactor, resolveRedactor, truncateRedactor, valueScanRedactor };
290
+ //# sourceMappingURL=index.mjs.map
291
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/redactors.ts","../src/catalog.ts"],"names":["isJsonObject"],"mappings":"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","file":"index.mjs","sourcesContent":["import type { JsonValue, Redactor } from \"./types.js\"\n\nfunction isJsonArray(value: JsonValue): value is readonly JsonValue[] {\n return Array.isArray(value)\n}\n\nfunction isJsonObject(value: JsonValue): value is { readonly [key: string]: JsonValue } {\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n}\n\n/** Passthrough redactor — returns the value unchanged. */\nexport const noneRedactor: Redactor = {\n slug: \"none\",\n redact(value) {\n return value\n },\n}\n\n/** Default case-insensitive deny patterns, matched if a key CONTAINS them. */\nconst DEFAULT_DENY_PATTERNS: readonly string[] = [\n \"authorization\",\n \"password\",\n \"passwd\",\n \"secret\",\n \"token\",\n \"apikey\",\n \"api[-_]?key\",\n \"access[-_]?key\",\n \"client[-_]?secret\",\n \"bearer\",\n \"cookie\",\n \"session[-_]?id\",\n \"private[-_]?key\",\n \"credential\",\n]\n\nfunction escapeRegExp(literal: string): string {\n return literal.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n}\n\nfunction toKeyPattern(key: string | RegExp): RegExp {\n if (key instanceof RegExp) {\n return new RegExp(key.source, key.flags.includes(\"i\") ? key.flags : `${key.flags}i`)\n }\n return new RegExp(escapeRegExp(key), \"i\")\n}\n\nexport interface DenyListRedactorOptions {\n readonly extraKeys?: readonly (string | RegExp)[]\n readonly placeholder?: string\n}\n\n/**\n * Deep, immutable walk that masks the VALUE of every object entry whose KEY\n * matches a deny pattern (case-insensitive substring/regex match), regardless\n * of that value's type. Recurses into nested objects and array elements.\n * Never mutates the input — always returns a new structure.\n */\nexport function denyListRedactor(opts: DenyListRedactorOptions = {}): Redactor {\n const placeholder = opts.placeholder ?? \"[redacted]\"\n const patterns: readonly RegExp[] = [\n ...DEFAULT_DENY_PATTERNS.map((pattern) => new RegExp(pattern, \"i\")),\n ...(opts.extraKeys ?? []).map(toKeyPattern),\n ]\n\n function keyIsDenied(key: string): boolean {\n return patterns.some((pattern) => pattern.test(key))\n }\n\n function walk(value: JsonValue): JsonValue {\n if (isJsonArray(value)) {\n return value.map((element) => walk(element))\n }\n if (isJsonObject(value)) {\n const result: { [key: string]: JsonValue } = {}\n for (const [key, entryValue] of Object.entries(value)) {\n result[key] = keyIsDenied(key) ? placeholder : walk(entryValue)\n }\n return result\n }\n return value\n }\n\n return {\n slug: \"deny-list\",\n redact(value) {\n return walk(value)\n },\n }\n}\n\nexport interface TruncateRedactorOptions {\n readonly maxStringLength?: number\n readonly maxArrayLength?: number\n}\n\n/**\n * Caps long strings and long arrays so oversized payloads don't leave the\n * process. Recurses into nested objects/arrays. Never mutates the input.\n */\nexport function truncateRedactor(opts: TruncateRedactorOptions = {}): Redactor {\n const maxStringLength = opts.maxStringLength ?? 2000\n const maxArrayLength = opts.maxArrayLength ?? 100\n\n function walk(value: JsonValue): JsonValue {\n if (typeof value === \"string\") {\n if (value.length <= maxStringLength) {\n return value\n }\n const extra = value.length - maxStringLength\n return `${value.slice(0, maxStringLength)}…[+${extra} chars]`\n }\n if (isJsonArray(value)) {\n const walked = value.map((element) => walk(element))\n if (walked.length <= maxArrayLength) {\n return walked\n }\n const extra = walked.length - maxArrayLength\n return [...walked.slice(0, maxArrayLength), `…[+${extra} items]`]\n }\n if (isJsonObject(value)) {\n const result: { [key: string]: JsonValue } = {}\n for (const [key, entryValue] of Object.entries(value)) {\n result[key] = walk(entryValue)\n }\n return result\n }\n return value\n }\n\n return {\n slug: \"truncate\",\n redact(value) {\n return walk(value)\n },\n }\n}\n\n/**\n * Well-known secret shapes, matched inside STRING VALUES regardless of the key\n * they sit under. All patterns are prefix-anchored and linear (no nested\n * quantifiers over overlapping classes) so they can't backtrack pathologically.\n */\nconst DEFAULT_SECRET_PATTERNS: readonly RegExp[] = [\n // PEM private key block (masked whole).\n /-----BEGIN[A-Z ]*PRIVATE KEY-----[\\s\\S]*?-----END[A-Z ]*PRIVATE KEY-----/g,\n // JWT — header.payload.signature.\n /\\beyJ[A-Za-z0-9_-]{8,}\\.eyJ[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}\\b/g,\n // OpenAI / Anthropic / Stripe-style prefixed keys: sk-, sk-ant-, sk_live_, …\n /\\bsk[-_](?:ant[-_]|proj[-_]|live[-_]|test[-_])?[A-Za-z0-9]{16,}\\b/g,\n // AWS access key id.\n /\\bAKIA[0-9A-Z]{16}\\b/g,\n // GitHub token — ghp_/gho_/ghu_/ghs_/ghr_.\n /\\bgh[pousr]_[A-Za-z0-9]{36,}\\b/g,\n // Slack token.\n /\\bxox[baprs]-[A-Za-z0-9-]{10,}\\b/g,\n // Google API key.\n /\\bAIza[0-9A-Za-z_-]{35}\\b/g,\n]\n\nexport interface ValueScanRedactorOptions {\n readonly placeholder?: string\n /** Extra whole-match secret patterns (use the `g` flag). */\n readonly extraPatterns?: readonly RegExp[]\n}\n\n/**\n * Deep, immutable walk that scans every STRING VALUE for well-known secret\n * shapes and masks the matched substring — catching a credential leaked in a\n * value whose key ISN'T denied (e.g. `{ note: \"use sk-live-abc…\" }`), which the\n * key-based {@link denyListRedactor} cannot see. Surrounding text is preserved,\n * and an `Authorization`-style `Bearer`/`Basic` scheme keeps the scheme word\n * while masking only the token. Never mutates the input.\n */\nexport function valueScanRedactor(opts: ValueScanRedactorOptions = {}): Redactor {\n const placeholder = opts.placeholder ?? \"[redacted]\"\n const patterns: readonly RegExp[] = [...DEFAULT_SECRET_PATTERNS, ...(opts.extraPatterns ?? [])]\n\n function scan(input: string): string {\n // Scheme-preserving first: keep \"Bearer\"/\"Basic\", mask the token after it.\n let out = input.replace(\n /\\b(Bearer|Basic)\\s+[A-Za-z0-9._~+/=-]{12,}/gi,\n (_match: string, scheme: string) => `${scheme} ${placeholder}`,\n )\n for (const pattern of patterns) {\n out = out.replace(pattern, placeholder)\n }\n return out\n }\n\n function walk(value: JsonValue): JsonValue {\n if (typeof value === \"string\") {\n return scan(value)\n }\n if (isJsonArray(value)) {\n return value.map((element) => walk(element))\n }\n if (isJsonObject(value)) {\n const result: { [key: string]: JsonValue } = {}\n for (const [key, entryValue] of Object.entries(value)) {\n result[key] = walk(entryValue)\n }\n return result\n }\n return value\n }\n\n return {\n slug: \"value-scan\",\n redact(value) {\n return walk(value)\n },\n }\n}\n\n/** Applies each redactor in order — the output of one feeds the next. */\nexport function chainRedactors(redactors: readonly Redactor[], slug?: string): Redactor {\n const chainSlug = slug ?? redactors.map((redactor) => redactor.slug).join(\"+\")\n return {\n slug: chainSlug,\n redact(value, ctx) {\n return redactors.reduce((acc, redactor) => redactor.redact(acc, ctx), value)\n },\n }\n}\n","import {\n chainRedactors,\n denyListRedactor,\n noneRedactor,\n truncateRedactor,\n valueScanRedactor,\n} from \"./redactors.js\"\nimport type {\n DenyListRedactorOptions,\n TruncateRedactorOptions,\n ValueScanRedactorOptions,\n} from \"./redactors.js\"\nimport type { JsonValue, Redactor, RedactorCatalogEntry, RedactorSpec } from \"./types.js\"\n\nfunction isJsonObject(value: JsonValue): value is { readonly [key: string]: JsonValue } {\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n}\n\n/**\n * Builds deny-list options from JSON. Only string `extraKeys` can round-trip\n * through JSON (a `RegExp` cannot); pass `RegExp` entries by calling\n * {@link denyListRedactor} directly instead of through the catalog.\n */\nfunction toDenyListOptions(options: { readonly [key: string]: JsonValue }): DenyListRedactorOptions {\n const placeholderValue = options.placeholder\n const extraKeysValue = options.extraKeys\n return {\n placeholder: typeof placeholderValue === \"string\" ? placeholderValue : undefined,\n extraKeys: Array.isArray(extraKeysValue)\n ? extraKeysValue.filter((entry): entry is string => typeof entry === \"string\")\n : undefined,\n }\n}\n\nfunction toTruncateOptions(options: { readonly [key: string]: JsonValue }): TruncateRedactorOptions {\n const maxStringLengthValue = options.maxStringLength\n const maxArrayLengthValue = options.maxArrayLength\n return {\n maxStringLength: typeof maxStringLengthValue === \"number\" ? maxStringLengthValue : undefined,\n maxArrayLength: typeof maxArrayLengthValue === \"number\" ? maxArrayLengthValue : undefined,\n }\n}\n\n/**\n * Builds value-scan options from JSON. Only `placeholder` round-trips through\n * JSON; `extraPatterns` are `RegExp` and must be passed by calling\n * {@link valueScanRedactor} directly.\n */\nfunction toValueScanOptions(options: {\n readonly [key: string]: JsonValue\n}): ValueScanRedactorOptions {\n const placeholderValue = options.placeholder\n return {\n placeholder: typeof placeholderValue === \"string\" ? placeholderValue : undefined,\n }\n}\n\n/** Static catalog of built-in redactor backends. All v1 entries are local, dependency-free transforms. */\nexport const REDACTOR_CATALOG: Readonly<Record<string, RedactorCatalogEntry>> = {\n none: {\n slug: \"none\",\n description: \"Passthrough — returns the value unchanged.\",\n needsCreds: false,\n build() {\n return noneRedactor\n },\n },\n \"deny-list\": {\n slug: \"deny-list\",\n description:\n \"Deep, immutable walk that masks values whose object key matches a deny pattern \" +\n \"(credentials, tokens, cookies, etc).\",\n needsCreds: false,\n build(options) {\n if (options === undefined) {\n return denyListRedactor()\n }\n if (!isJsonObject(options)) {\n throw new Error(\"deny-list redactor options must be a JSON object\")\n }\n return denyListRedactor(toDenyListOptions(options))\n },\n },\n truncate: {\n slug: \"truncate\",\n description: \"Caps long strings and long arrays so oversized payloads don't leave the process.\",\n needsCreds: false,\n build(options) {\n if (options === undefined) {\n return truncateRedactor()\n }\n if (!isJsonObject(options)) {\n throw new Error(\"truncate redactor options must be a JSON object\")\n }\n return truncateRedactor(toTruncateOptions(options))\n },\n },\n \"value-scan\": {\n slug: \"value-scan\",\n description:\n \"Deep walk that scans string VALUES for well-known secret shapes (prefixed API \" +\n \"keys, JWTs, PEM private keys, cloud tokens) and masks the match, regardless of \" +\n \"key. Complements deny-list, which only masks by key.\",\n needsCreds: false,\n build(options) {\n if (options === undefined) {\n return valueScanRedactor()\n }\n if (!isJsonObject(options)) {\n throw new Error(\"value-scan redactor options must be a JSON object\")\n }\n return valueScanRedactor(toValueScanOptions(options))\n },\n },\n secrets: {\n slug: \"secrets\",\n description:\n \"Recommended default for egress: deny-list (mask by key) chained with value-scan \" +\n \"(mask secret shapes in values). Catches a credential whether it sits under a \" +\n \"sensitive key or is embedded in an innocuous one.\",\n needsCreds: false,\n build() {\n return chainRedactors([denyListRedactor(), valueScanRedactor()], \"secrets\")\n },\n },\n}\n\nfunction isRedactorSpecObject(\n spec: RedactorSpec,\n): spec is { readonly slug: string; readonly options?: JsonValue } {\n return typeof spec === \"object\" && spec !== null && !Array.isArray(spec)\n}\n\nfunction resolveOne(slug: string, options?: JsonValue): Redactor {\n const entry = REDACTOR_CATALOG[slug]\n if (entry === undefined) {\n const known = Object.keys(REDACTOR_CATALOG).join(\", \")\n throw new Error(`unknown redactor slug: ${slug} (known slugs: ${known})`)\n }\n return entry.build(options)\n}\n\n/**\n * Resolve a {@link RedactorSpec} to a concrete {@link Redactor}.\n * - `undefined` / `\"none\"` / `[]` resolve to {@link noneRedactor}.\n * - A slug string resolves via the catalog's `build()`; throws on unknown slugs.\n * - `{ slug, options }` resolves via the catalog's `build(options)`.\n * - An array resolves each member and chains them in order.\n */\nexport function resolveRedactor(spec?: RedactorSpec): Redactor {\n if (spec === undefined) {\n return noneRedactor\n }\n if (Array.isArray(spec)) {\n if (spec.length === 0) {\n return noneRedactor\n }\n return chainRedactors(spec.map((member) => resolveRedactor(member)))\n }\n if (typeof spec === \"string\") {\n if (spec === \"none\") {\n return noneRedactor\n }\n return resolveOne(spec)\n }\n if (isRedactorSpecObject(spec)) {\n return resolveOne(spec.slug, spec.options)\n }\n throw new Error(\"invalid redactor spec\")\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@agentproto/redaction",
3
+ "version": "0.2.0",
4
+ "description": "agentproto/redaction — vendor-neutral Redactor port + built-in redactors (none, deny-list, truncate, chain) and a catalog/resolver so any egress boundary can scrub outbound payloads before they leave the process.",
5
+ "keywords": [
6
+ "agentproto",
7
+ "redaction",
8
+ "pii",
9
+ "privacy",
10
+ "egress",
11
+ "open-standard",
12
+ "agentic"
13
+ ],
14
+ "homepage": "https://agentik.net/docs",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/agentproto/ts",
18
+ "directory": "packages/redaction"
19
+ },
20
+ "bugs": {
21
+ "url": "https://github.com/agentproto/ts/issues"
22
+ },
23
+ "license": "MIT",
24
+ "type": "module",
25
+ "main": "dist/index.mjs",
26
+ "module": "dist/index.mjs",
27
+ "types": "dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.mjs",
32
+ "default": "./dist/index.mjs"
33
+ },
34
+ "./package.json": "./package.json"
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "README.md",
39
+ "LICENSE"
40
+ ],
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^25.6.2",
46
+ "tsup": "^8.5.1",
47
+ "typescript": "^5.9.3",
48
+ "vitest": "^3.2.4",
49
+ "@agentproto/tooling": "0.1.0-alpha.0"
50
+ },
51
+ "scripts": {
52
+ "dev": "tsup --watch",
53
+ "build": "tsup",
54
+ "clean": "rm -rf dist",
55
+ "check-types": "tsc --noEmit",
56
+ "test": "vitest run --passWithNoTests",
57
+ "test:watch": "vitest"
58
+ }
59
+ }