@artemiskit/core 0.1.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/CHANGELOG.md +48 -0
- package/dist/adapters/factory.d.ts +23 -0
- package/dist/adapters/factory.d.ts.map +1 -0
- package/dist/adapters/index.d.ts +7 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/registry.d.ts +56 -0
- package/dist/adapters/registry.d.ts.map +1 -0
- package/dist/adapters/types.d.ts +151 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/artifacts/index.d.ts +6 -0
- package/dist/artifacts/index.d.ts.map +1 -0
- package/dist/artifacts/manifest.d.ts +19 -0
- package/dist/artifacts/manifest.d.ts.map +1 -0
- package/dist/artifacts/types.d.ts +368 -0
- package/dist/artifacts/types.d.ts.map +1 -0
- package/dist/evaluators/contains.d.ts +10 -0
- package/dist/evaluators/contains.d.ts.map +1 -0
- package/dist/evaluators/exact.d.ts +10 -0
- package/dist/evaluators/exact.d.ts.map +1 -0
- package/dist/evaluators/fuzzy.d.ts +10 -0
- package/dist/evaluators/fuzzy.d.ts.map +1 -0
- package/dist/evaluators/index.d.ts +24 -0
- package/dist/evaluators/index.d.ts.map +1 -0
- package/dist/evaluators/json-schema.d.ts +11 -0
- package/dist/evaluators/json-schema.d.ts.map +1 -0
- package/dist/evaluators/llm-grader.d.ts +11 -0
- package/dist/evaluators/llm-grader.d.ts.map +1 -0
- package/dist/evaluators/regex.d.ts +10 -0
- package/dist/evaluators/regex.d.ts.map +1 -0
- package/dist/evaluators/types.d.ts +29 -0
- package/dist/evaluators/types.d.ts.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26021 -0
- package/dist/provenance/environment.d.ts +12 -0
- package/dist/provenance/environment.d.ts.map +1 -0
- package/dist/provenance/git.d.ts +9 -0
- package/dist/provenance/git.d.ts.map +1 -0
- package/dist/provenance/index.d.ts +6 -0
- package/dist/provenance/index.d.ts.map +1 -0
- package/dist/redaction/index.d.ts +3 -0
- package/dist/redaction/index.d.ts.map +1 -0
- package/dist/redaction/redactor.d.ts +79 -0
- package/dist/redaction/redactor.d.ts.map +1 -0
- package/dist/redaction/types.d.ts +120 -0
- package/dist/redaction/types.d.ts.map +1 -0
- package/dist/runner/executor.d.ts +11 -0
- package/dist/runner/executor.d.ts.map +1 -0
- package/dist/runner/index.d.ts +7 -0
- package/dist/runner/index.d.ts.map +1 -0
- package/dist/runner/runner.d.ts +13 -0
- package/dist/runner/runner.d.ts.map +1 -0
- package/dist/runner/types.d.ts +57 -0
- package/dist/runner/types.d.ts.map +1 -0
- package/dist/scenario/index.d.ts +7 -0
- package/dist/scenario/index.d.ts.map +1 -0
- package/dist/scenario/parser.d.ts +17 -0
- package/dist/scenario/parser.d.ts.map +1 -0
- package/dist/scenario/schema.d.ts +945 -0
- package/dist/scenario/schema.d.ts.map +1 -0
- package/dist/scenario/variables.d.ts +19 -0
- package/dist/scenario/variables.d.ts.map +1 -0
- package/dist/storage/factory.d.ts +13 -0
- package/dist/storage/factory.d.ts.map +1 -0
- package/dist/storage/index.d.ts +8 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/local.d.ts +20 -0
- package/dist/storage/local.d.ts.map +1 -0
- package/dist/storage/supabase.d.ts +21 -0
- package/dist/storage/supabase.d.ts.map +1 -0
- package/dist/storage/types.d.ts +86 -0
- package/dist/storage/types.d.ts.map +1 -0
- package/dist/utils/errors.d.ts +25 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/logger.d.ts +21 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/package.json +56 -0
- package/src/adapters/factory.ts +75 -0
- package/src/adapters/index.ts +7 -0
- package/src/adapters/registry.ts +143 -0
- package/src/adapters/types.ts +184 -0
- package/src/artifacts/index.ts +6 -0
- package/src/artifacts/manifest.test.ts +206 -0
- package/src/artifacts/manifest.ts +136 -0
- package/src/artifacts/types.ts +426 -0
- package/src/evaluators/contains.test.ts +58 -0
- package/src/evaluators/contains.ts +41 -0
- package/src/evaluators/exact.test.ts +48 -0
- package/src/evaluators/exact.ts +33 -0
- package/src/evaluators/fuzzy.test.ts +50 -0
- package/src/evaluators/fuzzy.ts +39 -0
- package/src/evaluators/index.ts +53 -0
- package/src/evaluators/json-schema.ts +98 -0
- package/src/evaluators/llm-grader.ts +100 -0
- package/src/evaluators/regex.test.ts +73 -0
- package/src/evaluators/regex.ts +43 -0
- package/src/evaluators/types.ts +37 -0
- package/src/index.ts +31 -0
- package/src/provenance/environment.ts +18 -0
- package/src/provenance/git.ts +48 -0
- package/src/provenance/index.ts +6 -0
- package/src/redaction/index.ts +23 -0
- package/src/redaction/redactor.test.ts +258 -0
- package/src/redaction/redactor.ts +246 -0
- package/src/redaction/types.ts +135 -0
- package/src/runner/executor.ts +251 -0
- package/src/runner/index.ts +7 -0
- package/src/runner/runner.ts +153 -0
- package/src/runner/types.ts +60 -0
- package/src/scenario/index.ts +7 -0
- package/src/scenario/parser.test.ts +99 -0
- package/src/scenario/parser.ts +108 -0
- package/src/scenario/schema.ts +176 -0
- package/src/scenario/variables.test.ts +150 -0
- package/src/scenario/variables.ts +60 -0
- package/src/storage/factory.ts +52 -0
- package/src/storage/index.ts +8 -0
- package/src/storage/local.test.ts +165 -0
- package/src/storage/local.ts +194 -0
- package/src/storage/supabase.ts +151 -0
- package/src/storage/types.ts +98 -0
- package/src/utils/errors.ts +76 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/logger.ts +59 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
Redactor,
|
|
4
|
+
createDefaultRedactor,
|
|
5
|
+
createNoOpRedactor,
|
|
6
|
+
hashText,
|
|
7
|
+
redactText,
|
|
8
|
+
redactWithHash,
|
|
9
|
+
resolvePatterns,
|
|
10
|
+
} from './redactor';
|
|
11
|
+
import { BUILTIN_PATTERNS, DEFAULT_REDACTION_PATTERNS } from './types';
|
|
12
|
+
|
|
13
|
+
describe('redactText', () => {
|
|
14
|
+
it('should redact email addresses', () => {
|
|
15
|
+
const text = 'Contact me at john.doe@example.com for more info';
|
|
16
|
+
const patterns = [/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g];
|
|
17
|
+
const result = redactText(text, patterns);
|
|
18
|
+
|
|
19
|
+
expect(result.wasRedacted).toBe(true);
|
|
20
|
+
expect(result.redactionCount).toBe(1);
|
|
21
|
+
expect(result.text).toBe('Contact me at [REDACTED] for more info');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should redact multiple occurrences', () => {
|
|
25
|
+
const text = 'Email john@test.com or jane@test.com';
|
|
26
|
+
const patterns = [/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g];
|
|
27
|
+
const result = redactText(text, patterns);
|
|
28
|
+
|
|
29
|
+
expect(result.wasRedacted).toBe(true);
|
|
30
|
+
expect(result.redactionCount).toBe(2);
|
|
31
|
+
expect(result.text).toBe('Email [REDACTED] or [REDACTED]');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should use custom replacement', () => {
|
|
35
|
+
const text = 'Call me at 555-123-4567';
|
|
36
|
+
const patterns = [/\b\d{3}[-\s]?\d{3}[-\s]?\d{4}\b/g];
|
|
37
|
+
const result = redactText(text, patterns, '***');
|
|
38
|
+
|
|
39
|
+
expect(result.text).toBe('Call me at ***');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should return original text when no matches', () => {
|
|
43
|
+
const text = 'Hello world';
|
|
44
|
+
const patterns = [/\b\d{3}-\d{3}-\d{4}\b/g];
|
|
45
|
+
const result = redactText(text, patterns);
|
|
46
|
+
|
|
47
|
+
expect(result.wasRedacted).toBe(false);
|
|
48
|
+
expect(result.redactionCount).toBe(0);
|
|
49
|
+
expect(result.text).toBe('Hello world');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should handle empty text', () => {
|
|
53
|
+
const result = redactText('', [/test/g]);
|
|
54
|
+
expect(result.wasRedacted).toBe(false);
|
|
55
|
+
expect(result.text).toBe('');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should handle empty patterns', () => {
|
|
59
|
+
const result = redactText('some text', []);
|
|
60
|
+
expect(result.wasRedacted).toBe(false);
|
|
61
|
+
expect(result.text).toBe('some text');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('resolvePatterns', () => {
|
|
66
|
+
it('should resolve built-in pattern names', () => {
|
|
67
|
+
const patterns = resolvePatterns([BUILTIN_PATTERNS.EMAIL, BUILTIN_PATTERNS.PHONE]);
|
|
68
|
+
expect(patterns).toHaveLength(2);
|
|
69
|
+
expect(patterns[0].name).toBe('email');
|
|
70
|
+
expect(patterns[1].name).toBe('phone');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should handle custom regex strings', () => {
|
|
74
|
+
const patterns = resolvePatterns(['\\btest\\b', 'custom-pattern']);
|
|
75
|
+
expect(patterns).toHaveLength(2);
|
|
76
|
+
expect(patterns[0].name).toContain('custom:');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should filter out invalid regex', () => {
|
|
80
|
+
const patterns = resolvePatterns(['[invalid', 'valid']);
|
|
81
|
+
expect(patterns).toHaveLength(1);
|
|
82
|
+
expect(patterns[0].name).toContain('custom:');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('hashText', () => {
|
|
87
|
+
it('should generate consistent SHA-256 hash', () => {
|
|
88
|
+
const text = 'sensitive data';
|
|
89
|
+
const hash1 = hashText(text);
|
|
90
|
+
const hash2 = hashText(text);
|
|
91
|
+
|
|
92
|
+
expect(hash1).toBe(hash2);
|
|
93
|
+
expect(hash1).toHaveLength(64); // SHA-256 hex is 64 chars
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should generate different hashes for different text', () => {
|
|
97
|
+
const hash1 = hashText('text1');
|
|
98
|
+
const hash2 = hashText('text2');
|
|
99
|
+
|
|
100
|
+
expect(hash1).not.toBe(hash2);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('redactWithHash', () => {
|
|
105
|
+
it('should return both redacted text and original hash', () => {
|
|
106
|
+
const original = 'Contact: test@email.com';
|
|
107
|
+
const patterns = [/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g];
|
|
108
|
+
const result = redactWithHash(original, patterns);
|
|
109
|
+
|
|
110
|
+
expect(result.wasRedacted).toBe(true);
|
|
111
|
+
expect(result.text).toBe('Contact: [REDACTED]');
|
|
112
|
+
expect(result.originalHash).toHaveLength(64);
|
|
113
|
+
expect(result.originalHash).toBe(hashText(original));
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('Redactor class', () => {
|
|
118
|
+
it('should create with default patterns when enabled', () => {
|
|
119
|
+
const redactor = createDefaultRedactor();
|
|
120
|
+
expect(redactor.enabled).toBe(true);
|
|
121
|
+
expect(redactor.patternNames.length).toBeGreaterThan(0);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should create no-op redactor when disabled', () => {
|
|
125
|
+
const redactor = createNoOpRedactor();
|
|
126
|
+
expect(redactor.enabled).toBe(false);
|
|
127
|
+
|
|
128
|
+
const result = redactor.redact('test@email.com');
|
|
129
|
+
expect(result.wasRedacted).toBe(false);
|
|
130
|
+
expect(result.text).toBe('test@email.com');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should redact prompts when configured', () => {
|
|
134
|
+
const redactor = new Redactor({
|
|
135
|
+
enabled: true,
|
|
136
|
+
redactPrompts: true,
|
|
137
|
+
patterns: [BUILTIN_PATTERNS.EMAIL],
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const result = redactor.redactPrompt('Send to user@test.com');
|
|
141
|
+
expect(result.wasRedacted).toBe(true);
|
|
142
|
+
expect(result.text).toBe('Send to [REDACTED]');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should not redact prompts when disabled', () => {
|
|
146
|
+
const redactor = new Redactor({
|
|
147
|
+
enabled: true,
|
|
148
|
+
redactPrompts: false,
|
|
149
|
+
patterns: [BUILTIN_PATTERNS.EMAIL],
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const result = redactor.redactPrompt('Send to user@test.com');
|
|
153
|
+
expect(result.wasRedacted).toBe(false);
|
|
154
|
+
expect(result.text).toBe('Send to user@test.com');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should redact responses when configured', () => {
|
|
158
|
+
const redactor = new Redactor({
|
|
159
|
+
enabled: true,
|
|
160
|
+
redactResponses: true,
|
|
161
|
+
patterns: [BUILTIN_PATTERNS.PHONE],
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const result = redactor.redactResponse('Call me at 555-123-4567');
|
|
165
|
+
expect(result.wasRedacted).toBe(true);
|
|
166
|
+
expect(result.text).toBe('Call me at [REDACTED]');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should use custom replacement string', () => {
|
|
170
|
+
const redactor = new Redactor({
|
|
171
|
+
enabled: true,
|
|
172
|
+
replacement: '<<HIDDEN>>',
|
|
173
|
+
patterns: [BUILTIN_PATTERNS.EMAIL],
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const result = redactor.redact('test@example.com');
|
|
177
|
+
expect(result.text).toBe('<<HIDDEN>>');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should redact metadata when enabled', () => {
|
|
181
|
+
const redactor = new Redactor({
|
|
182
|
+
enabled: true,
|
|
183
|
+
redactMetadata: true,
|
|
184
|
+
patterns: [BUILTIN_PATTERNS.EMAIL],
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const metadata = {
|
|
188
|
+
user: 'john@test.com',
|
|
189
|
+
action: 'login',
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const result = redactor.redactMetadata(metadata);
|
|
193
|
+
expect(result.user).toBe('[REDACTED]');
|
|
194
|
+
expect(result.action).toBe('login');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should not redact metadata when disabled', () => {
|
|
198
|
+
const redactor = new Redactor({
|
|
199
|
+
enabled: true,
|
|
200
|
+
redactMetadata: false,
|
|
201
|
+
patterns: [BUILTIN_PATTERNS.EMAIL],
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const metadata = { user: 'john@test.com' };
|
|
205
|
+
const result = redactor.redactMetadata(metadata);
|
|
206
|
+
expect(result.user).toBe('john@test.com');
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('Built-in patterns', () => {
|
|
211
|
+
const createTestRedactor = (pattern: string) =>
|
|
212
|
+
new Redactor({ enabled: true, patterns: [pattern] });
|
|
213
|
+
|
|
214
|
+
it('should redact credit card numbers', () => {
|
|
215
|
+
const redactor = createTestRedactor(BUILTIN_PATTERNS.CREDIT_CARD);
|
|
216
|
+
const result = redactor.redact('Card: 4111-1111-1111-1111');
|
|
217
|
+
expect(result.wasRedacted).toBe(true);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should redact SSN', () => {
|
|
221
|
+
const redactor = createTestRedactor(BUILTIN_PATTERNS.SSN);
|
|
222
|
+
const result = redactor.redact('SSN: 123-45-6789');
|
|
223
|
+
expect(result.wasRedacted).toBe(true);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('should redact API keys', () => {
|
|
227
|
+
const redactor = createTestRedactor(BUILTIN_PATTERNS.API_KEY);
|
|
228
|
+
// Using clearly fake test key pattern (not a real key format)
|
|
229
|
+
const result = redactor.redact('Key: sk_test_FAKE_KEY_FOR_TESTING_1234567890');
|
|
230
|
+
expect(result.wasRedacted).toBe(true);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should redact AWS keys', () => {
|
|
234
|
+
const redactor = createTestRedactor(BUILTIN_PATTERNS.AWS_KEY);
|
|
235
|
+
const result = redactor.redact('AWS: AKIAIOSFODNN7EXAMPLE');
|
|
236
|
+
expect(result.wasRedacted).toBe(true);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should redact secrets in assignments', () => {
|
|
240
|
+
const redactor = createTestRedactor(BUILTIN_PATTERNS.SECRETS);
|
|
241
|
+
const result = redactor.redact('password=mysecretpass123');
|
|
242
|
+
expect(result.wasRedacted).toBe(true);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should redact IPv4 addresses', () => {
|
|
246
|
+
const redactor = createTestRedactor(BUILTIN_PATTERNS.IPV4);
|
|
247
|
+
const result = redactor.redact('Server: 192.168.1.100');
|
|
248
|
+
expect(result.wasRedacted).toBe(true);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('should redact JWT tokens', () => {
|
|
252
|
+
const redactor = createTestRedactor(BUILTIN_PATTERNS.JWT);
|
|
253
|
+
const jwt =
|
|
254
|
+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U';
|
|
255
|
+
const result = redactor.redact(`Token: ${jwt}`);
|
|
256
|
+
expect(result.wasRedacted).toBe(true);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
BUILTIN_REGEX_PATTERNS,
|
|
4
|
+
type BuiltinPatternName,
|
|
5
|
+
DEFAULT_REDACTION_PATTERNS,
|
|
6
|
+
type RedactionConfig,
|
|
7
|
+
type RedactionOptions,
|
|
8
|
+
type RedactionResult,
|
|
9
|
+
} from './types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Resolve pattern strings to RegExp objects
|
|
13
|
+
* Supports built-in pattern names and custom regex strings
|
|
14
|
+
*/
|
|
15
|
+
export function resolvePatterns(patterns: string[]): { regex: RegExp; name: string }[] {
|
|
16
|
+
return patterns
|
|
17
|
+
.map((pattern) => {
|
|
18
|
+
// Check if it's a built-in pattern name
|
|
19
|
+
if (pattern in BUILTIN_REGEX_PATTERNS) {
|
|
20
|
+
const regex = BUILTIN_REGEX_PATTERNS[pattern as BuiltinPatternName];
|
|
21
|
+
return { regex: new RegExp(regex.source, regex.flags), name: pattern };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Treat as custom regex
|
|
25
|
+
try {
|
|
26
|
+
return { regex: new RegExp(pattern, 'g'), name: `custom:${pattern.slice(0, 20)}` };
|
|
27
|
+
} catch {
|
|
28
|
+
// Invalid regex, skip it
|
|
29
|
+
console.warn(`Invalid redaction pattern: ${pattern}`);
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
.filter((p): p is { regex: RegExp; name: string } => p !== null);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create RedactionOptions from RedactionConfig
|
|
38
|
+
*/
|
|
39
|
+
export function createRedactionOptions(config: RedactionConfig): RedactionOptions {
|
|
40
|
+
const patternNames = config.patterns?.length ? config.patterns : DEFAULT_REDACTION_PATTERNS;
|
|
41
|
+
const resolvedPatterns = resolvePatterns(patternNames);
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
enabled: config.enabled,
|
|
45
|
+
patterns: resolvedPatterns.map((p) => p.regex),
|
|
46
|
+
redactPrompts: config.redactPrompts ?? true,
|
|
47
|
+
redactResponses: config.redactResponses ?? true,
|
|
48
|
+
redactMetadata: config.redactMetadata ?? false,
|
|
49
|
+
replacement: config.replacement ?? '[REDACTED]',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Apply redaction to a text string
|
|
55
|
+
*/
|
|
56
|
+
export function redactText(
|
|
57
|
+
text: string,
|
|
58
|
+
patterns: (string | RegExp)[],
|
|
59
|
+
replacement = '[REDACTED]'
|
|
60
|
+
): RedactionResult {
|
|
61
|
+
if (!text || patterns.length === 0) {
|
|
62
|
+
return {
|
|
63
|
+
text,
|
|
64
|
+
wasRedacted: false,
|
|
65
|
+
redactionCount: 0,
|
|
66
|
+
matchedPatterns: [],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let result = text;
|
|
71
|
+
let totalCount = 0;
|
|
72
|
+
const matchedPatterns: string[] = [];
|
|
73
|
+
|
|
74
|
+
for (const pattern of patterns) {
|
|
75
|
+
const regex = typeof pattern === 'string' ? new RegExp(pattern, 'g') : pattern;
|
|
76
|
+
const matches = result.match(regex);
|
|
77
|
+
|
|
78
|
+
if (matches && matches.length > 0) {
|
|
79
|
+
totalCount += matches.length;
|
|
80
|
+
matchedPatterns.push(regex.source.slice(0, 30));
|
|
81
|
+
result = result.replace(regex, replacement);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
text: result,
|
|
87
|
+
wasRedacted: totalCount > 0,
|
|
88
|
+
redactionCount: totalCount,
|
|
89
|
+
matchedPatterns: [...new Set(matchedPatterns)],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Generate a SHA-256 hash of the original text
|
|
95
|
+
* Useful for audit trails where you need to verify content without storing it
|
|
96
|
+
*/
|
|
97
|
+
export function hashText(text: string): string {
|
|
98
|
+
return createHash('sha256').update(text).digest('hex');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Redact text and return both redacted version and hash of original
|
|
103
|
+
*/
|
|
104
|
+
export function redactWithHash(
|
|
105
|
+
text: string,
|
|
106
|
+
patterns: (string | RegExp)[],
|
|
107
|
+
replacement = '[REDACTED]'
|
|
108
|
+
): RedactionResult & { originalHash: string } {
|
|
109
|
+
const originalHash = hashText(text);
|
|
110
|
+
const result = redactText(text, patterns, replacement);
|
|
111
|
+
return { ...result, originalHash };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Main Redactor class for applying redaction with consistent configuration
|
|
116
|
+
*/
|
|
117
|
+
export class Redactor {
|
|
118
|
+
private options: RedactionOptions;
|
|
119
|
+
private resolvedPatterns: { regex: RegExp; name: string }[];
|
|
120
|
+
|
|
121
|
+
constructor(config: RedactionConfig) {
|
|
122
|
+
this.options = createRedactionOptions(config);
|
|
123
|
+
const patternNames = config.patterns?.length ? config.patterns : DEFAULT_REDACTION_PATTERNS;
|
|
124
|
+
this.resolvedPatterns = resolvePatterns(patternNames);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Check if redaction is enabled
|
|
129
|
+
*/
|
|
130
|
+
get enabled(): boolean {
|
|
131
|
+
return this.options.enabled;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get pattern names being used
|
|
136
|
+
*/
|
|
137
|
+
get patternNames(): string[] {
|
|
138
|
+
return this.resolvedPatterns.map((p) => p.name);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get the replacement string
|
|
143
|
+
*/
|
|
144
|
+
get replacement(): string {
|
|
145
|
+
return this.options.replacement;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Redact a prompt string
|
|
150
|
+
*/
|
|
151
|
+
redactPrompt(prompt: string): RedactionResult {
|
|
152
|
+
if (!this.options.enabled || !this.options.redactPrompts) {
|
|
153
|
+
return {
|
|
154
|
+
text: prompt,
|
|
155
|
+
wasRedacted: false,
|
|
156
|
+
redactionCount: 0,
|
|
157
|
+
matchedPatterns: [],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
return redactText(prompt, this.options.patterns, this.options.replacement);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Redact a response string
|
|
165
|
+
*/
|
|
166
|
+
redactResponse(response: string): RedactionResult {
|
|
167
|
+
if (!this.options.enabled || !this.options.redactResponses) {
|
|
168
|
+
return {
|
|
169
|
+
text: response,
|
|
170
|
+
wasRedacted: false,
|
|
171
|
+
redactionCount: 0,
|
|
172
|
+
matchedPatterns: [],
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return redactText(response, this.options.patterns, this.options.replacement);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Redact metadata object values
|
|
180
|
+
*/
|
|
181
|
+
redactMetadata<T extends Record<string, unknown>>(metadata: T): T {
|
|
182
|
+
if (!this.options.enabled || !this.options.redactMetadata) {
|
|
183
|
+
return metadata;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const redacted = { ...metadata } as T;
|
|
187
|
+
for (const key of Object.keys(redacted)) {
|
|
188
|
+
const value = redacted[key];
|
|
189
|
+
if (typeof value === 'string') {
|
|
190
|
+
const result = redactText(value, this.options.patterns, this.options.replacement);
|
|
191
|
+
(redacted as Record<string, unknown>)[key] = result.text;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return redacted;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Redact any text with current configuration
|
|
199
|
+
*/
|
|
200
|
+
redact(text: string): RedactionResult {
|
|
201
|
+
if (!this.options.enabled) {
|
|
202
|
+
return {
|
|
203
|
+
text,
|
|
204
|
+
wasRedacted: false,
|
|
205
|
+
redactionCount: 0,
|
|
206
|
+
matchedPatterns: [],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
return redactText(text, this.options.patterns, this.options.replacement);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Redact and get hash of original
|
|
214
|
+
*/
|
|
215
|
+
redactAndHash(text: string): RedactionResult & { originalHash: string } {
|
|
216
|
+
const originalHash = hashText(text);
|
|
217
|
+
const result = this.redact(text);
|
|
218
|
+
return { ...result, originalHash };
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Create a no-op redactor that doesn't redact anything
|
|
224
|
+
*/
|
|
225
|
+
export function createNoOpRedactor(): Redactor {
|
|
226
|
+
return new Redactor({
|
|
227
|
+
enabled: false,
|
|
228
|
+
redactPrompts: true,
|
|
229
|
+
redactResponses: true,
|
|
230
|
+
redactMetadata: false,
|
|
231
|
+
replacement: '[REDACTED]',
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Create a redactor with default patterns
|
|
237
|
+
*/
|
|
238
|
+
export function createDefaultRedactor(): Redactor {
|
|
239
|
+
return new Redactor({
|
|
240
|
+
enabled: true,
|
|
241
|
+
redactPrompts: true,
|
|
242
|
+
redactResponses: true,
|
|
243
|
+
redactMetadata: false,
|
|
244
|
+
replacement: '[REDACTED]',
|
|
245
|
+
});
|
|
246
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Built-in redaction pattern names
|
|
5
|
+
*/
|
|
6
|
+
export const BUILTIN_PATTERNS = {
|
|
7
|
+
/** Email addresses */
|
|
8
|
+
EMAIL: 'email',
|
|
9
|
+
/** Phone numbers (various formats) */
|
|
10
|
+
PHONE: 'phone',
|
|
11
|
+
/** Credit card numbers */
|
|
12
|
+
CREDIT_CARD: 'credit_card',
|
|
13
|
+
/** Social Security Numbers */
|
|
14
|
+
SSN: 'ssn',
|
|
15
|
+
/** API keys (common formats) */
|
|
16
|
+
API_KEY: 'api_key',
|
|
17
|
+
/** IPv4 addresses */
|
|
18
|
+
IPV4: 'ipv4',
|
|
19
|
+
/** JWT tokens */
|
|
20
|
+
JWT: 'jwt',
|
|
21
|
+
/** AWS access keys */
|
|
22
|
+
AWS_KEY: 'aws_key',
|
|
23
|
+
/** Generic secrets (password=, secret=, etc.) */
|
|
24
|
+
SECRETS: 'secrets',
|
|
25
|
+
} as const;
|
|
26
|
+
|
|
27
|
+
export type BuiltinPatternName = (typeof BUILTIN_PATTERNS)[keyof typeof BUILTIN_PATTERNS];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Regex patterns for built-in redaction
|
|
31
|
+
*/
|
|
32
|
+
export const BUILTIN_REGEX_PATTERNS: Record<BuiltinPatternName, RegExp> = {
|
|
33
|
+
[BUILTIN_PATTERNS.EMAIL]: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
|
|
34
|
+
[BUILTIN_PATTERNS.PHONE]: /\b(?:\+?1[-.\s]?)?(?:\(?\d{3}\)?[-.\s]?)?\d{3}[-.\s]?\d{4}\b/g,
|
|
35
|
+
[BUILTIN_PATTERNS.CREDIT_CARD]: /\b(?:\d{4}[-\s]?){3}\d{4}\b/g,
|
|
36
|
+
[BUILTIN_PATTERNS.SSN]: /\b\d{3}[-\s]?\d{2}[-\s]?\d{4}\b/g,
|
|
37
|
+
[BUILTIN_PATTERNS.API_KEY]:
|
|
38
|
+
/\b(?:sk|pk|api|key)[-_]?(?:[a-zA-Z0-9]+[-_]?){2,}[a-zA-Z0-9]{10,}\b/gi,
|
|
39
|
+
[BUILTIN_PATTERNS.IPV4]: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g,
|
|
40
|
+
[BUILTIN_PATTERNS.JWT]: /\beyJ[A-Za-z0-9-_]+\.eyJ[A-Za-z0-9-_]+\.[A-Za-z0-9-_.+/=]*\b/g,
|
|
41
|
+
[BUILTIN_PATTERNS.AWS_KEY]: /\b(?:AKIA|ABIA|ACCA|ASIA)[A-Z0-9]{16}\b/g,
|
|
42
|
+
[BUILTIN_PATTERNS.SECRETS]:
|
|
43
|
+
/\b(?:password|secret|token|apikey|api_key|auth)[\s]*[=:]\s*['"]?[^\s'"]+['"]?/gi,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Default patterns to use when redaction is enabled without specific patterns
|
|
48
|
+
*/
|
|
49
|
+
export const DEFAULT_REDACTION_PATTERNS: BuiltinPatternName[] = [
|
|
50
|
+
BUILTIN_PATTERNS.EMAIL,
|
|
51
|
+
BUILTIN_PATTERNS.PHONE,
|
|
52
|
+
BUILTIN_PATTERNS.CREDIT_CARD,
|
|
53
|
+
BUILTIN_PATTERNS.SSN,
|
|
54
|
+
BUILTIN_PATTERNS.API_KEY,
|
|
55
|
+
BUILTIN_PATTERNS.AWS_KEY,
|
|
56
|
+
BUILTIN_PATTERNS.SECRETS,
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Redaction configuration schema for scenarios
|
|
61
|
+
*/
|
|
62
|
+
export const RedactionConfigSchema = z.object({
|
|
63
|
+
/** Enable redaction */
|
|
64
|
+
enabled: z.boolean().default(false),
|
|
65
|
+
/** Built-in pattern names or custom regex patterns */
|
|
66
|
+
patterns: z.array(z.string()).optional(),
|
|
67
|
+
/** Redact prompt content */
|
|
68
|
+
redactPrompts: z.boolean().optional().default(true),
|
|
69
|
+
/** Redact response content */
|
|
70
|
+
redactResponses: z.boolean().optional().default(true),
|
|
71
|
+
/** Redact metadata fields */
|
|
72
|
+
redactMetadata: z.boolean().optional().default(false),
|
|
73
|
+
/** Custom replacement string */
|
|
74
|
+
replacement: z.string().optional().default('[REDACTED]'),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
export type RedactionConfig = z.infer<typeof RedactionConfigSchema>;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Redaction options used at runtime
|
|
81
|
+
*/
|
|
82
|
+
export interface RedactionOptions {
|
|
83
|
+
enabled: boolean;
|
|
84
|
+
patterns: (string | RegExp)[];
|
|
85
|
+
redactPrompts: boolean;
|
|
86
|
+
redactResponses: boolean;
|
|
87
|
+
redactMetadata: boolean;
|
|
88
|
+
replacement: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Result of a redaction operation
|
|
93
|
+
*/
|
|
94
|
+
export interface RedactionResult {
|
|
95
|
+
/** The redacted text */
|
|
96
|
+
text: string;
|
|
97
|
+
/** Whether any redaction was applied */
|
|
98
|
+
wasRedacted: boolean;
|
|
99
|
+
/** Count of redactions made */
|
|
100
|
+
redactionCount: number;
|
|
101
|
+
/** Types of patterns that matched */
|
|
102
|
+
matchedPatterns: string[];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Metadata about redaction in a manifest
|
|
107
|
+
*/
|
|
108
|
+
export interface RedactionMetadata {
|
|
109
|
+
/** Whether redaction was enabled */
|
|
110
|
+
enabled: boolean;
|
|
111
|
+
/** Patterns used (names only, not actual regex) */
|
|
112
|
+
patternsUsed: string[];
|
|
113
|
+
/** Replacement string used */
|
|
114
|
+
replacement: string;
|
|
115
|
+
/** Summary of what was redacted */
|
|
116
|
+
summary: {
|
|
117
|
+
promptsRedacted: number;
|
|
118
|
+
responsesRedacted: number;
|
|
119
|
+
totalRedactions: number;
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Redaction details for a single case result
|
|
125
|
+
*/
|
|
126
|
+
export interface CaseRedactionDetails {
|
|
127
|
+
/** Whether this case had redaction applied */
|
|
128
|
+
redacted: boolean;
|
|
129
|
+
/** Whether prompt was redacted */
|
|
130
|
+
promptRedacted: boolean;
|
|
131
|
+
/** Whether response was redacted */
|
|
132
|
+
responseRedacted: boolean;
|
|
133
|
+
/** Number of redactions in this case */
|
|
134
|
+
redactionCount: number;
|
|
135
|
+
}
|