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.
@@ -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
+ });