tlc-claude-code 1.6.4 → 1.7.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.
Files changed (81) hide show
  1. package/dashboard/dist/components/ContainerSecurityPane.d.ts +45 -0
  2. package/dashboard/dist/components/ContainerSecurityPane.js +44 -0
  3. package/dashboard/dist/components/ContainerSecurityPane.test.d.ts +1 -0
  4. package/dashboard/dist/components/ContainerSecurityPane.test.js +153 -0
  5. package/package.json +1 -1
  6. package/server/lib/access-control.test.js +1 -1
  7. package/server/lib/agents-cancel-command.test.js +1 -1
  8. package/server/lib/agents-get-command.test.js +1 -1
  9. package/server/lib/agents-list-command.test.js +1 -1
  10. package/server/lib/agents-logs-command.test.js +1 -1
  11. package/server/lib/agents-retry-command.test.js +1 -1
  12. package/server/lib/budget-limits.test.js +2 -2
  13. package/server/lib/code-gate/bypass-logger.js +129 -0
  14. package/server/lib/code-gate/bypass-logger.test.js +142 -0
  15. package/server/lib/code-gate/gate-command.js +114 -0
  16. package/server/lib/code-gate/gate-command.test.js +111 -0
  17. package/server/lib/code-gate/gate-config.js +163 -0
  18. package/server/lib/code-gate/gate-config.test.js +181 -0
  19. package/server/lib/code-gate/gate-engine.js +193 -0
  20. package/server/lib/code-gate/gate-engine.test.js +258 -0
  21. package/server/lib/code-gate/gate-reporter.js +123 -0
  22. package/server/lib/code-gate/gate-reporter.test.js +159 -0
  23. package/server/lib/code-gate/hooks-generator.js +149 -0
  24. package/server/lib/code-gate/hooks-generator.test.js +142 -0
  25. package/server/lib/code-gate/llm-reviewer.js +176 -0
  26. package/server/lib/code-gate/llm-reviewer.test.js +161 -0
  27. package/server/lib/code-gate/push-gate.js +133 -0
  28. package/server/lib/code-gate/push-gate.test.js +190 -0
  29. package/server/lib/code-gate/rules/architecture-rules.js +228 -0
  30. package/server/lib/code-gate/rules/architecture-rules.test.js +155 -0
  31. package/server/lib/code-gate/rules/client-rules.js +120 -0
  32. package/server/lib/code-gate/rules/client-rules.test.js +121 -0
  33. package/server/lib/code-gate/rules/config-rules.js +140 -0
  34. package/server/lib/code-gate/rules/config-rules.test.js +103 -0
  35. package/server/lib/code-gate/rules/database-rules.js +158 -0
  36. package/server/lib/code-gate/rules/database-rules.test.js +119 -0
  37. package/server/lib/code-gate/rules/docker-rules.js +201 -0
  38. package/server/lib/code-gate/rules/docker-rules.test.js +104 -0
  39. package/server/lib/code-gate/rules/quality-rules.js +304 -0
  40. package/server/lib/code-gate/rules/quality-rules.test.js +199 -0
  41. package/server/lib/code-gate/rules/security-rules.js +228 -0
  42. package/server/lib/code-gate/rules/security-rules.test.js +131 -0
  43. package/server/lib/code-gate/rules/structure-rules.js +155 -0
  44. package/server/lib/code-gate/rules/structure-rules.test.js +107 -0
  45. package/server/lib/code-gate/rules/test-rules.js +93 -0
  46. package/server/lib/code-gate/rules/test-rules.test.js +97 -0
  47. package/server/lib/code-gate/typescript-gate.js +128 -0
  48. package/server/lib/code-gate/typescript-gate.test.js +131 -0
  49. package/server/lib/code-generator.test.js +1 -1
  50. package/server/lib/cost-command.test.js +1 -1
  51. package/server/lib/cost-optimizer.test.js +1 -1
  52. package/server/lib/cost-projections.test.js +1 -1
  53. package/server/lib/cost-reports.test.js +1 -1
  54. package/server/lib/cost-tracker.test.js +1 -1
  55. package/server/lib/crypto-patterns.test.js +1 -1
  56. package/server/lib/design-command.test.js +1 -1
  57. package/server/lib/design-parser.test.js +1 -1
  58. package/server/lib/gemini-vision.test.js +1 -1
  59. package/server/lib/input-validator.test.js +1 -1
  60. package/server/lib/litellm-client.test.js +1 -1
  61. package/server/lib/litellm-command.test.js +1 -1
  62. package/server/lib/litellm-config.test.js +1 -1
  63. package/server/lib/model-pricing.test.js +1 -1
  64. package/server/lib/models-command.test.js +1 -1
  65. package/server/lib/optimize-command.test.js +1 -1
  66. package/server/lib/orchestration-integration.test.js +1 -1
  67. package/server/lib/output-encoder.test.js +1 -1
  68. package/server/lib/quality-evaluator.test.js +1 -1
  69. package/server/lib/quality-gate-command.test.js +1 -1
  70. package/server/lib/quality-gate-scorer.test.js +1 -1
  71. package/server/lib/quality-history.test.js +1 -1
  72. package/server/lib/quality-presets.test.js +1 -1
  73. package/server/lib/quality-retry.test.js +1 -1
  74. package/server/lib/quality-thresholds.test.js +1 -1
  75. package/server/lib/secure-auth.test.js +1 -1
  76. package/server/lib/secure-code-command.test.js +1 -1
  77. package/server/lib/secure-errors.test.js +1 -1
  78. package/server/lib/security/auth-security.test.js +4 -3
  79. package/server/lib/vision-command.test.js +1 -1
  80. package/server/lib/visual-command.test.js +1 -1
  81. package/server/lib/visual-testing.test.js +1 -1
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Architecture Rules Tests
3
+ *
4
+ * Detects single-writer violations, fake API calls,
5
+ * stale re-exports, and raw API bypass patterns.
6
+ */
7
+ import { describe, it, expect } from 'vitest';
8
+
9
+ const {
10
+ checkSingleWriter,
11
+ checkFakeApiCalls,
12
+ checkStaleReexports,
13
+ checkRawApiRequests,
14
+ } = require('./architecture-rules.js');
15
+
16
+ describe('Architecture Rules', () => {
17
+ describe('checkSingleWriter', () => {
18
+ it('detects db.insert(users) outside users.service', () => {
19
+ const findings = checkSingleWriter(
20
+ 'src/leads/leads.service.ts',
21
+ 'const result = await db.insert(users).values(data);'
22
+ );
23
+ expect(findings).toHaveLength(1);
24
+ expect(findings[0].severity).toBe('block');
25
+ expect(findings[0].rule).toBe('single-writer');
26
+ expect(findings[0].message).toContain('users');
27
+ });
28
+
29
+ it('passes when inside correct service file', () => {
30
+ const findings = checkSingleWriter(
31
+ 'src/users/users.service.ts',
32
+ 'const result = await db.insert(users).values(data);'
33
+ );
34
+ expect(findings).toHaveLength(0);
35
+ });
36
+
37
+ it('detects db.update(companies) outside company.service', () => {
38
+ const findings = checkSingleWriter(
39
+ 'src/api/controller.ts',
40
+ 'await db.update(companies).set({ status: "active" });'
41
+ );
42
+ expect(findings).toHaveLength(1);
43
+ expect(findings[0].message).toContain('companies');
44
+ });
45
+
46
+ it('handles singular service name for plural table', () => {
47
+ // company.service.ts should be allowed to write to companies table
48
+ const findings = checkSingleWriter(
49
+ 'src/company/company.service.ts',
50
+ 'await db.insert(companies).values(data);'
51
+ );
52
+ expect(findings).toHaveLength(0);
53
+ });
54
+
55
+ it('skips test files', () => {
56
+ const findings = checkSingleWriter(
57
+ 'src/api/controller.test.ts',
58
+ 'await db.insert(users).values(data);'
59
+ );
60
+ expect(findings).toHaveLength(0);
61
+ });
62
+ });
63
+
64
+ describe('checkFakeApiCalls', () => {
65
+ it('detects setTimeout + resolve mock pattern', () => {
66
+ const code = `
67
+ function getUsers() {
68
+ return new Promise((resolve) => {
69
+ setTimeout(() => resolve([{ id: 1, name: 'Test' }]), 500);
70
+ });
71
+ }
72
+ `;
73
+ const findings = checkFakeApiCalls('src/api/users.ts', code);
74
+ expect(findings).toHaveLength(1);
75
+ expect(findings[0].severity).toBe('block');
76
+ expect(findings[0].rule).toBe('no-fake-api');
77
+ });
78
+
79
+ it('allows real setTimeout with function callback', () => {
80
+ const code = `
81
+ setTimeout(() => {
82
+ refreshDashboard();
83
+ }, 1000);
84
+ `;
85
+ const findings = checkFakeApiCalls('src/ui/dashboard.ts', code);
86
+ expect(findings).toHaveLength(0);
87
+ });
88
+
89
+ it('skips test files', () => {
90
+ const code = 'setTimeout(() => resolve(mockData), 500);';
91
+ const findings = checkFakeApiCalls('src/api/users.test.ts', code);
92
+ expect(findings).toHaveLength(0);
93
+ });
94
+ });
95
+
96
+ describe('checkStaleReexports', () => {
97
+ it('detects file with only module.exports = require(...)', () => {
98
+ const code = "module.exports = require('./new-location');";
99
+ const findings = checkStaleReexports('src/old-module.js', code);
100
+ expect(findings).toHaveLength(1);
101
+ expect(findings[0].severity).toBe('warn');
102
+ expect(findings[0].rule).toBe('stale-reexport');
103
+ });
104
+
105
+ it('passes file with real logic alongside export', () => {
106
+ const code = `
107
+ const helper = require('./utils');
108
+ function doWork() { return helper.process(); }
109
+ module.exports = { doWork };
110
+ `;
111
+ const findings = checkStaleReexports('src/module.js', code);
112
+ expect(findings).toHaveLength(0);
113
+ });
114
+
115
+ it('detects export default re-export', () => {
116
+ const code = "export { default } from './new-location';";
117
+ const findings = checkStaleReexports('src/old-module.ts', code);
118
+ expect(findings).toHaveLength(1);
119
+ });
120
+ });
121
+
122
+ describe('checkRawApiRequests', () => {
123
+ it('detects apiRequest("POST", "/api/...")', () => {
124
+ const code = 'const result = await apiRequest("POST", "/api/companies", data);';
125
+ const findings = checkRawApiRequests('src/components/form.tsx', code);
126
+ expect(findings).toHaveLength(1);
127
+ expect(findings[0].severity).toBe('warn');
128
+ expect(findings[0].rule).toBe('no-raw-api');
129
+ });
130
+
131
+ it('detects raw fetch("/api/...")', () => {
132
+ const code = 'const res = await fetch("/api/leads", { method: "POST" });';
133
+ const findings = checkRawApiRequests('src/components/leads.tsx', code);
134
+ expect(findings).toHaveLength(1);
135
+ });
136
+
137
+ it('allows non-API fetch calls', () => {
138
+ const code = 'const res = await fetch("https://cdn.example.com/data.json");';
139
+ const findings = checkRawApiRequests('src/utils/loader.ts', code);
140
+ expect(findings).toHaveLength(0);
141
+ });
142
+
143
+ it('allows fetch in API helper files', () => {
144
+ const code = 'const res = await fetch("/api/companies");';
145
+ const findings = checkRawApiRequests('src/lib/api.ts', code);
146
+ expect(findings).toHaveLength(0);
147
+ });
148
+
149
+ it('skips test files', () => {
150
+ const code = 'await apiRequest("POST", "/api/test", data);';
151
+ const findings = checkRawApiRequests('src/components/form.test.tsx', code);
152
+ expect(findings).toHaveLength(0);
153
+ });
154
+ });
155
+ });
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Client-Side Pattern Rules
3
+ *
4
+ * Detects Zustand stores without persistence middleware
5
+ * and Zod schemas using z.date() instead of z.coerce.date().
6
+ *
7
+ * Derived from Bug #13 (state lost on refresh) and Bug #12 (date coercion).
8
+ *
9
+ * @module code-gate/rules/client-rules
10
+ */
11
+
12
+ /**
13
+ * @param {string} filePath
14
+ * @returns {boolean}
15
+ */
16
+ function isTestFile(filePath) {
17
+ return /\.(test|spec)\.[jt]sx?$/.test(filePath) || filePath.includes('__tests__');
18
+ }
19
+
20
+ /** Store names that are intentionally ephemeral (UI state, not data) */
21
+ const EPHEMERAL_PATTERNS = [
22
+ 'ui-store', 'ui.store', 'uiStore',
23
+ 'modal-store', 'modalStore',
24
+ 'toast-store', 'toastStore',
25
+ 'theme-store', 'themeStore',
26
+ ];
27
+
28
+ /**
29
+ * Detect Zustand create() without persist middleware.
30
+ * Stores that hold user data should use persist() to survive page refreshes.
31
+ * UI-only stores (modals, toasts) are exempt.
32
+ *
33
+ * @param {string} filePath
34
+ * @param {string} content
35
+ * @returns {Array<{severity: string, rule: string, line: number, message: string, fix: string}>}
36
+ */
37
+ function checkZustandPersistence(filePath, content) {
38
+ if (isTestFile(filePath)) return [];
39
+
40
+ // Only check store files
41
+ if (!filePath.includes('store')) return [];
42
+
43
+ // Skip ephemeral stores (UI state)
44
+ const fileBase = filePath.toLowerCase();
45
+ if (EPHEMERAL_PATTERNS.some(p => fileBase.includes(p))) return [];
46
+
47
+ // Check if file imports/uses zustand create
48
+ const hasZustandCreate = /\bcreate\s*[<(]/.test(content) &&
49
+ (content.includes("from 'zustand'") || content.includes('from "zustand"') ||
50
+ content.includes("require('zustand')") || content.includes('require("zustand")'));
51
+
52
+ if (!hasZustandCreate) return [];
53
+
54
+ // Check if persist is used
55
+ const hasPersist = content.includes('persist(') || content.includes('persist<');
56
+
57
+ if (!hasPersist) {
58
+ const findings = [];
59
+ const lines = content.split('\n');
60
+ for (let i = 0; i < lines.length; i++) {
61
+ if (/\bcreate\s*[<(]/.test(lines[i])) {
62
+ findings.push({
63
+ severity: 'warn',
64
+ rule: 'zustand-needs-persist',
65
+ line: i + 1,
66
+ message: 'Zustand store without persist middleware — state lost on page refresh',
67
+ fix: "Wrap with persist(): create(persist((set) => ({...}), { name: 'store-name' }))",
68
+ });
69
+ break;
70
+ }
71
+ }
72
+ return findings;
73
+ }
74
+
75
+ return [];
76
+ }
77
+
78
+ /**
79
+ * Detect z.date() in schema files that should use z.coerce.date().
80
+ * When API clients send ISO strings, z.date() rejects them.
81
+ * z.coerce.date() handles both Date objects and ISO strings.
82
+ *
83
+ * @param {string} filePath
84
+ * @param {string} content
85
+ * @returns {Array}
86
+ */
87
+ function checkZodDateCoercion(filePath, content) {
88
+ if (isTestFile(filePath)) return [];
89
+
90
+ // Only check schema-related files
91
+ const fileBase = filePath.toLowerCase();
92
+ if (!fileBase.includes('schema') && !fileBase.includes('schemas')) return [];
93
+
94
+ const findings = [];
95
+ const lines = content.split('\n');
96
+
97
+ for (let i = 0; i < lines.length; i++) {
98
+ const line = lines[i];
99
+ const trimmed = line.trim();
100
+ if (trimmed.startsWith('//') || trimmed.startsWith('*')) continue;
101
+
102
+ // z.date() without coerce
103
+ if (/z\.date\s*\(/.test(line) && !line.includes('z.coerce.date')) {
104
+ findings.push({
105
+ severity: 'warn',
106
+ rule: 'zod-use-coerce-date',
107
+ line: i + 1,
108
+ message: 'z.date() rejects ISO strings from API clients — use z.coerce.date()',
109
+ fix: 'Replace z.date() with z.coerce.date() to accept both Date objects and ISO strings',
110
+ });
111
+ }
112
+ }
113
+
114
+ return findings;
115
+ }
116
+
117
+ module.exports = {
118
+ checkZustandPersistence,
119
+ checkZodDateCoercion,
120
+ };
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Client-Side Pattern Rules Tests
3
+ *
4
+ * Detects Zustand stores without persistence and
5
+ * Zod schemas with z.date() instead of z.coerce.date().
6
+ */
7
+ import { describe, it, expect } from 'vitest';
8
+
9
+ const {
10
+ checkZustandPersistence,
11
+ checkZodDateCoercion,
12
+ } = require('./client-rules.js');
13
+
14
+ describe('Client Rules', () => {
15
+ describe('checkZustandPersistence', () => {
16
+ it('detects create() without persist', () => {
17
+ const code = `
18
+ import { create } from 'zustand';
19
+ const useFormStore = create((set) => ({
20
+ data: null,
21
+ setData: (data) => set({ data }),
22
+ }));
23
+ `;
24
+ const findings = checkZustandPersistence('src/stores/form-store.ts', code);
25
+ expect(findings).toHaveLength(1);
26
+ expect(findings[0].severity).toBe('warn');
27
+ expect(findings[0].rule).toBe('zustand-needs-persist');
28
+ });
29
+
30
+ it('passes create(persist(...))', () => {
31
+ const code = `
32
+ import { create } from 'zustand';
33
+ import { persist } from 'zustand/middleware';
34
+ const useFormStore = create(persist((set) => ({
35
+ data: null,
36
+ }), { name: 'form-store' }));
37
+ `;
38
+ const findings = checkZustandPersistence('src/stores/form-store.ts', code);
39
+ expect(findings).toHaveLength(0);
40
+ });
41
+
42
+ it('allows stores in ephemeral files', () => {
43
+ const code = `
44
+ import { create } from 'zustand';
45
+ const useUIStore = create((set) => ({ open: false }));
46
+ `;
47
+ const findings = checkZustandPersistence('src/stores/ui-store.ts', code);
48
+ expect(findings).toHaveLength(0);
49
+ });
50
+
51
+ it('skips test files', () => {
52
+ const code = `
53
+ import { create } from 'zustand';
54
+ const useTestStore = create((set) => ({ x: 1 }));
55
+ `;
56
+ const findings = checkZustandPersistence('src/stores/form.test.ts', code);
57
+ expect(findings).toHaveLength(0);
58
+ });
59
+
60
+ it('handles create<Type>() generic syntax', () => {
61
+ const code = `
62
+ import { create } from 'zustand';
63
+ interface FormState { data: string | null; }
64
+ const useFormStore = create<FormState>((set) => ({
65
+ data: null,
66
+ }));
67
+ `;
68
+ const findings = checkZustandPersistence('src/stores/form-store.ts', code);
69
+ expect(findings).toHaveLength(1);
70
+ });
71
+ });
72
+
73
+ describe('checkZodDateCoercion', () => {
74
+ it('detects z.date() in schema files', () => {
75
+ const code = `
76
+ const insertLeadSchema = z.object({
77
+ name: z.string(),
78
+ createdAt: z.date(),
79
+ });
80
+ `;
81
+ const findings = checkZodDateCoercion('src/db/schema/leads.ts', code);
82
+ expect(findings).toHaveLength(1);
83
+ expect(findings[0].severity).toBe('warn');
84
+ expect(findings[0].rule).toBe('zod-use-coerce-date');
85
+ });
86
+
87
+ it('passes z.coerce.date()', () => {
88
+ const code = `
89
+ const insertLeadSchema = z.object({
90
+ name: z.string(),
91
+ createdAt: z.coerce.date(),
92
+ });
93
+ `;
94
+ const findings = checkZodDateCoercion('src/db/schema/leads.ts', code);
95
+ expect(findings).toHaveLength(0);
96
+ });
97
+
98
+ it('allows z.date() in non-schema files', () => {
99
+ const code = 'const validator = z.date();';
100
+ const findings = checkZodDateCoercion('src/utils/helpers.ts', code);
101
+ expect(findings).toHaveLength(0);
102
+ });
103
+
104
+ it('skips test files', () => {
105
+ const code = 'const schema = z.object({ at: z.date() });';
106
+ const findings = checkZodDateCoercion('src/db/schema/leads.test.ts', code);
107
+ expect(findings).toHaveLength(0);
108
+ });
109
+
110
+ it('detects in insertSchema definitions', () => {
111
+ const code = `
112
+ export const insertQuoteSchema = z.object({
113
+ validUntil: z.date(),
114
+ amount: z.number(),
115
+ });
116
+ `;
117
+ const findings = checkZodDateCoercion('server/schemas/quotes.ts', code);
118
+ expect(findings).toHaveLength(1);
119
+ });
120
+ });
121
+ });
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Config Rules
3
+ *
4
+ * Detects magic numbers (hardcoded timeouts, durations, thresholds)
5
+ * and hardcoded role strings that should live in config or database.
6
+ *
7
+ * Derived from Bug #18 (hardcoded role mappings) and Bug #23 (magic numbers).
8
+ *
9
+ * @module code-gate/rules/config-rules
10
+ */
11
+
12
+ /**
13
+ * @param {string} filePath
14
+ * @returns {boolean}
15
+ */
16
+ function isTestFile(filePath) {
17
+ return /\.(test|spec)\.[jt]sx?$/.test(filePath) || filePath.includes('__tests__');
18
+ }
19
+
20
+ /**
21
+ * @param {string} filePath
22
+ * @returns {boolean}
23
+ */
24
+ function isConfigFile(filePath) {
25
+ const base = filePath.toLowerCase();
26
+ return base.includes('config') || base.includes('constants') ||
27
+ base.includes('.env') || base.endsWith('.json') ||
28
+ base.endsWith('.yml') || base.endsWith('.yaml');
29
+ }
30
+
31
+ /** Numbers that are safe to hardcode (HTTP status codes, common values) */
32
+ const SAFE_NUMBERS = new Set([
33
+ 0, 1, 2, 3, 4, 5, 10, 24, 60, 100,
34
+ 200, 201, 204, 301, 302, 304, 400, 401, 403, 404, 409, 422, 429, 500, 502, 503,
35
+ 1000, // 1 second in ms
36
+ 1024, 2048, 4096, // byte sizes
37
+ ]);
38
+
39
+ /** Threshold above which a number is suspicious (1 minute in ms) */
40
+ const MAGIC_THRESHOLD = 60000;
41
+
42
+ /**
43
+ * Detect large hardcoded numbers that look like timeouts or durations.
44
+ * Numbers >= 60000 in assignments (not in const UPPER_CASE declarations)
45
+ * are flagged as likely magic numbers.
46
+ *
47
+ * @param {string} filePath
48
+ * @param {string} content
49
+ * @returns {Array<{severity: string, rule: string, line: number, message: string, fix: string}>}
50
+ */
51
+ function checkMagicNumbers(filePath, content) {
52
+ if (isTestFile(filePath)) return [];
53
+ if (isConfigFile(filePath)) return [];
54
+ const findings = [];
55
+ const lines = content.split('\n');
56
+
57
+ for (let i = 0; i < lines.length; i++) {
58
+ const line = lines[i];
59
+ const trimmed = line.trim();
60
+ if (trimmed.startsWith('//') || trimmed.startsWith('*')) continue;
61
+
62
+ // Find number literals >= MAGIC_THRESHOLD
63
+ const numberMatches = line.matchAll(/\b(\d{5,})\b/g);
64
+ for (const match of numberMatches) {
65
+ const num = parseInt(match[1], 10);
66
+ if (num < MAGIC_THRESHOLD || SAFE_NUMBERS.has(num)) continue;
67
+
68
+ // Allow if it's a UPPER_CASE constant declaration
69
+ if (/\b(?:const|let|var)\s+[A-Z][A-Z0-9_]+\s*=/.test(line)) continue;
70
+
71
+ findings.push({
72
+ severity: 'warn',
73
+ rule: 'no-magic-numbers',
74
+ line: i + 1,
75
+ message: `Magic number ${num} — use a named constant or config value`,
76
+ fix: 'Extract to a named constant (e.g. const SESSION_TIMEOUT = ...) or use config',
77
+ });
78
+ break; // One finding per line
79
+ }
80
+ }
81
+
82
+ return findings;
83
+ }
84
+
85
+ /** Role-related keywords to detect in comparisons and object literals */
86
+ const ROLE_STRINGS = [
87
+ 'admin', 'manager', 'editor', 'viewer', 'user', 'moderator',
88
+ 'owner', 'member', 'guest', 'superadmin', 'super_admin',
89
+ ];
90
+
91
+ /**
92
+ * Detect hardcoded role strings in comparisons and object literals.
93
+ * Roles should come from constants, RBAC tables, or config — not inline strings.
94
+ *
95
+ * @param {string} filePath
96
+ * @param {string} content
97
+ * @returns {Array}
98
+ */
99
+ function checkHardcodedRoles(filePath, content) {
100
+ if (isTestFile(filePath)) return [];
101
+
102
+ // Skip role definition files
103
+ const base = filePath.toLowerCase();
104
+ if (base.includes('role') || base.includes('permission') ||
105
+ base.includes('rbac') || isConfigFile(filePath)) {
106
+ return [];
107
+ }
108
+
109
+ const findings = [];
110
+ const lines = content.split('\n');
111
+ const rolePattern = new RegExp(
112
+ `(?:role|userRole|workspaceRole)\\s*(?:===?|!==?)\\s*['"\`](${ROLE_STRINGS.join('|')})['"\`]`
113
+ );
114
+ const roleLiteralPattern = new RegExp(
115
+ `\\brole\\s*:\\s*['"\`](${ROLE_STRINGS.join('|')})['"\`]`
116
+ );
117
+
118
+ for (let i = 0; i < lines.length; i++) {
119
+ const line = lines[i];
120
+ const trimmed = line.trim();
121
+ if (trimmed.startsWith('//') || trimmed.startsWith('*')) continue;
122
+
123
+ if (rolePattern.test(line) || roleLiteralPattern.test(line)) {
124
+ findings.push({
125
+ severity: 'warn',
126
+ rule: 'no-hardcoded-roles',
127
+ line: i + 1,
128
+ message: 'Hardcoded role string — use RBAC constants or database roles',
129
+ fix: 'Import role constants (e.g. ROLES.ADMIN) instead of string literals',
130
+ });
131
+ }
132
+ }
133
+
134
+ return findings;
135
+ }
136
+
137
+ module.exports = {
138
+ checkMagicNumbers,
139
+ checkHardcodedRoles,
140
+ };
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Config Rules Tests
3
+ *
4
+ * Detects magic numbers (hardcoded timeouts/thresholds)
5
+ * and hardcoded role strings that should be in config/DB.
6
+ */
7
+ import { describe, it, expect } from 'vitest';
8
+
9
+ const {
10
+ checkMagicNumbers,
11
+ checkHardcodedRoles,
12
+ } = require('./config-rules.js');
13
+
14
+ describe('Config Rules', () => {
15
+ describe('checkMagicNumbers', () => {
16
+ it('detects hardcoded 86400000 (24h in ms)', () => {
17
+ const code = 'const sessionTimeout = 86400000;';
18
+ const findings = checkMagicNumbers('src/auth/session.ts', code);
19
+ expect(findings).toHaveLength(1);
20
+ expect(findings[0].severity).toBe('warn');
21
+ expect(findings[0].rule).toBe('no-magic-numbers');
22
+ });
23
+
24
+ it('detects hardcoded 3600000 (1h in ms)', () => {
25
+ const code = 'const resetExpiry = 3600000;';
26
+ const findings = checkMagicNumbers('src/auth/reset.ts', code);
27
+ expect(findings).toHaveLength(1);
28
+ });
29
+
30
+ it('allows named constants (const TIMEOUT = 86400000)', () => {
31
+ // Already a named constant at declaration — this is the pattern we want
32
+ const code = 'const SESSION_TIMEOUT = 86400000;';
33
+ const findings = checkMagicNumbers('src/config/constants.ts', code);
34
+ expect(findings).toHaveLength(0);
35
+ });
36
+
37
+ it('allows common safe numbers', () => {
38
+ const code = [
39
+ 'const count = 0;',
40
+ 'const index = 1;',
41
+ 'const percent = 100;',
42
+ 'return res.status(200).json(data);',
43
+ 'return res.status(404).json({ error: "Not found" });',
44
+ 'return res.status(500).json({ error: "Server error" });',
45
+ ].join('\n');
46
+ const findings = checkMagicNumbers('src/api/handler.ts', code);
47
+ expect(findings).toHaveLength(0);
48
+ });
49
+
50
+ it('skips test files', () => {
51
+ const code = 'const timeout = 86400000;';
52
+ const findings = checkMagicNumbers('src/auth/session.test.ts', code);
53
+ expect(findings).toHaveLength(0);
54
+ });
55
+
56
+ it('skips config and constants files', () => {
57
+ const code = 'const timeout = 86400000;';
58
+ const findings = checkMagicNumbers('src/config/timeouts.ts', code);
59
+ expect(findings).toHaveLength(0);
60
+ });
61
+ });
62
+
63
+ describe('checkHardcodedRoles', () => {
64
+ it('detects role === "admin" comparison', () => {
65
+ const code = 'if (user.role === "admin") { allowAccess(); }';
66
+ const findings = checkHardcodedRoles('src/middleware/auth.ts', code);
67
+ expect(findings).toHaveLength(1);
68
+ expect(findings[0].severity).toBe('warn');
69
+ expect(findings[0].rule).toBe('no-hardcoded-roles');
70
+ });
71
+
72
+ it('detects role: "manager" in object literal', () => {
73
+ const code = 'const defaultUser = { name: "test", role: "manager" };';
74
+ const findings = checkHardcodedRoles('src/api/users.ts', code);
75
+ expect(findings).toHaveLength(1);
76
+ });
77
+
78
+ it('allows role variable references without string', () => {
79
+ const code = 'if (user.role === ROLES.ADMIN) { allowAccess(); }';
80
+ const findings = checkHardcodedRoles('src/middleware/auth.ts', code);
81
+ expect(findings).toHaveLength(0);
82
+ });
83
+
84
+ it('allows RBAC constant definitions', () => {
85
+ // This is WHERE roles are defined — that's fine
86
+ const code = 'const ROLES = { ADMIN: "admin", USER: "user" };';
87
+ const findings = checkHardcodedRoles('src/config/roles.ts', code);
88
+ expect(findings).toHaveLength(0);
89
+ });
90
+
91
+ it('skips test files', () => {
92
+ const code = 'if (user.role === "admin") {}';
93
+ const findings = checkHardcodedRoles('src/middleware/auth.test.ts', code);
94
+ expect(findings).toHaveLength(0);
95
+ });
96
+
97
+ it('skips role definition files', () => {
98
+ const code = 'role: "admin"';
99
+ const findings = checkHardcodedRoles('src/permissions/roles.ts', code);
100
+ expect(findings).toHaveLength(0);
101
+ });
102
+ });
103
+ });