tlc-claude-code 1.4.8 → 1.4.9

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 (169) hide show
  1. package/package.json +1 -1
  2. package/server/index.js +229 -14
  3. package/server/lib/compliance/control-mapper.js +401 -0
  4. package/server/lib/compliance/control-mapper.test.js +117 -0
  5. package/server/lib/compliance/evidence-linker.js +296 -0
  6. package/server/lib/compliance/evidence-linker.test.js +121 -0
  7. package/server/lib/compliance/gdpr-checklist.js +416 -0
  8. package/server/lib/compliance/gdpr-checklist.test.js +131 -0
  9. package/server/lib/compliance/hipaa-checklist.js +277 -0
  10. package/server/lib/compliance/hipaa-checklist.test.js +101 -0
  11. package/server/lib/compliance/iso27001-checklist.js +287 -0
  12. package/server/lib/compliance/iso27001-checklist.test.js +99 -0
  13. package/server/lib/compliance/multi-framework-reporter.js +284 -0
  14. package/server/lib/compliance/multi-framework-reporter.test.js +127 -0
  15. package/server/lib/compliance/pci-dss-checklist.js +214 -0
  16. package/server/lib/compliance/pci-dss-checklist.test.js +95 -0
  17. package/server/lib/compliance/trust-centre.js +187 -0
  18. package/server/lib/compliance/trust-centre.test.js +93 -0
  19. package/server/lib/dashboard/api-server.js +155 -0
  20. package/server/lib/dashboard/api-server.test.js +155 -0
  21. package/server/lib/dashboard/health-api.js +199 -0
  22. package/server/lib/dashboard/health-api.test.js +122 -0
  23. package/server/lib/dashboard/notes-api.js +234 -0
  24. package/server/lib/dashboard/notes-api.test.js +134 -0
  25. package/server/lib/dashboard/router-api.js +176 -0
  26. package/server/lib/dashboard/router-api.test.js +132 -0
  27. package/server/lib/dashboard/tasks-api.js +289 -0
  28. package/server/lib/dashboard/tasks-api.test.js +161 -0
  29. package/server/lib/dashboard/tlc-introspection.js +197 -0
  30. package/server/lib/dashboard/tlc-introspection.test.js +138 -0
  31. package/server/lib/dashboard/version-api.js +222 -0
  32. package/server/lib/dashboard/version-api.test.js +112 -0
  33. package/server/lib/dashboard/websocket-server.js +104 -0
  34. package/server/lib/dashboard/websocket-server.test.js +118 -0
  35. package/server/lib/deploy/branch-classifier.js +163 -0
  36. package/server/lib/deploy/branch-classifier.test.js +164 -0
  37. package/server/lib/deploy/deployment-approval.js +299 -0
  38. package/server/lib/deploy/deployment-approval.test.js +296 -0
  39. package/server/lib/deploy/deployment-audit.js +374 -0
  40. package/server/lib/deploy/deployment-audit.test.js +307 -0
  41. package/server/lib/deploy/deployment-executor.js +335 -0
  42. package/server/lib/deploy/deployment-executor.test.js +329 -0
  43. package/server/lib/deploy/deployment-rules.js +163 -0
  44. package/server/lib/deploy/deployment-rules.test.js +188 -0
  45. package/server/lib/deploy/rollback-manager.js +379 -0
  46. package/server/lib/deploy/rollback-manager.test.js +321 -0
  47. package/server/lib/deploy/security-gates.js +236 -0
  48. package/server/lib/deploy/security-gates.test.js +222 -0
  49. package/server/lib/k8s/gitops-config.js +188 -0
  50. package/server/lib/k8s/gitops-config.test.js +59 -0
  51. package/server/lib/k8s/helm-generator.js +196 -0
  52. package/server/lib/k8s/helm-generator.test.js +59 -0
  53. package/server/lib/k8s/kustomize-generator.js +176 -0
  54. package/server/lib/k8s/kustomize-generator.test.js +58 -0
  55. package/server/lib/k8s/network-policy.js +114 -0
  56. package/server/lib/k8s/network-policy.test.js +53 -0
  57. package/server/lib/k8s/pod-security.js +114 -0
  58. package/server/lib/k8s/pod-security.test.js +55 -0
  59. package/server/lib/k8s/rbac-generator.js +132 -0
  60. package/server/lib/k8s/rbac-generator.test.js +57 -0
  61. package/server/lib/k8s/resource-manager.js +172 -0
  62. package/server/lib/k8s/resource-manager.test.js +60 -0
  63. package/server/lib/k8s/secrets-encryption.js +168 -0
  64. package/server/lib/k8s/secrets-encryption.test.js +49 -0
  65. package/server/lib/monitoring/alert-manager.js +238 -0
  66. package/server/lib/monitoring/alert-manager.test.js +106 -0
  67. package/server/lib/monitoring/health-check.js +226 -0
  68. package/server/lib/monitoring/health-check.test.js +176 -0
  69. package/server/lib/monitoring/incident-manager.js +230 -0
  70. package/server/lib/monitoring/incident-manager.test.js +98 -0
  71. package/server/lib/monitoring/log-aggregator.js +147 -0
  72. package/server/lib/monitoring/log-aggregator.test.js +89 -0
  73. package/server/lib/monitoring/metrics-collector.js +337 -0
  74. package/server/lib/monitoring/metrics-collector.test.js +172 -0
  75. package/server/lib/monitoring/status-page.js +214 -0
  76. package/server/lib/monitoring/status-page.test.js +105 -0
  77. package/server/lib/monitoring/uptime-monitor.js +194 -0
  78. package/server/lib/monitoring/uptime-monitor.test.js +109 -0
  79. package/server/lib/network/fail2ban-config.js +294 -0
  80. package/server/lib/network/fail2ban-config.test.js +275 -0
  81. package/server/lib/network/firewall-manager.js +252 -0
  82. package/server/lib/network/firewall-manager.test.js +254 -0
  83. package/server/lib/network/geoip-filter.js +282 -0
  84. package/server/lib/network/geoip-filter.test.js +264 -0
  85. package/server/lib/network/rate-limiter.js +229 -0
  86. package/server/lib/network/rate-limiter.test.js +293 -0
  87. package/server/lib/network/request-validator.js +351 -0
  88. package/server/lib/network/request-validator.test.js +345 -0
  89. package/server/lib/network/security-headers.js +251 -0
  90. package/server/lib/network/security-headers.test.js +283 -0
  91. package/server/lib/network/tls-config.js +210 -0
  92. package/server/lib/network/tls-config.test.js +248 -0
  93. package/server/lib/security/auth-security.js +369 -0
  94. package/server/lib/security/auth-security.test.js +448 -0
  95. package/server/lib/security/cis-benchmark.js +152 -0
  96. package/server/lib/security/cis-benchmark.test.js +137 -0
  97. package/server/lib/security/compose-templates.js +312 -0
  98. package/server/lib/security/compose-templates.test.js +229 -0
  99. package/server/lib/security/container-runtime.js +456 -0
  100. package/server/lib/security/container-runtime.test.js +503 -0
  101. package/server/lib/security/cors-validator.js +278 -0
  102. package/server/lib/security/cors-validator.test.js +310 -0
  103. package/server/lib/security/crypto-utils.js +253 -0
  104. package/server/lib/security/crypto-utils.test.js +409 -0
  105. package/server/lib/security/dockerfile-linter.js +459 -0
  106. package/server/lib/security/dockerfile-linter.test.js +483 -0
  107. package/server/lib/security/dockerfile-templates.js +278 -0
  108. package/server/lib/security/dockerfile-templates.test.js +164 -0
  109. package/server/lib/security/error-sanitizer.js +426 -0
  110. package/server/lib/security/error-sanitizer.test.js +331 -0
  111. package/server/lib/security/headers-generator.js +368 -0
  112. package/server/lib/security/headers-generator.test.js +398 -0
  113. package/server/lib/security/image-scanner.js +83 -0
  114. package/server/lib/security/image-scanner.test.js +106 -0
  115. package/server/lib/security/input-validator.js +352 -0
  116. package/server/lib/security/input-validator.test.js +330 -0
  117. package/server/lib/security/network-policy.js +174 -0
  118. package/server/lib/security/network-policy.test.js +164 -0
  119. package/server/lib/security/output-encoder.js +237 -0
  120. package/server/lib/security/output-encoder.test.js +276 -0
  121. package/server/lib/security/path-validator.js +359 -0
  122. package/server/lib/security/path-validator.test.js +293 -0
  123. package/server/lib/security/query-builder.js +421 -0
  124. package/server/lib/security/query-builder.test.js +318 -0
  125. package/server/lib/security/secret-detector.js +290 -0
  126. package/server/lib/security/secret-detector.test.js +354 -0
  127. package/server/lib/security/secrets-validator.js +137 -0
  128. package/server/lib/security/secrets-validator.test.js +120 -0
  129. package/server/lib/security-testing/dast-runner.js +154 -0
  130. package/server/lib/security-testing/dast-runner.test.js +62 -0
  131. package/server/lib/security-testing/dependency-scanner.js +172 -0
  132. package/server/lib/security-testing/dependency-scanner.test.js +64 -0
  133. package/server/lib/security-testing/pentest-runner.js +230 -0
  134. package/server/lib/security-testing/pentest-runner.test.js +60 -0
  135. package/server/lib/security-testing/sast-runner.js +136 -0
  136. package/server/lib/security-testing/sast-runner.test.js +62 -0
  137. package/server/lib/security-testing/secret-scanner.js +153 -0
  138. package/server/lib/security-testing/secret-scanner.test.js +66 -0
  139. package/server/lib/security-testing/security-gate.js +216 -0
  140. package/server/lib/security-testing/security-gate.test.js +115 -0
  141. package/server/lib/security-testing/security-reporter.js +303 -0
  142. package/server/lib/security-testing/security-reporter.test.js +114 -0
  143. package/server/lib/standards/audit-checker.js +546 -0
  144. package/server/lib/standards/audit-checker.test.js +415 -0
  145. package/server/lib/standards/cleanup-executor.js +452 -0
  146. package/server/lib/standards/cleanup-executor.test.js +293 -0
  147. package/server/lib/standards/refactor-stepper.js +425 -0
  148. package/server/lib/standards/refactor-stepper.test.js +298 -0
  149. package/server/lib/standards/standards-injector.js +167 -0
  150. package/server/lib/standards/standards-injector.test.js +232 -0
  151. package/server/lib/user-management.test.js +284 -0
  152. package/server/lib/vps/backup-manager.js +157 -0
  153. package/server/lib/vps/backup-manager.test.js +59 -0
  154. package/server/lib/vps/caddy-config.js +159 -0
  155. package/server/lib/vps/caddy-config.test.js +48 -0
  156. package/server/lib/vps/compose-orchestrator.js +219 -0
  157. package/server/lib/vps/compose-orchestrator.test.js +50 -0
  158. package/server/lib/vps/database-config.js +208 -0
  159. package/server/lib/vps/database-config.test.js +47 -0
  160. package/server/lib/vps/deploy-script.js +211 -0
  161. package/server/lib/vps/deploy-script.test.js +53 -0
  162. package/server/lib/vps/secrets-manager.js +148 -0
  163. package/server/lib/vps/secrets-manager.test.js +58 -0
  164. package/server/lib/vps/server-hardening.js +174 -0
  165. package/server/lib/vps/server-hardening.test.js +70 -0
  166. package/server/package-lock.json +19 -0
  167. package/server/package.json +1 -0
  168. package/server/templates/CLAUDE.md +37 -0
  169. package/server/templates/CODING-STANDARDS.md +408 -0
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Path Validator Tests
3
+ *
4
+ * Tests for path traversal prevention.
5
+ */
6
+
7
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
8
+ import {
9
+ validatePath,
10
+ normalizePath,
11
+ isWithinBase,
12
+ validateExtension,
13
+ createPathValidator,
14
+ PathTraversalError,
15
+ } from './path-validator.js';
16
+
17
+ describe('path-validator', () => {
18
+ describe('validatePath', () => {
19
+ it('allows path within base directory', () => {
20
+ const result = validatePath('/var/app/uploads/file.txt', {
21
+ baseDir: '/var/app/uploads',
22
+ });
23
+ expect(result.valid).toBe(true);
24
+ expect(result.normalizedPath).toBe('/var/app/uploads/file.txt');
25
+ });
26
+
27
+ it('allows nested paths within base', () => {
28
+ const result = validatePath('/var/app/uploads/user/123/file.txt', {
29
+ baseDir: '/var/app/uploads',
30
+ });
31
+ expect(result.valid).toBe(true);
32
+ });
33
+
34
+ it('blocks ../ traversal', () => {
35
+ const result = validatePath('/var/app/uploads/../secrets/password.txt', {
36
+ baseDir: '/var/app/uploads',
37
+ });
38
+ expect(result.valid).toBe(false);
39
+ expect(result.threat).toBe('path_traversal');
40
+ });
41
+
42
+ it('blocks multiple ../ traversal', () => {
43
+ const result = validatePath('/var/app/uploads/../../../etc/passwd', {
44
+ baseDir: '/var/app/uploads',
45
+ });
46
+ expect(result.valid).toBe(false);
47
+ expect(result.threat).toBe('path_traversal');
48
+ });
49
+
50
+ it('blocks URL-encoded traversal (..%2f)', () => {
51
+ const result = validatePath('/var/app/uploads/..%2f..%2fetc/passwd', {
52
+ baseDir: '/var/app/uploads',
53
+ });
54
+ expect(result.valid).toBe(false);
55
+ expect(result.threat).toBe('path_traversal');
56
+ });
57
+
58
+ it('blocks double URL-encoded traversal (..%252f)', () => {
59
+ const result = validatePath('/var/app/uploads/..%252f..%252fetc/passwd', {
60
+ baseDir: '/var/app/uploads',
61
+ });
62
+ expect(result.valid).toBe(false);
63
+ expect(result.threat).toBe('path_traversal');
64
+ });
65
+
66
+ it('blocks null byte in path', () => {
67
+ const result = validatePath('/var/app/uploads/file.txt\x00.jpg', {
68
+ baseDir: '/var/app/uploads',
69
+ });
70
+ expect(result.valid).toBe(false);
71
+ expect(result.threat).toBe('null_byte');
72
+ });
73
+
74
+ it('blocks Windows path traversal (..\\)', () => {
75
+ const result = validatePath('C:\\app\\uploads\\..\\..\\windows\\system32', {
76
+ baseDir: 'C:\\app\\uploads',
77
+ });
78
+ expect(result.valid).toBe(false);
79
+ expect(result.threat).toBe('path_traversal');
80
+ });
81
+
82
+ it('blocks absolute path outside base', () => {
83
+ const result = validatePath('/etc/passwd', {
84
+ baseDir: '/var/app/uploads',
85
+ });
86
+ expect(result.valid).toBe(false);
87
+ expect(result.threat).toBe('outside_base');
88
+ });
89
+
90
+ it('handles relative paths by resolving against base', () => {
91
+ const result = validatePath('subdir/file.txt', {
92
+ baseDir: '/var/app/uploads',
93
+ });
94
+ expect(result.valid).toBe(true);
95
+ expect(result.normalizedPath).toBe('/var/app/uploads/subdir/file.txt');
96
+ });
97
+ });
98
+
99
+ describe('normalizePath', () => {
100
+ it('resolves single dots', () => {
101
+ const result = normalizePath('/var/app/./uploads/./file.txt');
102
+ expect(result).toBe('/var/app/uploads/file.txt');
103
+ });
104
+
105
+ it('resolves double dots within allowed bounds', () => {
106
+ const result = normalizePath('/var/app/uploads/temp/../file.txt');
107
+ expect(result).toBe('/var/app/uploads/file.txt');
108
+ });
109
+
110
+ it('removes trailing slashes', () => {
111
+ const result = normalizePath('/var/app/uploads/');
112
+ expect(result).toBe('/var/app/uploads');
113
+ });
114
+
115
+ it('collapses multiple slashes', () => {
116
+ const result = normalizePath('/var//app///uploads////file.txt');
117
+ expect(result).toBe('/var/app/uploads/file.txt');
118
+ });
119
+
120
+ it('decodes URL-encoded characters', () => {
121
+ const result = normalizePath('/var/app/uploads/my%20file.txt');
122
+ expect(result).toBe('/var/app/uploads/my file.txt');
123
+ });
124
+
125
+ it('handles Windows paths', () => {
126
+ const result = normalizePath('C:\\Users\\app\\uploads\\file.txt');
127
+ expect(result).toContain('Users');
128
+ expect(result).toContain('file.txt');
129
+ });
130
+ });
131
+
132
+ describe('isWithinBase', () => {
133
+ it('returns true for path within base', () => {
134
+ const result = isWithinBase('/var/app/uploads/file.txt', '/var/app/uploads');
135
+ expect(result).toBe(true);
136
+ });
137
+
138
+ it('returns false for path outside base', () => {
139
+ const result = isWithinBase('/etc/passwd', '/var/app/uploads');
140
+ expect(result).toBe(false);
141
+ });
142
+
143
+ it('returns false for sibling directory', () => {
144
+ const result = isWithinBase('/var/app/secrets/key.txt', '/var/app/uploads');
145
+ expect(result).toBe(false);
146
+ });
147
+
148
+ it('handles trailing slashes consistently', () => {
149
+ const result1 = isWithinBase('/var/app/uploads/file.txt', '/var/app/uploads');
150
+ const result2 = isWithinBase('/var/app/uploads/file.txt', '/var/app/uploads/');
151
+ expect(result1).toBe(result2);
152
+ });
153
+ });
154
+
155
+ describe('validateExtension', () => {
156
+ it('allows whitelisted extension', () => {
157
+ const result = validateExtension('file.jpg', {
158
+ allowed: ['.jpg', '.png', '.gif'],
159
+ });
160
+ expect(result.valid).toBe(true);
161
+ });
162
+
163
+ it('rejects non-whitelisted extension', () => {
164
+ const result = validateExtension('file.exe', {
165
+ allowed: ['.jpg', '.png', '.gif'],
166
+ });
167
+ expect(result.valid).toBe(false);
168
+ });
169
+
170
+ it('handles case insensitivity', () => {
171
+ const result = validateExtension('file.JPG', {
172
+ allowed: ['.jpg', '.png'],
173
+ caseSensitive: false,
174
+ });
175
+ expect(result.valid).toBe(true);
176
+ });
177
+
178
+ it('rejects double extensions', () => {
179
+ const result = validateExtension('file.jpg.exe', {
180
+ allowed: ['.jpg'],
181
+ checkDoubleExtension: true,
182
+ });
183
+ expect(result.valid).toBe(false);
184
+ });
185
+
186
+ it('rejects hidden files when configured', () => {
187
+ const result = validateExtension('.htaccess', {
188
+ allowed: ['.txt'],
189
+ rejectHidden: true,
190
+ });
191
+ expect(result.valid).toBe(false);
192
+ });
193
+
194
+ it('blocks blacklisted extensions', () => {
195
+ const result = validateExtension('script.php', {
196
+ blocked: ['.php', '.exe', '.sh'],
197
+ });
198
+ expect(result.valid).toBe(false);
199
+ });
200
+ });
201
+
202
+ describe('createPathValidator', () => {
203
+ it('creates validator with multiple base directories', () => {
204
+ const validator = createPathValidator({
205
+ baseDirs: ['/var/app/uploads', '/var/app/public'],
206
+ });
207
+
208
+ expect(validator.validate('/var/app/uploads/file.txt').valid).toBe(true);
209
+ expect(validator.validate('/var/app/public/file.txt').valid).toBe(true);
210
+ expect(validator.validate('/etc/passwd').valid).toBe(false);
211
+ });
212
+
213
+ it('creates validator with extension whitelist', () => {
214
+ const validator = createPathValidator({
215
+ baseDirs: ['/var/app/uploads'],
216
+ allowedExtensions: ['.jpg', '.png'],
217
+ });
218
+
219
+ expect(validator.validate('/var/app/uploads/image.jpg').valid).toBe(true);
220
+ expect(validator.validate('/var/app/uploads/script.php').valid).toBe(false);
221
+ });
222
+
223
+ it('creates validator with max path length', () => {
224
+ const validator = createPathValidator({
225
+ baseDirs: ['/var/app/uploads'],
226
+ maxPathLength: 100,
227
+ });
228
+
229
+ const longPath = '/var/app/uploads/' + 'a'.repeat(100) + '.txt';
230
+ expect(validator.validate(longPath).valid).toBe(false);
231
+ });
232
+
233
+ it('creates validator with custom forbidden patterns', () => {
234
+ const validator = createPathValidator({
235
+ baseDirs: ['/var/app/uploads'],
236
+ forbiddenPatterns: [/\.git/, /node_modules/],
237
+ });
238
+
239
+ expect(validator.validate('/var/app/uploads/.git/config').valid).toBe(false);
240
+ expect(validator.validate('/var/app/uploads/node_modules/pkg').valid).toBe(false);
241
+ });
242
+ });
243
+
244
+ describe('symlink handling', () => {
245
+ it('blocks symlinks pointing outside base', async () => {
246
+ const validator = createPathValidator({
247
+ baseDirs: ['/var/app/uploads'],
248
+ followSymlinks: false,
249
+ });
250
+
251
+ // Mock fs.lstat to return symlink info
252
+ const result = await validator.validateAsync('/var/app/uploads/link-to-etc', {
253
+ checkSymlinks: true,
254
+ });
255
+
256
+ // This would need actual fs mocking in implementation
257
+ expect(result).toBeDefined();
258
+ });
259
+ });
260
+
261
+ describe('edge cases', () => {
262
+ it('handles empty path', () => {
263
+ const result = validatePath('', { baseDir: '/var/app/uploads' });
264
+ expect(result.valid).toBe(false);
265
+ });
266
+
267
+ it('handles path with only dots', () => {
268
+ const result = validatePath('....', { baseDir: '/var/app/uploads' });
269
+ expect(result.valid).toBe(false);
270
+ });
271
+
272
+ it('handles unicode in path', () => {
273
+ const result = validatePath('/var/app/uploads/文件.txt', {
274
+ baseDir: '/var/app/uploads',
275
+ });
276
+ expect(result.valid).toBe(true);
277
+ });
278
+
279
+ it('handles path with spaces', () => {
280
+ const result = validatePath('/var/app/uploads/my file.txt', {
281
+ baseDir: '/var/app/uploads',
282
+ });
283
+ expect(result.valid).toBe(true);
284
+ });
285
+
286
+ it('rejects path with control characters', () => {
287
+ const result = validatePath('/var/app/uploads/file\x1f.txt', {
288
+ baseDir: '/var/app/uploads',
289
+ });
290
+ expect(result.valid).toBe(false);
291
+ });
292
+ });
293
+ });
@@ -0,0 +1,421 @@
1
+ /**
2
+ * Query Builder Module
3
+ *
4
+ * Safe parameterized query building to prevent SQL injection.
5
+ * Addresses OWASP A03: Injection
6
+ */
7
+
8
+ /**
9
+ * Custom error for security violations
10
+ */
11
+ export class SecurityError extends Error {
12
+ constructor(message) {
13
+ super(message);
14
+ this.name = 'SecurityError';
15
+ }
16
+ }
17
+
18
+ /**
19
+ * SQL reserved words that need quoting
20
+ */
21
+ const RESERVED_WORDS = new Set([
22
+ 'order', 'group', 'select', 'table', 'user', 'index', 'key', 'from', 'to',
23
+ 'where', 'and', 'or', 'not', 'null', 'true', 'false', 'like', 'in', 'as',
24
+ 'join', 'left', 'right', 'inner', 'outer', 'on', 'using', 'limit', 'offset',
25
+ ]);
26
+
27
+ /**
28
+ * Escape identifier (table/column name)
29
+ * @param {string} identifier - The identifier to escape
30
+ * @returns {string} Escaped identifier
31
+ */
32
+ function escapeIdentifier(identifier) {
33
+ // Handle schema-qualified names (e.g., public.users)
34
+ if (identifier.includes('.') && !identifier.includes('..')) {
35
+ return identifier.split('.').map(escapeIdentifier).join('.');
36
+ }
37
+
38
+ // Check for dangerous SQL injection patterns in identifier (but allow hyphens)
39
+ if (/[;'"\\]/.test(identifier) || /--/.test(identifier)) {
40
+ throw new SecurityError(`Invalid identifier: ${identifier}`);
41
+ }
42
+
43
+ // Quote if contains special chars or is reserved word
44
+ if (/-/.test(identifier) || RESERVED_WORDS.has(identifier.toLowerCase())) {
45
+ return `"${identifier}"`;
46
+ }
47
+
48
+ return identifier;
49
+ }
50
+
51
+ /**
52
+ * Create a SELECT query builder
53
+ * @param {string} table - The table name
54
+ * @returns {Object} Query builder
55
+ */
56
+ export function select(table) {
57
+ return new SelectBuilder(table);
58
+ }
59
+
60
+ /**
61
+ * Create an INSERT query builder
62
+ * @param {string} table - The table name
63
+ * @returns {Object} Query builder
64
+ */
65
+ export function insert(table) {
66
+ return new InsertBuilder(table);
67
+ }
68
+
69
+ /**
70
+ * Create an UPDATE query builder
71
+ * @param {string} table - The table name
72
+ * @returns {Object} Query builder
73
+ */
74
+ export function update(table) {
75
+ return new UpdateBuilder(table);
76
+ }
77
+
78
+ /**
79
+ * Create a DELETE query builder
80
+ * @param {string} table - The table name
81
+ * @returns {Object} Query builder
82
+ */
83
+ export function del(table) {
84
+ return new DeleteBuilder(table);
85
+ }
86
+
87
+ /**
88
+ * Create a query builder with options
89
+ * @param {Object} options - Builder options
90
+ * @returns {Object} Query builder factory
91
+ */
92
+ export function createQueryBuilder(options = {}) {
93
+ const { dialect = 'postgresql', allowedTables = null, allowedColumns = null } = options;
94
+
95
+ return {
96
+ select(table) {
97
+ if (allowedTables && !allowedTables.includes(table)) {
98
+ throw new SecurityError(`Table not allowed: ${table}`);
99
+ }
100
+ const builder = new SelectBuilder(table, { dialect, allowedColumns });
101
+ return builder;
102
+ },
103
+ insert(table) {
104
+ if (allowedTables && !allowedTables.includes(table)) {
105
+ throw new SecurityError(`Table not allowed: ${table}`);
106
+ }
107
+ return new InsertBuilder(table, { dialect });
108
+ },
109
+ update(table) {
110
+ if (allowedTables && !allowedTables.includes(table)) {
111
+ throw new SecurityError(`Table not allowed: ${table}`);
112
+ }
113
+ return new UpdateBuilder(table, { dialect });
114
+ },
115
+ delete(table) {
116
+ if (allowedTables && !allowedTables.includes(table)) {
117
+ throw new SecurityError(`Table not allowed: ${table}`);
118
+ }
119
+ return new DeleteBuilder(table, { dialect });
120
+ },
121
+ };
122
+ }
123
+
124
+ /**
125
+ * Base query builder class
126
+ */
127
+ class BaseBuilder {
128
+ constructor(table, options = {}) {
129
+ this._table = escapeIdentifier(table);
130
+ this._dialect = options.dialect || 'postgresql';
131
+ this._params = [];
132
+ this._paramIndex = 0;
133
+ this._allowedColumns = options.allowedColumns;
134
+ }
135
+
136
+ _placeholder() {
137
+ this._paramIndex++;
138
+ if (this._dialect === 'postgresql') {
139
+ return `$${this._paramIndex}`;
140
+ }
141
+ return '?';
142
+ }
143
+
144
+ _validateColumn(column) {
145
+ if (this._allowedColumns && this._allowedColumns[this._table]) {
146
+ if (!this._allowedColumns[this._table].includes(column)) {
147
+ throw new SecurityError(`Column not allowed: ${column}`);
148
+ }
149
+ }
150
+ }
151
+ }
152
+
153
+ /**
154
+ * SELECT query builder
155
+ */
156
+ class SelectBuilder extends BaseBuilder {
157
+ constructor(table, options) {
158
+ super(table, options);
159
+ this._columns = ['*'];
160
+ this._where = [];
161
+ this._orderBy = [];
162
+ this._limit = null;
163
+ this._offset = null;
164
+ }
165
+
166
+ columns(cols) {
167
+ if (cols.length > 0 && cols[0] !== '*') {
168
+ cols.forEach((col) => this._validateColumn(col));
169
+ }
170
+ this._columns = cols.map((col) => col === '*' ? '*' : escapeIdentifier(col));
171
+ return this;
172
+ }
173
+
174
+ where(column, operator, value) {
175
+ this._where.push({
176
+ column: escapeIdentifier(column),
177
+ operator,
178
+ value,
179
+ type: 'AND',
180
+ });
181
+ return this;
182
+ }
183
+
184
+ orWhere(column, operator, value) {
185
+ this._where.push({
186
+ column: escapeIdentifier(column),
187
+ operator,
188
+ value,
189
+ type: 'OR',
190
+ });
191
+ return this;
192
+ }
193
+
194
+ whereIn(column, values) {
195
+ this._where.push({
196
+ column: escapeIdentifier(column),
197
+ operator: 'IN',
198
+ value: values,
199
+ type: 'AND',
200
+ isIn: true,
201
+ });
202
+ return this;
203
+ }
204
+
205
+ whereRaw(/* raw */) {
206
+ throw new SecurityError('Raw WHERE clauses are not allowed - use parameterized queries');
207
+ }
208
+
209
+ orderBy(column, direction = 'ASC') {
210
+ const dir = direction.toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
211
+ this._orderBy.push(`${escapeIdentifier(column)} ${dir}`);
212
+ return this;
213
+ }
214
+
215
+ limit(n) {
216
+ this._limit = n;
217
+ return this;
218
+ }
219
+
220
+ offset(n) {
221
+ this._offset = n;
222
+ return this;
223
+ }
224
+
225
+ build() {
226
+ const parts = [`SELECT ${this._columns.join(', ')} FROM ${this._table}`];
227
+
228
+ if (this._where.length > 0) {
229
+ const whereClauses = this._where.map((w, i) => {
230
+ let clause;
231
+ if (w.isIn) {
232
+ const placeholders = w.value.map(() => this._placeholder());
233
+ this._params.push(...w.value);
234
+ clause = `${w.column} IN (${placeholders.join(', ')})`;
235
+ } else {
236
+ clause = `${w.column} ${w.operator} ${this._placeholder()}`;
237
+ this._params.push(w.value);
238
+ }
239
+ return i === 0 ? clause : `${w.type} ${clause}`;
240
+ });
241
+ parts.push(`WHERE ${whereClauses.join(' ')}`);
242
+ }
243
+
244
+ if (this._orderBy.length > 0) {
245
+ parts.push(`ORDER BY ${this._orderBy.join(', ')}`);
246
+ }
247
+
248
+ if (this._limit !== null) {
249
+ parts.push(`LIMIT ${this._placeholder()}`);
250
+ this._params.push(this._limit);
251
+ }
252
+
253
+ if (this._offset !== null) {
254
+ parts.push(`OFFSET ${this._placeholder()}`);
255
+ this._params.push(this._offset);
256
+ }
257
+
258
+ return { sql: parts.join(' '), params: this._params };
259
+ }
260
+ }
261
+
262
+ /**
263
+ * INSERT query builder
264
+ */
265
+ class InsertBuilder extends BaseBuilder {
266
+ constructor(table, options) {
267
+ super(table, options);
268
+ this._values = null;
269
+ this._returning = [];
270
+ }
271
+
272
+ values(data) {
273
+ this._values = Array.isArray(data) ? data : [data];
274
+ return this;
275
+ }
276
+
277
+ returning(cols) {
278
+ this._returning = cols;
279
+ return this;
280
+ }
281
+
282
+ build() {
283
+ if (!this._values || this._values.length === 0) {
284
+ throw new SecurityError('INSERT requires values');
285
+ }
286
+
287
+ const columns = Object.keys(this._values[0]);
288
+
289
+ // Validate column names
290
+ columns.forEach((col) => {
291
+ if (/[;'"\\\/]/.test(col) || /\b(drop|delete|truncate|alter)\b/i.test(col)) {
292
+ throw new SecurityError(`Invalid column name: ${col}`);
293
+ }
294
+ });
295
+
296
+ const escapedColumns = columns.map((col) => escapeIdentifier(col));
297
+ const valueSets = this._values.map((row) => {
298
+ const placeholders = columns.map(() => this._placeholder());
299
+ columns.forEach((col) => this._params.push(row[col]));
300
+ return `(${placeholders.join(', ')})`;
301
+ });
302
+
303
+ let sql = `INSERT INTO ${this._table} (${escapedColumns.join(', ')}) VALUES ${valueSets.join(', ')}`;
304
+
305
+ if (this._returning.length > 0) {
306
+ sql += ` RETURNING ${this._returning.map((c) => escapeIdentifier(c)).join(', ')}`;
307
+ }
308
+
309
+ return { sql, params: this._params };
310
+ }
311
+ }
312
+
313
+ /**
314
+ * UPDATE query builder
315
+ */
316
+ class UpdateBuilder extends BaseBuilder {
317
+ constructor(table, options) {
318
+ super(table, options);
319
+ this._set = {};
320
+ this._where = [];
321
+ this._returning = [];
322
+ this._unsafe = false;
323
+ }
324
+
325
+ set(data) {
326
+ this._set = data;
327
+ return this;
328
+ }
329
+
330
+ where(column, operator, value) {
331
+ this._where.push({
332
+ column: escapeIdentifier(column),
333
+ operator,
334
+ value,
335
+ });
336
+ return this;
337
+ }
338
+
339
+ returning(cols) {
340
+ this._returning = cols;
341
+ return this;
342
+ }
343
+
344
+ unsafe() {
345
+ this._unsafe = true;
346
+ return this;
347
+ }
348
+
349
+ build() {
350
+ if (!this._unsafe && this._where.length === 0) {
351
+ throw new SecurityError('UPDATE without WHERE clause requires .unsafe() call');
352
+ }
353
+
354
+ const setClauses = Object.entries(this._set).map(([col, val]) => {
355
+ const placeholder = this._placeholder();
356
+ this._params.push(val);
357
+ return `${escapeIdentifier(col)} = ${placeholder}`;
358
+ });
359
+
360
+ let sql = `UPDATE ${this._table} SET ${setClauses.join(', ')}`;
361
+
362
+ if (this._where.length > 0) {
363
+ const whereClauses = this._where.map((w, i) => {
364
+ const clause = `${w.column} ${w.operator} ${this._placeholder()}`;
365
+ this._params.push(w.value);
366
+ return i === 0 ? clause : `AND ${clause}`;
367
+ });
368
+ sql += ` WHERE ${whereClauses.join(' ')}`;
369
+ }
370
+
371
+ if (this._returning.length > 0) {
372
+ sql += ` RETURNING ${this._returning.map((c) => escapeIdentifier(c)).join(', ')}`;
373
+ }
374
+
375
+ return { sql, params: this._params };
376
+ }
377
+ }
378
+
379
+ /**
380
+ * DELETE query builder
381
+ */
382
+ class DeleteBuilder extends BaseBuilder {
383
+ constructor(table, options) {
384
+ super(table, options);
385
+ this._where = [];
386
+ this._unsafe = false;
387
+ }
388
+
389
+ where(column, operator, value) {
390
+ this._where.push({
391
+ column: escapeIdentifier(column),
392
+ operator,
393
+ value,
394
+ });
395
+ return this;
396
+ }
397
+
398
+ unsafe() {
399
+ this._unsafe = true;
400
+ return this;
401
+ }
402
+
403
+ build() {
404
+ if (!this._unsafe && this._where.length === 0) {
405
+ throw new SecurityError('DELETE without WHERE clause requires .unsafe() call');
406
+ }
407
+
408
+ let sql = `DELETE FROM ${this._table}`;
409
+
410
+ if (this._where.length > 0) {
411
+ const whereClauses = this._where.map((w, i) => {
412
+ const clause = `${w.column} ${w.operator} ${this._placeholder()}`;
413
+ this._params.push(w.value);
414
+ return i === 0 ? clause : `AND ${clause}`;
415
+ });
416
+ sql += ` WHERE ${whereClauses.join(' ')}`;
417
+ }
418
+
419
+ return { sql, params: this._params };
420
+ }
421
+ }