claude-session-share 1.0.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 +21 -0
- package/README.md +313 -0
- package/dist/__tests__/e2e.test.js +532 -0
- package/dist/__tests__/gist-client.test.js +341 -0
- package/dist/__tests__/index.test.js +16 -0
- package/dist/__tests__/mcp-integration.test.js +403 -0
- package/dist/__tests__/path-encoding.test.js +77 -0
- package/dist/__tests__/pipeline.test.js +342 -0
- package/dist/__tests__/redactor.test.js +162 -0
- package/dist/__tests__/sanitizer.test.js +345 -0
- package/dist/__tests__/session-importer.test.js +216 -0
- package/dist/__tests__/session-reader.test.js +298 -0
- package/dist/__tests__/session-uploader.test.js +216 -0
- package/dist/__tests__/session-writer.test.js +286 -0
- package/dist/__tests__/uuid-mapper.test.js +249 -0
- package/dist/gist/client.js +199 -0
- package/dist/gist/types.js +7 -0
- package/dist/index.js +214 -0
- package/dist/sanitization/pipeline.js +48 -0
- package/dist/sanitization/redactor.js +48 -0
- package/dist/sanitization/sanitizer.js +87 -0
- package/dist/services/session-importer.js +88 -0
- package/dist/services/session-uploader.js +64 -0
- package/dist/session/finder.js +65 -0
- package/dist/session/metadata.js +55 -0
- package/dist/session/reader.js +101 -0
- package/dist/session/types.js +11 -0
- package/dist/session/writer.js +74 -0
- package/dist/utils/path-encoding.js +54 -0
- package/dist/utils/uuid-mapper.js +73 -0
- package/package.json +54 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { sanitizeSession, inferBasePath } from '../sanitization/pipeline.js';
|
|
3
|
+
describe('sanitizeSession', () => {
|
|
4
|
+
const basePath = '/Users/testuser/project';
|
|
5
|
+
it('should sanitize full session with all message types', () => {
|
|
6
|
+
const messages = [
|
|
7
|
+
{
|
|
8
|
+
type: 'user',
|
|
9
|
+
uuid: 'msg-1',
|
|
10
|
+
sessionId: 'session-1',
|
|
11
|
+
timestamp: '2026-01-11T12:00:00Z',
|
|
12
|
+
parentUuid: null,
|
|
13
|
+
message: { role: 'user', content: 'Hello' },
|
|
14
|
+
cwd: '/Users/testuser/project/src',
|
|
15
|
+
version: '1.0',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
type: 'assistant',
|
|
19
|
+
uuid: 'msg-2',
|
|
20
|
+
sessionId: 'session-1',
|
|
21
|
+
timestamp: '2026-01-11T12:00:01Z',
|
|
22
|
+
parentUuid: 'msg-1',
|
|
23
|
+
messageId: 'msg-2',
|
|
24
|
+
snapshot: {
|
|
25
|
+
thinking: 'Internal reasoning',
|
|
26
|
+
messages: [
|
|
27
|
+
{
|
|
28
|
+
role: 'assistant',
|
|
29
|
+
content: 'File at /Users/testuser/project/src/index.ts with api_key="secret123456"',
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
type: 'file-history-snapshot',
|
|
36
|
+
uuid: 'msg-3',
|
|
37
|
+
sessionId: 'session-1',
|
|
38
|
+
timestamp: '2026-01-11T12:00:02Z',
|
|
39
|
+
parentUuid: 'msg-2',
|
|
40
|
+
isSnapshotUpdate: false,
|
|
41
|
+
snapshot: {
|
|
42
|
+
files: [{ path: '/Users/testuser/project/src/utils.ts' }],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
const sanitized = sanitizeSession(messages, basePath);
|
|
47
|
+
// Verify thinking stripped
|
|
48
|
+
expect(sanitized[1].snapshot.thinking).toBeNull();
|
|
49
|
+
// Verify cwd sanitized
|
|
50
|
+
expect(sanitized[0].cwd).toBe('src');
|
|
51
|
+
// Verify file paths sanitized
|
|
52
|
+
expect(sanitized[2].snapshot.files[0].path).toBe('src/utils.ts');
|
|
53
|
+
// Verify secret redacted
|
|
54
|
+
expect(sanitized[1].snapshot.messages[0].content).toContain('[REDACTED]');
|
|
55
|
+
expect(sanitized[1].snapshot.messages[0].content).not.toContain('secret123456');
|
|
56
|
+
// Verify path in content sanitized
|
|
57
|
+
expect(sanitized[1].snapshot.messages[0].content).toContain('src/index.ts');
|
|
58
|
+
expect(sanitized[1].snapshot.messages[0].content).not.toContain('/Users/testuser/project/src/index.ts');
|
|
59
|
+
});
|
|
60
|
+
it('should handle empty session', () => {
|
|
61
|
+
const messages = [];
|
|
62
|
+
const sanitized = sanitizeSession(messages, basePath);
|
|
63
|
+
expect(sanitized).toEqual([]);
|
|
64
|
+
});
|
|
65
|
+
it('should handle session with only user messages', () => {
|
|
66
|
+
const messages = [
|
|
67
|
+
{
|
|
68
|
+
type: 'user',
|
|
69
|
+
uuid: 'msg-1',
|
|
70
|
+
sessionId: 'session-1',
|
|
71
|
+
timestamp: '2026-01-11T12:00:00Z',
|
|
72
|
+
parentUuid: null,
|
|
73
|
+
message: { role: 'user', content: 'Hello' },
|
|
74
|
+
cwd: '/Users/testuser/project',
|
|
75
|
+
version: '1.0',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
type: 'user',
|
|
79
|
+
uuid: 'msg-2',
|
|
80
|
+
sessionId: 'session-1',
|
|
81
|
+
timestamp: '2026-01-11T12:00:01Z',
|
|
82
|
+
parentUuid: 'msg-1',
|
|
83
|
+
message: { role: 'user', content: 'Another message' },
|
|
84
|
+
cwd: '/Users/testuser/project/src',
|
|
85
|
+
version: '1.0',
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
const sanitized = sanitizeSession(messages, basePath);
|
|
89
|
+
expect(sanitized[0].cwd).toBe('.');
|
|
90
|
+
expect(sanitized[1].cwd).toBe('src');
|
|
91
|
+
});
|
|
92
|
+
it('should handle session with only assistant messages', () => {
|
|
93
|
+
const messages = [
|
|
94
|
+
{
|
|
95
|
+
type: 'assistant',
|
|
96
|
+
uuid: 'msg-1',
|
|
97
|
+
sessionId: 'session-1',
|
|
98
|
+
timestamp: '2026-01-11T12:00:00Z',
|
|
99
|
+
parentUuid: null,
|
|
100
|
+
messageId: 'msg-1',
|
|
101
|
+
snapshot: {
|
|
102
|
+
thinking: 'Thinking 1',
|
|
103
|
+
messages: [{ role: 'assistant', content: 'Response 1' }],
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
type: 'assistant',
|
|
108
|
+
uuid: 'msg-2',
|
|
109
|
+
sessionId: 'session-1',
|
|
110
|
+
timestamp: '2026-01-11T12:00:01Z',
|
|
111
|
+
parentUuid: 'msg-1',
|
|
112
|
+
messageId: 'msg-2',
|
|
113
|
+
snapshot: {
|
|
114
|
+
thinking: 'Thinking 2',
|
|
115
|
+
messages: [{ role: 'assistant', content: 'Response 2' }],
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
const sanitized = sanitizeSession(messages, basePath);
|
|
120
|
+
expect(sanitized[0].snapshot.thinking).toBeNull();
|
|
121
|
+
expect(sanitized[1].snapshot.thinking).toBeNull();
|
|
122
|
+
});
|
|
123
|
+
it('should preserve message order', () => {
|
|
124
|
+
const messages = [
|
|
125
|
+
{
|
|
126
|
+
type: 'user',
|
|
127
|
+
uuid: 'msg-1',
|
|
128
|
+
sessionId: 'session-1',
|
|
129
|
+
timestamp: '2026-01-11T12:00:00Z',
|
|
130
|
+
parentUuid: null,
|
|
131
|
+
message: { role: 'user', content: 'First' },
|
|
132
|
+
cwd: '/Users/testuser/project',
|
|
133
|
+
version: '1.0',
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
type: 'assistant',
|
|
137
|
+
uuid: 'msg-2',
|
|
138
|
+
sessionId: 'session-1',
|
|
139
|
+
timestamp: '2026-01-11T12:00:01Z',
|
|
140
|
+
parentUuid: 'msg-1',
|
|
141
|
+
messageId: 'msg-2',
|
|
142
|
+
snapshot: { thinking: null, messages: [{ role: 'assistant', content: 'Second' }] },
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
type: 'user',
|
|
146
|
+
uuid: 'msg-3',
|
|
147
|
+
sessionId: 'session-1',
|
|
148
|
+
timestamp: '2026-01-11T12:00:02Z',
|
|
149
|
+
parentUuid: 'msg-2',
|
|
150
|
+
message: { role: 'user', content: 'Third' },
|
|
151
|
+
cwd: '/Users/testuser/project',
|
|
152
|
+
version: '1.0',
|
|
153
|
+
},
|
|
154
|
+
];
|
|
155
|
+
const sanitized = sanitizeSession(messages, basePath);
|
|
156
|
+
expect(sanitized[0].type).toBe('user');
|
|
157
|
+
expect(sanitized[1].type).toBe('assistant');
|
|
158
|
+
expect(sanitized[2].type).toBe('user');
|
|
159
|
+
expect(sanitized[0].message.content).toBe('First');
|
|
160
|
+
expect(sanitized[1].snapshot.messages[0].content).toBe('Second');
|
|
161
|
+
expect(sanitized[2].message.content).toBe('Third');
|
|
162
|
+
});
|
|
163
|
+
it('should be immutable (preserve original messages)', () => {
|
|
164
|
+
const messages = [
|
|
165
|
+
{
|
|
166
|
+
type: 'assistant',
|
|
167
|
+
uuid: 'msg-1',
|
|
168
|
+
sessionId: 'session-1',
|
|
169
|
+
timestamp: '2026-01-11T12:00:00Z',
|
|
170
|
+
parentUuid: null,
|
|
171
|
+
messageId: 'msg-1',
|
|
172
|
+
snapshot: {
|
|
173
|
+
thinking: 'Original thinking',
|
|
174
|
+
messages: [{ role: 'assistant', content: 'Original content' }],
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
];
|
|
178
|
+
const sanitized = sanitizeSession(messages, basePath);
|
|
179
|
+
// Original unchanged
|
|
180
|
+
expect(messages[0].snapshot.thinking).toBe('Original thinking');
|
|
181
|
+
expect(messages[0].snapshot.messages[0].content).toBe('Original content');
|
|
182
|
+
// Sanitized changed
|
|
183
|
+
expect(sanitized[0].snapshot.thinking).toBeNull();
|
|
184
|
+
});
|
|
185
|
+
it('should handle realistic session snippet', () => {
|
|
186
|
+
// Fixture: Small realistic session with thinking, paths, and secrets
|
|
187
|
+
const messages = [
|
|
188
|
+
{
|
|
189
|
+
type: 'user',
|
|
190
|
+
uuid: 'user-1',
|
|
191
|
+
sessionId: 'session-abc',
|
|
192
|
+
timestamp: '2026-01-11T14:30:00Z',
|
|
193
|
+
parentUuid: null,
|
|
194
|
+
message: { role: 'user', content: 'Create an API endpoint' },
|
|
195
|
+
cwd: '/Users/testuser/project',
|
|
196
|
+
version: '1.0',
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
type: 'assistant',
|
|
200
|
+
uuid: 'asst-1',
|
|
201
|
+
sessionId: 'session-abc',
|
|
202
|
+
timestamp: '2026-01-11T14:30:05Z',
|
|
203
|
+
parentUuid: 'user-1',
|
|
204
|
+
messageId: 'asst-1',
|
|
205
|
+
snapshot: {
|
|
206
|
+
thinking: 'User wants an API endpoint. I should create a REST endpoint with proper validation.',
|
|
207
|
+
messages: [
|
|
208
|
+
{
|
|
209
|
+
role: 'assistant',
|
|
210
|
+
content: 'I will create the endpoint at /Users/testuser/project/src/api/users.ts',
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
role: 'tool',
|
|
214
|
+
content: JSON.stringify({
|
|
215
|
+
file_path: '/Users/testuser/project/src/api/users.ts',
|
|
216
|
+
api_key: 'sk-1234567890abcdef',
|
|
217
|
+
status: 'created',
|
|
218
|
+
}),
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
type: 'file-history-snapshot',
|
|
225
|
+
uuid: 'snapshot-1',
|
|
226
|
+
sessionId: 'session-abc',
|
|
227
|
+
timestamp: '2026-01-11T14:30:06Z',
|
|
228
|
+
parentUuid: 'asst-1',
|
|
229
|
+
isSnapshotUpdate: true,
|
|
230
|
+
snapshot: {
|
|
231
|
+
files: [
|
|
232
|
+
{ path: '/Users/testuser/project/src/api/users.ts' },
|
|
233
|
+
{ path: '/Users/testuser/project/src/types.ts' },
|
|
234
|
+
],
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
];
|
|
238
|
+
const sanitized = sanitizeSession(messages, basePath);
|
|
239
|
+
// User message: cwd sanitized
|
|
240
|
+
expect(sanitized[0].cwd).toBe('.');
|
|
241
|
+
// Assistant message: thinking stripped, paths sanitized, secrets redacted
|
|
242
|
+
const assistantMsg = sanitized[1];
|
|
243
|
+
expect(assistantMsg.snapshot.thinking).toBeNull();
|
|
244
|
+
expect(assistantMsg.snapshot.messages[0].content).toContain('src/api/users.ts');
|
|
245
|
+
expect(assistantMsg.snapshot.messages[0].content).not.toContain('/Users/testuser/project/src');
|
|
246
|
+
expect(assistantMsg.snapshot.messages[1].content).toContain('[REDACTED]');
|
|
247
|
+
expect(assistantMsg.snapshot.messages[1].content).not.toContain('sk-1234567890abcdef');
|
|
248
|
+
// File snapshot: paths sanitized
|
|
249
|
+
const snapshotMsg = sanitized[2];
|
|
250
|
+
expect(snapshotMsg.snapshot.files[0].path).toBe('src/api/users.ts');
|
|
251
|
+
expect(snapshotMsg.snapshot.files[1].path).toBe('src/types.ts');
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
describe('inferBasePath', () => {
|
|
255
|
+
it('should extract base path from first user message', () => {
|
|
256
|
+
const messages = [
|
|
257
|
+
{
|
|
258
|
+
type: 'user',
|
|
259
|
+
uuid: 'msg-1',
|
|
260
|
+
sessionId: 'session-1',
|
|
261
|
+
timestamp: '2026-01-11T12:00:00Z',
|
|
262
|
+
parentUuid: null,
|
|
263
|
+
message: { role: 'user', content: 'Hello' },
|
|
264
|
+
cwd: '/Users/testuser/project',
|
|
265
|
+
version: '1.0',
|
|
266
|
+
},
|
|
267
|
+
];
|
|
268
|
+
const basePath = inferBasePath(messages);
|
|
269
|
+
expect(basePath).toBe('/Users/testuser/project');
|
|
270
|
+
});
|
|
271
|
+
it('should return empty string if no user messages', () => {
|
|
272
|
+
const messages = [
|
|
273
|
+
{
|
|
274
|
+
type: 'assistant',
|
|
275
|
+
uuid: 'msg-1',
|
|
276
|
+
sessionId: 'session-1',
|
|
277
|
+
timestamp: '2026-01-11T12:00:00Z',
|
|
278
|
+
parentUuid: null,
|
|
279
|
+
messageId: 'msg-1',
|
|
280
|
+
snapshot: { thinking: null, messages: [] },
|
|
281
|
+
},
|
|
282
|
+
];
|
|
283
|
+
const basePath = inferBasePath(messages);
|
|
284
|
+
expect(basePath).toBe('');
|
|
285
|
+
});
|
|
286
|
+
it('should return empty string for empty session', () => {
|
|
287
|
+
const messages = [];
|
|
288
|
+
const basePath = inferBasePath(messages);
|
|
289
|
+
expect(basePath).toBe('');
|
|
290
|
+
});
|
|
291
|
+
it('should use first user message even if multiple exist', () => {
|
|
292
|
+
const messages = [
|
|
293
|
+
{
|
|
294
|
+
type: 'user',
|
|
295
|
+
uuid: 'msg-1',
|
|
296
|
+
sessionId: 'session-1',
|
|
297
|
+
timestamp: '2026-01-11T12:00:00Z',
|
|
298
|
+
parentUuid: null,
|
|
299
|
+
message: { role: 'user', content: 'First' },
|
|
300
|
+
cwd: '/Users/testuser/project',
|
|
301
|
+
version: '1.0',
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
type: 'user',
|
|
305
|
+
uuid: 'msg-2',
|
|
306
|
+
sessionId: 'session-1',
|
|
307
|
+
timestamp: '2026-01-11T12:00:01Z',
|
|
308
|
+
parentUuid: 'msg-1',
|
|
309
|
+
message: { role: 'user', content: 'Second' },
|
|
310
|
+
cwd: '/Users/testuser/different',
|
|
311
|
+
version: '1.0',
|
|
312
|
+
},
|
|
313
|
+
];
|
|
314
|
+
const basePath = inferBasePath(messages);
|
|
315
|
+
expect(basePath).toBe('/Users/testuser/project');
|
|
316
|
+
});
|
|
317
|
+
it('should skip non-user messages when finding cwd', () => {
|
|
318
|
+
const messages = [
|
|
319
|
+
{
|
|
320
|
+
type: 'assistant',
|
|
321
|
+
uuid: 'msg-1',
|
|
322
|
+
sessionId: 'session-1',
|
|
323
|
+
timestamp: '2026-01-11T12:00:00Z',
|
|
324
|
+
parentUuid: null,
|
|
325
|
+
messageId: 'msg-1',
|
|
326
|
+
snapshot: { thinking: null, messages: [] },
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
type: 'user',
|
|
330
|
+
uuid: 'msg-2',
|
|
331
|
+
sessionId: 'session-1',
|
|
332
|
+
timestamp: '2026-01-11T12:00:01Z',
|
|
333
|
+
parentUuid: 'msg-1',
|
|
334
|
+
message: { role: 'user', content: 'Hello' },
|
|
335
|
+
cwd: '/Users/testuser/project',
|
|
336
|
+
version: '1.0',
|
|
337
|
+
},
|
|
338
|
+
];
|
|
339
|
+
const basePath = inferBasePath(messages);
|
|
340
|
+
expect(basePath).toBe('/Users/testuser/project');
|
|
341
|
+
});
|
|
342
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { redactSecrets } from '../sanitization/redactor.js';
|
|
3
|
+
describe('redactSecrets', () => {
|
|
4
|
+
describe('API keys in JSON notation', () => {
|
|
5
|
+
it('should redact api_key in JSON', () => {
|
|
6
|
+
const input = '{"api_key": "sk-1234567890abcdef"}';
|
|
7
|
+
const result = redactSecrets(input);
|
|
8
|
+
expect(result).toBe('{"api_key": "[REDACTED]"}');
|
|
9
|
+
});
|
|
10
|
+
it('should redact apiKey in JSON', () => {
|
|
11
|
+
const input = '{"apiKey": "abc123def456xyz789"}';
|
|
12
|
+
const result = redactSecrets(input);
|
|
13
|
+
expect(result).toBe('{"apiKey": "[REDACTED]"}');
|
|
14
|
+
});
|
|
15
|
+
it('should redact API_KEY in JSON', () => {
|
|
16
|
+
const input = '{"API_KEY": "secret123456"}';
|
|
17
|
+
const result = redactSecrets(input);
|
|
18
|
+
expect(result).toBe('{"API_KEY": "[REDACTED]"}');
|
|
19
|
+
});
|
|
20
|
+
it('should redact access_token', () => {
|
|
21
|
+
const input = '{"access_token": "token123456789"}';
|
|
22
|
+
const result = redactSecrets(input);
|
|
23
|
+
expect(result).toBe('{"access_token": "[REDACTED]"}');
|
|
24
|
+
});
|
|
25
|
+
it('should redact bearer_token', () => {
|
|
26
|
+
const input = '{"bearer_token": "bearer123456789"}';
|
|
27
|
+
const result = redactSecrets(input);
|
|
28
|
+
expect(result).toBe('{"bearer_token": "[REDACTED]"}');
|
|
29
|
+
});
|
|
30
|
+
it('should redact secret_key', () => {
|
|
31
|
+
const input = '{"secret_key": "secret123456"}';
|
|
32
|
+
const result = redactSecrets(input);
|
|
33
|
+
expect(result).toBe('{"secret_key": "[REDACTED]"}');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe('Environment variable format', () => {
|
|
37
|
+
it('should redact API_KEY=value', () => {
|
|
38
|
+
const input = 'API_KEY=abc123def456';
|
|
39
|
+
const result = redactSecrets(input);
|
|
40
|
+
expect(result).toBe('API_KEY=[REDACTED]');
|
|
41
|
+
});
|
|
42
|
+
it('should redact SECRET_TOKEN=value', () => {
|
|
43
|
+
const input = 'SECRET_TOKEN=xyz789abc123';
|
|
44
|
+
const result = redactSecrets(input);
|
|
45
|
+
expect(result).toBe('SECRET_TOKEN=[REDACTED]');
|
|
46
|
+
});
|
|
47
|
+
it('should redact multiple env vars', () => {
|
|
48
|
+
const input = 'API_KEY=abc123def456\nSECRET_PASSWORD=xyz789abc123';
|
|
49
|
+
const result = redactSecrets(input);
|
|
50
|
+
expect(result).toContain('API_KEY=[REDACTED]');
|
|
51
|
+
expect(result).toContain('SECRET_PASSWORD=[REDACTED]');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe('GitHub tokens', () => {
|
|
55
|
+
it('should redact ghp_ tokens', () => {
|
|
56
|
+
const input = 'token: ghp_1234567890abcdefghijklmnopqrstuvwxyz';
|
|
57
|
+
const result = redactSecrets(input);
|
|
58
|
+
expect(result).toBe('token: [REDACTED]');
|
|
59
|
+
});
|
|
60
|
+
it('should redact ghs_ tokens', () => {
|
|
61
|
+
const input = 'token: ghs_1234567890abcdefghijklmnopqrstuvwxyz';
|
|
62
|
+
const result = redactSecrets(input);
|
|
63
|
+
expect(result).toBe('token: [REDACTED]');
|
|
64
|
+
});
|
|
65
|
+
it('should redact github_pat_ tokens', () => {
|
|
66
|
+
const input = 'token: github_pat_1234567890abcdefghijklmnopqrstuvwxyz';
|
|
67
|
+
const result = redactSecrets(input);
|
|
68
|
+
expect(result).toBe('token: [REDACTED]');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe('AWS credentials', () => {
|
|
72
|
+
it('should redact AWS access keys', () => {
|
|
73
|
+
const input = 'AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE';
|
|
74
|
+
const result = redactSecrets(input);
|
|
75
|
+
expect(result).toContain('[REDACTED]');
|
|
76
|
+
});
|
|
77
|
+
it('should redact aws_secret_access_key', () => {
|
|
78
|
+
const input = 'aws_secret_access_key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY';
|
|
79
|
+
const result = redactSecrets(input);
|
|
80
|
+
expect(result).toContain('[REDACTED]');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
describe('Generic long strings', () => {
|
|
84
|
+
it('should redact base64-like strings', () => {
|
|
85
|
+
const input = 'token=SGVsbG9Xb3JsZFRoaXNJc0FMb25nQmFzZTY0U3RyaW5n';
|
|
86
|
+
const result = redactSecrets(input);
|
|
87
|
+
expect(result).toBe('token=[REDACTED]');
|
|
88
|
+
});
|
|
89
|
+
it('should not redact short strings', () => {
|
|
90
|
+
const input = 'token=short123';
|
|
91
|
+
const result = redactSecrets(input);
|
|
92
|
+
// Short values might still be caught by other patterns, but generic pattern should not
|
|
93
|
+
expect(result).toContain('short123');
|
|
94
|
+
});
|
|
95
|
+
it('should not redact file paths', () => {
|
|
96
|
+
const input = 'path=/usr/local/bin/node/lib/modules/core';
|
|
97
|
+
const result = redactSecrets(input);
|
|
98
|
+
expect(result).toContain('/usr/local/bin');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
describe('Private keys', () => {
|
|
102
|
+
it('should redact RSA private keys', () => {
|
|
103
|
+
const input = `-----BEGIN RSA PRIVATE KEY-----
|
|
104
|
+
MIIEowIBAAKCAQEA123456789
|
|
105
|
+
-----END RSA PRIVATE KEY-----`;
|
|
106
|
+
const result = redactSecrets(input);
|
|
107
|
+
expect(result).toBe('[REDACTED PRIVATE KEY]');
|
|
108
|
+
});
|
|
109
|
+
it('should redact generic private keys', () => {
|
|
110
|
+
const input = `-----BEGIN PRIVATE KEY-----
|
|
111
|
+
MIIEvQIBADANBgkqhkiG9w0BAQEF
|
|
112
|
+
-----END PRIVATE KEY-----`;
|
|
113
|
+
const result = redactSecrets(input);
|
|
114
|
+
expect(result).toBe('[REDACTED PRIVATE KEY]');
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
describe('Mixed content', () => {
|
|
118
|
+
it('should redact secrets while preserving other text', () => {
|
|
119
|
+
const input = 'Response from API: {"api_key": "secret123456", "status": "success"}';
|
|
120
|
+
const result = redactSecrets(input);
|
|
121
|
+
expect(result).toContain('[REDACTED]');
|
|
122
|
+
expect(result).toContain('status');
|
|
123
|
+
expect(result).toContain('success');
|
|
124
|
+
expect(result).not.toContain('secret123456');
|
|
125
|
+
});
|
|
126
|
+
it('should handle multiple secret types in one string', () => {
|
|
127
|
+
const input = `Config:
|
|
128
|
+
API_KEY=abc123def456
|
|
129
|
+
token: ghp_1234567890abcdefghijklmnopqrstuvwxyz
|
|
130
|
+
AWS: AKIAIOSFODNN7EXAMPLE`;
|
|
131
|
+
const result = redactSecrets(input);
|
|
132
|
+
expect(result).toContain('API_KEY=[REDACTED]');
|
|
133
|
+
expect(result).toContain('token: [REDACTED]');
|
|
134
|
+
expect(result).toContain('[REDACTED]'); // AWS key
|
|
135
|
+
expect(result).not.toContain('abc123def456');
|
|
136
|
+
expect(result).not.toContain('ghp_123');
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
describe('Edge cases', () => {
|
|
140
|
+
it('should handle empty string', () => {
|
|
141
|
+
const result = redactSecrets('');
|
|
142
|
+
expect(result).toBe('');
|
|
143
|
+
});
|
|
144
|
+
it('should handle content with no secrets', () => {
|
|
145
|
+
const input = 'This is just normal text with no secrets';
|
|
146
|
+
const result = redactSecrets(input);
|
|
147
|
+
expect(result).toBe(input);
|
|
148
|
+
});
|
|
149
|
+
it('should handle null/undefined values gracefully', () => {
|
|
150
|
+
expect(redactSecrets('')).toBe('');
|
|
151
|
+
});
|
|
152
|
+
it('should preserve structure when redacting', () => {
|
|
153
|
+
const input = '{"apiKey": "secret123456", "userId": 42, "name": "test"}';
|
|
154
|
+
const result = redactSecrets(input);
|
|
155
|
+
expect(result).toContain('userId');
|
|
156
|
+
expect(result).toContain('42');
|
|
157
|
+
expect(result).toContain('name');
|
|
158
|
+
expect(result).toContain('test');
|
|
159
|
+
expect(result).not.toContain('secret123456');
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
});
|