tlc-claude-code 1.3.0 → 1.4.1

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.
Files changed (105) hide show
  1. package/dashboard/dist/components/AuditPane.d.ts +30 -0
  2. package/dashboard/dist/components/AuditPane.js +127 -0
  3. package/dashboard/dist/components/AuditPane.test.d.ts +1 -0
  4. package/dashboard/dist/components/AuditPane.test.js +339 -0
  5. package/dashboard/dist/components/CompliancePane.d.ts +39 -0
  6. package/dashboard/dist/components/CompliancePane.js +96 -0
  7. package/dashboard/dist/components/CompliancePane.test.d.ts +1 -0
  8. package/dashboard/dist/components/CompliancePane.test.js +183 -0
  9. package/dashboard/dist/components/SSOPane.d.ts +36 -0
  10. package/dashboard/dist/components/SSOPane.js +71 -0
  11. package/dashboard/dist/components/SSOPane.test.d.ts +1 -0
  12. package/dashboard/dist/components/SSOPane.test.js +155 -0
  13. package/dashboard/dist/components/WorkspaceDocsPane.js +0 -16
  14. package/dashboard/dist/components/WorkspacePane.d.ts +1 -1
  15. package/dashboard/dist/components/ZeroRetentionPane.d.ts +44 -0
  16. package/dashboard/dist/components/ZeroRetentionPane.js +83 -0
  17. package/dashboard/dist/components/ZeroRetentionPane.test.d.ts +1 -0
  18. package/dashboard/dist/components/ZeroRetentionPane.test.js +160 -0
  19. package/package.json +1 -1
  20. package/server/lib/access-control-doc.js +541 -0
  21. package/server/lib/access-control-doc.test.js +672 -0
  22. package/server/lib/adr-generator.js +423 -0
  23. package/server/lib/adr-generator.test.js +586 -0
  24. package/server/lib/agent-progress-monitor.js +223 -0
  25. package/server/lib/agent-progress-monitor.test.js +202 -0
  26. package/server/lib/audit-attribution.js +191 -0
  27. package/server/lib/audit-attribution.test.js +359 -0
  28. package/server/lib/audit-classifier.js +202 -0
  29. package/server/lib/audit-classifier.test.js +209 -0
  30. package/server/lib/audit-command.js +275 -0
  31. package/server/lib/audit-command.test.js +325 -0
  32. package/server/lib/audit-exporter.js +380 -0
  33. package/server/lib/audit-exporter.test.js +464 -0
  34. package/server/lib/audit-logger.js +236 -0
  35. package/server/lib/audit-logger.test.js +364 -0
  36. package/server/lib/audit-query.js +257 -0
  37. package/server/lib/audit-query.test.js +352 -0
  38. package/server/lib/audit-storage.js +269 -0
  39. package/server/lib/audit-storage.test.js +272 -0
  40. package/server/lib/bulk-repo-init.js +342 -0
  41. package/server/lib/bulk-repo-init.test.js +388 -0
  42. package/server/lib/compliance-checklist.js +866 -0
  43. package/server/lib/compliance-checklist.test.js +476 -0
  44. package/server/lib/compliance-command.js +616 -0
  45. package/server/lib/compliance-command.test.js +551 -0
  46. package/server/lib/compliance-reporter.js +692 -0
  47. package/server/lib/compliance-reporter.test.js +707 -0
  48. package/server/lib/data-flow-doc.js +665 -0
  49. package/server/lib/data-flow-doc.test.js +659 -0
  50. package/server/lib/ephemeral-storage.js +249 -0
  51. package/server/lib/ephemeral-storage.test.js +254 -0
  52. package/server/lib/evidence-collector.js +627 -0
  53. package/server/lib/evidence-collector.test.js +901 -0
  54. package/server/lib/flow-diagram-generator.js +474 -0
  55. package/server/lib/flow-diagram-generator.test.js +446 -0
  56. package/server/lib/idp-manager.js +626 -0
  57. package/server/lib/idp-manager.test.js +587 -0
  58. package/server/lib/memory-exclusion.js +326 -0
  59. package/server/lib/memory-exclusion.test.js +241 -0
  60. package/server/lib/mfa-handler.js +452 -0
  61. package/server/lib/mfa-handler.test.js +490 -0
  62. package/server/lib/oauth-flow.js +375 -0
  63. package/server/lib/oauth-flow.test.js +487 -0
  64. package/server/lib/oauth-registry.js +190 -0
  65. package/server/lib/oauth-registry.test.js +306 -0
  66. package/server/lib/readme-generator.js +490 -0
  67. package/server/lib/readme-generator.test.js +493 -0
  68. package/server/lib/repo-dependency-tracker.js +261 -0
  69. package/server/lib/repo-dependency-tracker.test.js +350 -0
  70. package/server/lib/retention-policy.js +281 -0
  71. package/server/lib/retention-policy.test.js +486 -0
  72. package/server/lib/role-mapper.js +236 -0
  73. package/server/lib/role-mapper.test.js +395 -0
  74. package/server/lib/saml-provider.js +765 -0
  75. package/server/lib/saml-provider.test.js +643 -0
  76. package/server/lib/security-policy-generator.js +682 -0
  77. package/server/lib/security-policy-generator.test.js +544 -0
  78. package/server/lib/sensitive-detector.js +112 -0
  79. package/server/lib/sensitive-detector.test.js +209 -0
  80. package/server/lib/service-interaction-diagram.js +700 -0
  81. package/server/lib/service-interaction-diagram.test.js +638 -0
  82. package/server/lib/service-summary.js +553 -0
  83. package/server/lib/service-summary.test.js +619 -0
  84. package/server/lib/session-purge.js +460 -0
  85. package/server/lib/session-purge.test.js +312 -0
  86. package/server/lib/sso-command.js +544 -0
  87. package/server/lib/sso-command.test.js +552 -0
  88. package/server/lib/sso-session.js +492 -0
  89. package/server/lib/sso-session.test.js +670 -0
  90. package/server/lib/workspace-command.js +249 -0
  91. package/server/lib/workspace-command.test.js +264 -0
  92. package/server/lib/workspace-config.js +270 -0
  93. package/server/lib/workspace-config.test.js +312 -0
  94. package/server/lib/workspace-docs-command.js +547 -0
  95. package/server/lib/workspace-docs-command.test.js +692 -0
  96. package/server/lib/workspace-memory.js +451 -0
  97. package/server/lib/workspace-memory.test.js +403 -0
  98. package/server/lib/workspace-scanner.js +452 -0
  99. package/server/lib/workspace-scanner.test.js +677 -0
  100. package/server/lib/workspace-test-runner.js +315 -0
  101. package/server/lib/workspace-test-runner.test.js +294 -0
  102. package/server/lib/zero-retention-command.js +439 -0
  103. package/server/lib/zero-retention-command.test.js +448 -0
  104. package/server/lib/zero-retention.js +322 -0
  105. package/server/lib/zero-retention.test.js +258 -0
@@ -0,0 +1,326 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ /**
5
+ * Memory Exclusion Patterns
6
+ *
7
+ * Configures what data to exclude from memory persistence.
8
+ * Supports both file patterns (glob) and content patterns (regex).
9
+ */
10
+
11
+ export const MODE = {
12
+ WHITELIST: 'whitelist',
13
+ BLACKLIST: 'blacklist',
14
+ };
15
+
16
+ export const DEFAULT_FILE_PATTERNS = [
17
+ '.env',
18
+ '.env.*',
19
+ '*.pem',
20
+ '*.key',
21
+ '*credentials*',
22
+ '*secrets*',
23
+ ];
24
+
25
+ export const DEFAULT_CONTENT_PATTERNS = [
26
+ 'password=',
27
+ 'api_key=',
28
+ 'secret=',
29
+ 'token=',
30
+ 'private_key',
31
+ ];
32
+
33
+ /**
34
+ * Load exclusion patterns from .tlc.json config file
35
+ * @param {string} projectRoot - The project root directory
36
+ * @returns {object} - Patterns configuration
37
+ */
38
+ export function loadPatterns(projectRoot) {
39
+ const configPath = path.join(projectRoot, '.tlc.json');
40
+ const defaults = {
41
+ mode: MODE.BLACKLIST,
42
+ filePatterns: [...DEFAULT_FILE_PATTERNS],
43
+ contentPatterns: [...DEFAULT_CONTENT_PATTERNS],
44
+ };
45
+
46
+ try {
47
+ if (!fs.existsSync(configPath)) {
48
+ return defaults;
49
+ }
50
+
51
+ const configContent = fs.readFileSync(configPath, 'utf8');
52
+ const config = JSON.parse(configContent);
53
+
54
+ if (!config.memoryExclusion) {
55
+ return defaults;
56
+ }
57
+
58
+ const exclusionConfig = config.memoryExclusion;
59
+ const result = {
60
+ mode: exclusionConfig.mode || MODE.BLACKLIST,
61
+ filePatterns: [],
62
+ contentPatterns: [],
63
+ };
64
+
65
+ // Handle merging with defaults
66
+ if (exclusionConfig.mergeDefaults) {
67
+ result.filePatterns = [...DEFAULT_FILE_PATTERNS];
68
+ result.contentPatterns = [...DEFAULT_CONTENT_PATTERNS];
69
+ }
70
+
71
+ // Add custom file patterns
72
+ if (Array.isArray(exclusionConfig.filePatterns)) {
73
+ for (const pattern of exclusionConfig.filePatterns) {
74
+ if (!result.filePatterns.includes(pattern)) {
75
+ result.filePatterns.push(pattern);
76
+ }
77
+ }
78
+ }
79
+
80
+ // Add custom content patterns
81
+ if (Array.isArray(exclusionConfig.contentPatterns)) {
82
+ for (const pattern of exclusionConfig.contentPatterns) {
83
+ if (!result.contentPatterns.includes(pattern)) {
84
+ result.contentPatterns.push(pattern);
85
+ }
86
+ }
87
+ }
88
+
89
+ // Use defaults if no custom patterns were provided
90
+ if (result.filePatterns.length === 0) {
91
+ result.filePatterns = [...DEFAULT_FILE_PATTERNS];
92
+ }
93
+ if (result.contentPatterns.length === 0) {
94
+ result.contentPatterns = [...DEFAULT_CONTENT_PATTERNS];
95
+ }
96
+
97
+ return result;
98
+ } catch {
99
+ return defaults;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Convert a glob pattern to a regex
105
+ * @param {string} pattern - Glob pattern
106
+ * @returns {RegExp} - Compiled regex
107
+ */
108
+ function globToRegex(pattern) {
109
+ // Use placeholders to protect ** patterns during processing
110
+ let processed = pattern;
111
+
112
+ // First, handle single * - convert to placeholder to protect from ** processing
113
+ // But we need to do this carefully - only convert * that aren't part of **
114
+ // We'll process ** first with placeholders, then handle remaining *
115
+
116
+ // Handle different ** patterns:
117
+ // 1. **/ at start: matches zero or more directories (including empty)
118
+ // 2. /** at end: matches everything after
119
+ // 3. **/ in middle: matches zero or more directories
120
+
121
+ // Replace **/ at start with placeholder (matches zero or more dirs)
122
+ processed = processed.replace(/^\*\*\//, '<<STARSTART>>');
123
+
124
+ // Replace /** at end with placeholder (matches rest of path)
125
+ processed = processed.replace(/\/\*\*$/, '<<STAREND>>');
126
+
127
+ // Replace remaining **/ with placeholder (matches zero or more dirs)
128
+ processed = processed.replace(/\/\*\*\//g, '<<STARMID>>');
129
+
130
+ // Replace remaining ** with placeholder (matches anything)
131
+ processed = processed.replace(/\*\*/g, '<<STARSTAR>>');
132
+
133
+ // Now replace remaining single * with placeholder
134
+ processed = processed.replace(/\*/g, '<<SINGLE>>');
135
+
136
+ // Replace ? with placeholder
137
+ processed = processed.replace(/\?/g, '<<QUESTION>>');
138
+
139
+ // Escape special regex characters
140
+ processed = processed.replace(/[.+^${}()|[\]\\]/g, '\\$&');
141
+
142
+ // Restore and convert placeholders to regex patterns
143
+ // <<STARSTART>> = matches zero or more directories at start (optional)
144
+ processed = processed.replace(/<<STARSTART>>/g, '(?:.*\\/)?');
145
+
146
+ // <<STAREND>> = matches rest of path (including slashes)
147
+ processed = processed.replace(/<<STAREND>>/g, '(?:\\/.*)?');
148
+
149
+ // <<STARMID>> = matches zero or more directories in middle
150
+ processed = processed.replace(/<<STARMID>>/g, '(?:\\/(?:.*\\/)?)?');
151
+
152
+ // <<STARSTAR>> = matches anything including slashes
153
+ processed = processed.replace(/<<STARSTAR>>/g, '.*');
154
+
155
+ // <<SINGLE>> = matches anything except path separator
156
+ processed = processed.replace(/<<SINGLE>>/g, '[^/]*');
157
+
158
+ // <<QUESTION>> = matches single character
159
+ processed = processed.replace(/<<QUESTION>>/g, '.');
160
+
161
+ // Build regex that matches the pattern
162
+ return new RegExp(`^${processed}$`);
163
+ }
164
+
165
+ /**
166
+ * Check if a value matches a pattern
167
+ * @param {string} value - The value to test
168
+ * @param {string|RegExp} pattern - The pattern to match against
169
+ * @param {object} options - Options for matching
170
+ * @param {boolean} options.ignoreCase - Case insensitive matching
171
+ * @param {boolean} options.isContent - Whether this is content matching (uses substring)
172
+ * @returns {boolean} - Whether the value matches the pattern
173
+ */
174
+ export function matchesPattern(value, pattern, options = {}) {
175
+ if (!value) {
176
+ return false;
177
+ }
178
+
179
+ const { ignoreCase = false, isContent = false } = options;
180
+
181
+ // Handle RegExp objects
182
+ if (pattern instanceof RegExp) {
183
+ const flags = ignoreCase && !pattern.flags.includes('i') ? pattern.flags + 'i' : pattern.flags;
184
+ const testPattern = new RegExp(pattern.source, flags);
185
+ return testPattern.test(value);
186
+ }
187
+
188
+ // Handle regex: prefix for string patterns
189
+ if (pattern.startsWith('regex:')) {
190
+ const regexStr = pattern.slice(6);
191
+ const regex = ignoreCase ? new RegExp(regexStr, 'i') : new RegExp(regexStr);
192
+ return regex.test(value);
193
+ }
194
+
195
+ // For content matching without wildcards, use substring match
196
+ if (isContent && !pattern.includes('*') && !pattern.includes('?')) {
197
+ const testValue = ignoreCase ? value.toLowerCase() : value;
198
+ const testPattern = ignoreCase ? pattern.toLowerCase() : pattern;
199
+ return testValue.includes(testPattern);
200
+ }
201
+
202
+ // Handle exact match for file patterns without wildcards
203
+ if (!pattern.includes('*') && !pattern.includes('?')) {
204
+ const testValue = ignoreCase ? value.toLowerCase() : value;
205
+ const testPattern = ignoreCase ? pattern.toLowerCase() : pattern;
206
+ // Only check for exact match - no basename matching for exact patterns
207
+ return testValue === testPattern;
208
+ }
209
+
210
+ // Handle glob pattern
211
+ const regex = globToRegex(pattern);
212
+ const testPattern = ignoreCase ? new RegExp(regex.source, 'i') : regex;
213
+ return testPattern.test(value);
214
+ }
215
+
216
+ /**
217
+ * Check if a file path matches a pattern (for use in shouldExclude)
218
+ * This also checks basename for non-glob patterns
219
+ * @param {string} filePath - The file path to check
220
+ * @param {string} pattern - The pattern to match
221
+ * @returns {boolean} - Whether the file matches
222
+ */
223
+ function matchesFilePattern(filePath, pattern) {
224
+ // First try direct match
225
+ if (matchesPattern(filePath, pattern)) {
226
+ return true;
227
+ }
228
+
229
+ // For patterns without path separators and without wildcards,
230
+ // also check against the basename
231
+ if (!pattern.includes('/') && !pattern.includes('*') && !pattern.includes('?')) {
232
+ const basename = filePath.split('/').pop();
233
+ if (matchesPattern(basename, pattern)) {
234
+ return true;
235
+ }
236
+ }
237
+
238
+ // For glob patterns, also try matching the basename
239
+ if (pattern.includes('*') || pattern.includes('?')) {
240
+ const basename = filePath.split('/').pop();
241
+ if (matchesPattern(basename, pattern)) {
242
+ return true;
243
+ }
244
+ }
245
+
246
+ return false;
247
+ }
248
+
249
+ /**
250
+ * Check if a file or content should be excluded from memory
251
+ * @param {string} filePath - The file path to check
252
+ * @param {string|null} content - Optional content to check
253
+ * @param {object|null} config - Optional configuration override
254
+ * @returns {boolean} - Whether to exclude this file/content
255
+ */
256
+ export function shouldExclude(filePath, content = null, config = null) {
257
+ const patterns = config || {
258
+ mode: MODE.BLACKLIST,
259
+ filePatterns: DEFAULT_FILE_PATTERNS,
260
+ contentPatterns: DEFAULT_CONTENT_PATTERNS,
261
+ enforceDefaults: true,
262
+ };
263
+
264
+ const mode = patterns.mode || MODE.BLACKLIST;
265
+ // Support both 'patterns' (shorthand) and 'filePatterns' (explicit)
266
+ const filePatterns = patterns.filePatterns || patterns.patterns || [];
267
+ const contentPatterns = patterns.contentPatterns || [];
268
+ const enforceDefaults = patterns.enforceDefaults !== false;
269
+
270
+ // In whitelist mode, only allow files that match patterns
271
+ if (mode === MODE.WHITELIST) {
272
+ // Always exclude sensitive files if enforceDefaults is true
273
+ if (enforceDefaults) {
274
+ for (const pattern of DEFAULT_FILE_PATTERNS) {
275
+ if (matchesFilePattern(filePath, pattern)) {
276
+ return true;
277
+ }
278
+ }
279
+ }
280
+
281
+ // Check if file matches any whitelist pattern
282
+ let allowed = false;
283
+ for (const pattern of filePatterns) {
284
+ if (matchesFilePattern(filePath, pattern)) {
285
+ allowed = true;
286
+ break;
287
+ }
288
+ }
289
+
290
+ return !allowed;
291
+ }
292
+
293
+ // In blacklist mode, exclude files that match patterns
294
+ // Check default patterns first
295
+ for (const pattern of DEFAULT_FILE_PATTERNS) {
296
+ if (matchesFilePattern(filePath, pattern)) {
297
+ return true;
298
+ }
299
+ }
300
+
301
+ // Check custom file patterns
302
+ for (const pattern of filePatterns) {
303
+ if (matchesFilePattern(filePath, pattern)) {
304
+ return true;
305
+ }
306
+ }
307
+
308
+ // Check content patterns if content is provided
309
+ if (content) {
310
+ // Check default content patterns
311
+ for (const pattern of DEFAULT_CONTENT_PATTERNS) {
312
+ if (matchesPattern(content, pattern, { isContent: true })) {
313
+ return true;
314
+ }
315
+ }
316
+
317
+ // Check custom content patterns
318
+ for (const pattern of contentPatterns) {
319
+ if (matchesPattern(content, pattern, { isContent: true })) {
320
+ return true;
321
+ }
322
+ }
323
+ }
324
+
325
+ return false;
326
+ }
@@ -0,0 +1,241 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import {
6
+ shouldExclude,
7
+ matchesPattern,
8
+ loadPatterns,
9
+ DEFAULT_FILE_PATTERNS,
10
+ DEFAULT_CONTENT_PATTERNS,
11
+ MODE,
12
+ } from './memory-exclusion.js';
13
+
14
+ describe('memory-exclusion', () => {
15
+ let testDir;
16
+
17
+ beforeEach(() => {
18
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tlc-memory-exclusion-test-'));
19
+ });
20
+
21
+ afterEach(() => {
22
+ fs.rmSync(testDir, { recursive: true, force: true });
23
+ });
24
+
25
+ describe('shouldExclude', () => {
26
+ it('returns true for .env files', () => {
27
+ expect(shouldExclude('.env')).toBe(true);
28
+ expect(shouldExclude('.env.local')).toBe(true);
29
+ expect(shouldExclude('.env.production')).toBe(true);
30
+ expect(shouldExclude('src/.env')).toBe(true);
31
+ });
32
+
33
+ it('returns true for matching content patterns', () => {
34
+ expect(shouldExclude('config.js', 'password=secret123')).toBe(true);
35
+ expect(shouldExclude('config.js', 'api_key=abc123')).toBe(true);
36
+ expect(shouldExclude('config.js', 'secret=mysecret')).toBe(true);
37
+ expect(shouldExclude('config.js', 'token=xyz789')).toBe(true);
38
+ expect(shouldExclude('config.js', 'private_key=-----BEGIN')).toBe(true);
39
+ });
40
+
41
+ it('returns false for safe content', () => {
42
+ expect(shouldExclude('src/index.js')).toBe(false);
43
+ expect(shouldExclude('src/utils.js', 'function add(a, b) { return a + b; }')).toBe(false);
44
+ expect(shouldExclude('README.md', '# My Project')).toBe(false);
45
+ expect(shouldExclude('package.json', '{"name": "my-app"}')).toBe(false);
46
+ });
47
+
48
+ it('returns true for .pem and .key files', () => {
49
+ expect(shouldExclude('server.pem')).toBe(true);
50
+ expect(shouldExclude('private.key')).toBe(true);
51
+ expect(shouldExclude('certs/server.pem')).toBe(true);
52
+ });
53
+
54
+ it('returns true for files with credentials or secrets in name', () => {
55
+ expect(shouldExclude('credentials.json')).toBe(true);
56
+ expect(shouldExclude('my-credentials.yaml')).toBe(true);
57
+ expect(shouldExclude('secrets.json')).toBe(true);
58
+ expect(shouldExclude('app-secrets.yaml')).toBe(true);
59
+ });
60
+ });
61
+
62
+ describe('whitelist mode', () => {
63
+ it('only allows listed patterns', () => {
64
+ const config = {
65
+ mode: MODE.WHITELIST,
66
+ patterns: ['*.js', '*.ts', 'package.json'],
67
+ };
68
+
69
+ expect(shouldExclude('src/index.js', null, config)).toBe(false);
70
+ expect(shouldExclude('src/app.ts', null, config)).toBe(false);
71
+ expect(shouldExclude('package.json', null, config)).toBe(false);
72
+ expect(shouldExclude('config.yaml', null, config)).toBe(true);
73
+ expect(shouldExclude('README.md', null, config)).toBe(true);
74
+ });
75
+
76
+ it('still excludes sensitive files even in whitelist mode', () => {
77
+ const config = {
78
+ mode: MODE.WHITELIST,
79
+ patterns: ['*'],
80
+ enforceDefaults: true,
81
+ };
82
+
83
+ expect(shouldExclude('.env', null, config)).toBe(true);
84
+ expect(shouldExclude('server.pem', null, config)).toBe(true);
85
+ });
86
+ });
87
+
88
+ describe('blacklist mode', () => {
89
+ it('excludes listed patterns', () => {
90
+ const config = {
91
+ mode: MODE.BLACKLIST,
92
+ patterns: ['*.log', '*.tmp', 'node_modules/**'],
93
+ };
94
+
95
+ expect(shouldExclude('debug.log', null, config)).toBe(true);
96
+ expect(shouldExclude('temp.tmp', null, config)).toBe(true);
97
+ expect(shouldExclude('node_modules/lodash/index.js', null, config)).toBe(true);
98
+ expect(shouldExclude('src/index.js', null, config)).toBe(false);
99
+ });
100
+
101
+ it('combines with default patterns in blacklist mode', () => {
102
+ const config = {
103
+ mode: MODE.BLACKLIST,
104
+ patterns: ['*.log'],
105
+ };
106
+
107
+ expect(shouldExclude('debug.log', null, config)).toBe(true);
108
+ expect(shouldExclude('.env', null, config)).toBe(true);
109
+ });
110
+ });
111
+
112
+ describe('loadPatterns', () => {
113
+ it('reads patterns from config file', () => {
114
+ const configPath = path.join(testDir, '.tlc.json');
115
+ const config = {
116
+ memoryExclusion: {
117
+ mode: 'blacklist',
118
+ filePatterns: ['*.log', '*.tmp'],
119
+ contentPatterns: ['secret=', 'password='],
120
+ },
121
+ };
122
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
123
+
124
+ const patterns = loadPatterns(testDir);
125
+
126
+ expect(patterns.mode).toBe(MODE.BLACKLIST);
127
+ expect(patterns.filePatterns).toContain('*.log');
128
+ expect(patterns.filePatterns).toContain('*.tmp');
129
+ expect(patterns.contentPatterns).toContain('secret=');
130
+ });
131
+
132
+ it('uses defaults when no config file exists', () => {
133
+ const patterns = loadPatterns(testDir);
134
+
135
+ expect(patterns.mode).toBe(MODE.BLACKLIST);
136
+ expect(patterns.filePatterns).toEqual(DEFAULT_FILE_PATTERNS);
137
+ expect(patterns.contentPatterns).toEqual(DEFAULT_CONTENT_PATTERNS);
138
+ });
139
+
140
+ it('uses defaults when config has no memoryExclusion section', () => {
141
+ const configPath = path.join(testDir, '.tlc.json');
142
+ fs.writeFileSync(configPath, JSON.stringify({ project: 'test' }, null, 2));
143
+
144
+ const patterns = loadPatterns(testDir);
145
+
146
+ expect(patterns.filePatterns).toEqual(DEFAULT_FILE_PATTERNS);
147
+ expect(patterns.contentPatterns).toEqual(DEFAULT_CONTENT_PATTERNS);
148
+ });
149
+
150
+ it('merges custom patterns with defaults', () => {
151
+ const configPath = path.join(testDir, '.tlc.json');
152
+ const config = {
153
+ memoryExclusion: {
154
+ filePatterns: ['*.custom'],
155
+ mergeDefaults: true,
156
+ },
157
+ };
158
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
159
+
160
+ const patterns = loadPatterns(testDir);
161
+
162
+ expect(patterns.filePatterns).toContain('*.custom');
163
+ expect(patterns.filePatterns).toContain('.env');
164
+ });
165
+ });
166
+
167
+ describe('matchesPattern', () => {
168
+ it('handles glob patterns', () => {
169
+ expect(matchesPattern('test.env', '*.env')).toBe(true);
170
+ expect(matchesPattern('config.js', '*.env')).toBe(false);
171
+ expect(matchesPattern('.env.local', '.env.*')).toBe(true);
172
+ });
173
+
174
+ it('handles double star glob patterns', () => {
175
+ expect(matchesPattern('src/config/.secrets/key.txt', '**/.secrets/*')).toBe(true);
176
+ expect(matchesPattern('.secrets/key.txt', '**/.secrets/*')).toBe(true);
177
+ expect(matchesPattern('src/index.js', '**/.secrets/*')).toBe(false);
178
+ });
179
+
180
+ it('handles regex patterns', () => {
181
+ expect(matchesPattern('password=123', /password=/)).toBe(true);
182
+ expect(matchesPattern('api_key=abc', /api_key=/)).toBe(true);
183
+ expect(matchesPattern('normal content', /password=/)).toBe(false);
184
+ });
185
+
186
+ it('handles regex patterns as strings with regex: prefix', () => {
187
+ expect(matchesPattern('password=123', 'regex:password=')).toBe(true);
188
+ expect(matchesPattern('api_key=abc', 'regex:api_key=')).toBe(true);
189
+ expect(matchesPattern('normal content', 'regex:password=')).toBe(false);
190
+ });
191
+
192
+ it('handles case-insensitive matching', () => {
193
+ expect(matchesPattern('PASSWORD=123', 'regex:password=', { ignoreCase: true })).toBe(true);
194
+ expect(matchesPattern('Api_Key=abc', 'regex:api_key=', { ignoreCase: true })).toBe(true);
195
+ });
196
+
197
+ it('handles exact match patterns', () => {
198
+ expect(matchesPattern('.env', '.env')).toBe(true);
199
+ expect(matchesPattern('src/.env', '.env')).toBe(false);
200
+ });
201
+
202
+ it('handles patterns with special characters', () => {
203
+ expect(matchesPattern('file.test.js', '*.test.js')).toBe(true);
204
+ expect(matchesPattern('credentials.json', '*credentials*')).toBe(true);
205
+ });
206
+ });
207
+
208
+ describe('DEFAULT_FILE_PATTERNS', () => {
209
+ it('includes .env patterns', () => {
210
+ expect(DEFAULT_FILE_PATTERNS).toContain('.env');
211
+ expect(DEFAULT_FILE_PATTERNS).toContain('.env.*');
212
+ });
213
+
214
+ it('includes certificate patterns', () => {
215
+ expect(DEFAULT_FILE_PATTERNS).toContain('*.pem');
216
+ expect(DEFAULT_FILE_PATTERNS).toContain('*.key');
217
+ });
218
+
219
+ it('includes credential patterns', () => {
220
+ expect(DEFAULT_FILE_PATTERNS).toContain('*credentials*');
221
+ expect(DEFAULT_FILE_PATTERNS).toContain('*secrets*');
222
+ });
223
+ });
224
+
225
+ describe('DEFAULT_CONTENT_PATTERNS', () => {
226
+ it('includes sensitive content patterns', () => {
227
+ expect(DEFAULT_CONTENT_PATTERNS).toContain('password=');
228
+ expect(DEFAULT_CONTENT_PATTERNS).toContain('api_key=');
229
+ expect(DEFAULT_CONTENT_PATTERNS).toContain('secret=');
230
+ expect(DEFAULT_CONTENT_PATTERNS).toContain('token=');
231
+ expect(DEFAULT_CONTENT_PATTERNS).toContain('private_key');
232
+ });
233
+ });
234
+
235
+ describe('MODE', () => {
236
+ it('exports mode constants', () => {
237
+ expect(MODE.WHITELIST).toBe('whitelist');
238
+ expect(MODE.BLACKLIST).toBe('blacklist');
239
+ });
240
+ });
241
+ });