@tamyla/clodo-framework 4.5.0 → 4.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/README.md +157 -13
- package/dist/cli/clodo-service.js +13 -0
- package/dist/cli/commands/config-schema.js +144 -0
- package/dist/cli/commands/create.js +18 -1
- package/dist/cli/commands/deploy.js +61 -2
- package/dist/cli/commands/doctor.js +124 -0
- package/dist/cli/commands/secrets.js +258 -0
- package/dist/cli/security-cli.js +1 -1
- package/dist/index.js +3 -1
- package/dist/security/SecretsManager.js +398 -0
- package/dist/security/index.js +2 -0
- package/dist/service-management/ConfirmationEngine.js +4 -1
- package/dist/service-management/generators/utils/ServiceManifestGenerator.js +5 -1
- package/dist/service-management/handlers/ConfirmationHandler.js +1 -1
- package/dist/service-management/handlers/ValidationHandler.js +696 -0
- package/dist/validation/ConfigSchemaValidator.js +503 -0
- package/dist/validation/configSchemas.js +236 -0
- package/dist/validation/index.js +6 -2
- package/docs/00_START_HERE.md +26 -338
- package/package.json +1 -1
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clodo Framework - Secrets Manager
|
|
3
|
+
*
|
|
4
|
+
* Provides scanning, baseline management, and validation for secret leakage prevention.
|
|
5
|
+
* Extracted from ValidationHandler and enhanced with baseline update/show/validate flows.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import { SecretsManager } from './SecretsManager.js';
|
|
9
|
+
* const mgr = new SecretsManager();
|
|
10
|
+
* const results = await mgr.scan('/path/to/service');
|
|
11
|
+
* const validation = await mgr.validate('/path/to/service');
|
|
12
|
+
* await mgr.baselineUpdate('/path/to/service');
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fs from 'fs/promises';
|
|
16
|
+
import path from 'path';
|
|
17
|
+
import { SecretGenerator } from './SecretGenerator.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Default secret detection patterns
|
|
21
|
+
*/
|
|
22
|
+
const SECRET_PATTERNS = [{
|
|
23
|
+
name: 'api_key',
|
|
24
|
+
pattern: /(api[_-]?key|apikey)\s*[=:]\s*['"]([^'"]{20,})['"]/gi,
|
|
25
|
+
severity: 'high'
|
|
26
|
+
}, {
|
|
27
|
+
name: 'secret',
|
|
28
|
+
pattern: /(secret|token)\s*[=:]\s*['"]([^'"]{20,})['"]/gi,
|
|
29
|
+
severity: 'high'
|
|
30
|
+
}, {
|
|
31
|
+
name: 'password',
|
|
32
|
+
pattern: /(password|passwd|pwd)\s*[=:]\s*['"]([^'"]{8,})['"]/gi,
|
|
33
|
+
severity: 'critical'
|
|
34
|
+
}, {
|
|
35
|
+
name: 'private_key',
|
|
36
|
+
pattern: /-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----/gi,
|
|
37
|
+
severity: 'critical'
|
|
38
|
+
}, {
|
|
39
|
+
name: 'aws_access_key',
|
|
40
|
+
pattern: /AKIA[0-9A-Z]{16}/g,
|
|
41
|
+
severity: 'critical'
|
|
42
|
+
}, {
|
|
43
|
+
name: 'cloudflare_token',
|
|
44
|
+
pattern: /(?:cloudflare|cf)[_-]?(?:api[_-]?)?token\s*[=:]\s*['"]([^'"]{40,})['"]/gi,
|
|
45
|
+
severity: 'high'
|
|
46
|
+
}, {
|
|
47
|
+
name: 'jwt_secret',
|
|
48
|
+
pattern: /(?:jwt|json[_-]?web[_-]?token)[_-]?secret\s*[=:]\s*['"]([^'"]{20,})['"]/gi,
|
|
49
|
+
severity: 'high'
|
|
50
|
+
}, {
|
|
51
|
+
name: 'database_url',
|
|
52
|
+
pattern: /(?:database|db)[_-]?url\s*[=:]\s*['"]([^'"]*password[^'"]*)['"]/gi,
|
|
53
|
+
severity: 'high'
|
|
54
|
+
}, {
|
|
55
|
+
name: 'stripe_key',
|
|
56
|
+
pattern: /(sk|pk)_(?:test|live)_[0-9a-zA-Z]{20,}/g,
|
|
57
|
+
severity: 'critical'
|
|
58
|
+
}, {
|
|
59
|
+
name: 'github_token',
|
|
60
|
+
pattern: /gh[ps]_[A-Za-z0-9_]{36,}/g,
|
|
61
|
+
severity: 'critical'
|
|
62
|
+
}, {
|
|
63
|
+
name: 'slack_token',
|
|
64
|
+
pattern: /xox[baprs]-[0-9A-Za-z-]{10,}/g,
|
|
65
|
+
severity: 'high'
|
|
66
|
+
}, {
|
|
67
|
+
name: 'generic_secret',
|
|
68
|
+
pattern: /(?:auth|bearer)\s*[=:]\s*['"]([^'"]{30,})['"]/gi,
|
|
69
|
+
severity: 'medium'
|
|
70
|
+
}];
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* File extensions to scan
|
|
74
|
+
*/
|
|
75
|
+
const SCAN_EXTENSIONS = ['.js', '.ts', '.json', '.toml', '.env', '.md', '.txt', '.yaml', '.yml', '.cfg', '.ini'];
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Directories to skip during scanning
|
|
79
|
+
*/
|
|
80
|
+
const SKIP_DIRECTORIES = ['node_modules', '.git', 'dist', 'build', 'coverage', 'logs', '.wrangler', '.cache'];
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Words that indicate a value is clearly a test/example (case-insensitive)
|
|
84
|
+
*/
|
|
85
|
+
const FALSE_POSITIVE_INDICATORS = ['example', 'test', 'fake', 'placeholder', 'dummy', 'sample', 'your-', 'xxx', 'changeme', 'todo', 'fixme'];
|
|
86
|
+
export class SecretsManager {
|
|
87
|
+
/**
|
|
88
|
+
* @param {Object} options
|
|
89
|
+
* @param {Array} options.patterns - Custom patterns (defaults to SECRET_PATTERNS)
|
|
90
|
+
* @param {Array} options.extensions - File extensions to scan (defaults to SCAN_EXTENSIONS)
|
|
91
|
+
* @param {Array} options.skipDirs - Directories to skip (defaults to SKIP_DIRECTORIES)
|
|
92
|
+
* @param {boolean} options.includeTests - Include test/example content (default: false)
|
|
93
|
+
*/
|
|
94
|
+
constructor(options = {}) {
|
|
95
|
+
this.patterns = options.patterns || SECRET_PATTERNS;
|
|
96
|
+
this.extensions = options.extensions || SCAN_EXTENSIONS;
|
|
97
|
+
this.skipDirs = options.skipDirs || SKIP_DIRECTORIES;
|
|
98
|
+
this.includeTests = options.includeTests || false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Scan a directory for potential secrets
|
|
103
|
+
* @param {string} servicePath - Root path to scan
|
|
104
|
+
* @returns {Promise<Array<SecretFinding>>} Array of findings
|
|
105
|
+
*/
|
|
106
|
+
async scan(servicePath) {
|
|
107
|
+
const resolvedPath = path.resolve(servicePath);
|
|
108
|
+
const files = await this.getFilesToScan(resolvedPath);
|
|
109
|
+
const findings = [];
|
|
110
|
+
for (const file of files) {
|
|
111
|
+
try {
|
|
112
|
+
const content = await fs.readFile(file, 'utf8');
|
|
113
|
+
const lines = content.split('\n');
|
|
114
|
+
const relativePath = path.relative(resolvedPath, file);
|
|
115
|
+
lines.forEach((line, index) => {
|
|
116
|
+
for (const {
|
|
117
|
+
name,
|
|
118
|
+
pattern,
|
|
119
|
+
severity
|
|
120
|
+
} of this.patterns) {
|
|
121
|
+
// Reset regex lastIndex for global patterns
|
|
122
|
+
pattern.lastIndex = 0;
|
|
123
|
+
let match;
|
|
124
|
+
while ((match = pattern.exec(line)) !== null) {
|
|
125
|
+
// Skip false positives unless includeTests is set
|
|
126
|
+
if (!this.includeTests && this._isFalsePositive(line)) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
findings.push({
|
|
130
|
+
file: relativePath,
|
|
131
|
+
line: index + 1,
|
|
132
|
+
pattern: name,
|
|
133
|
+
severity,
|
|
134
|
+
match: this._truncateMatch(match[0]),
|
|
135
|
+
column: match.index + 1
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
} catch {
|
|
141
|
+
// Skip files that can't be read
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return findings;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Validate: scan and compare against baseline, return pass/fail
|
|
150
|
+
* @param {string} servicePath - Root path to scan
|
|
151
|
+
* @returns {Promise<ValidationResult>}
|
|
152
|
+
*/
|
|
153
|
+
async validate(servicePath) {
|
|
154
|
+
const resolvedPath = path.resolve(servicePath);
|
|
155
|
+
const findings = await this.scan(resolvedPath);
|
|
156
|
+
const baseline = await this.loadBaseline(resolvedPath);
|
|
157
|
+
const newFindings = findings.filter(f => !this._isInBaseline(f, baseline));
|
|
158
|
+
const removedFromBaseline = baseline.filter(b => !findings.some(f => f.file === b.file && f.line === b.line && f.pattern === b.pattern));
|
|
159
|
+
const passed = newFindings.length === 0;
|
|
160
|
+
return {
|
|
161
|
+
passed,
|
|
162
|
+
totalFindings: findings.length,
|
|
163
|
+
baselineCount: baseline.length,
|
|
164
|
+
newFindings,
|
|
165
|
+
removedFromBaseline,
|
|
166
|
+
message: passed ? findings.length === 0 ? 'No potential secrets found' : `All ${findings.length} findings are in the baseline` : `Found ${newFindings.length} new potential secret(s) not in baseline`
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Update the baseline file with current findings
|
|
172
|
+
* @param {string} servicePath - Root path to scan
|
|
173
|
+
* @param {Object} options
|
|
174
|
+
* @param {boolean} options.addAll - Add all new findings without prompting
|
|
175
|
+
* @param {boolean} options.prune - Remove stale entries no longer detected
|
|
176
|
+
* @param {string} options.reason - Reason for adding (for audit trail)
|
|
177
|
+
* @returns {Promise<BaselineUpdateResult>}
|
|
178
|
+
*/
|
|
179
|
+
async baselineUpdate(servicePath, options = {}) {
|
|
180
|
+
const resolvedPath = path.resolve(servicePath);
|
|
181
|
+
const findings = await this.scan(resolvedPath);
|
|
182
|
+
const baseline = await this.loadBaseline(resolvedPath);
|
|
183
|
+
const newFindings = findings.filter(f => !this._isInBaseline(f, baseline));
|
|
184
|
+
const staleEntries = baseline.filter(b => !findings.some(f => f.file === b.file && f.line === b.line && f.pattern === b.pattern));
|
|
185
|
+
let updatedBaseline = [...baseline];
|
|
186
|
+
let added = 0;
|
|
187
|
+
let pruned = 0;
|
|
188
|
+
|
|
189
|
+
// Add new findings
|
|
190
|
+
if (options.addAll && newFindings.length > 0) {
|
|
191
|
+
const timestamp = new Date().toISOString();
|
|
192
|
+
const newEntries = newFindings.map(f => ({
|
|
193
|
+
file: f.file,
|
|
194
|
+
line: f.line,
|
|
195
|
+
pattern: f.pattern,
|
|
196
|
+
match: f.match,
|
|
197
|
+
severity: f.severity,
|
|
198
|
+
addedAt: timestamp,
|
|
199
|
+
reason: options.reason || 'bulk-update'
|
|
200
|
+
}));
|
|
201
|
+
updatedBaseline = [...updatedBaseline, ...newEntries];
|
|
202
|
+
added = newEntries.length;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Prune stale entries
|
|
206
|
+
if (options.prune && staleEntries.length > 0) {
|
|
207
|
+
updatedBaseline = updatedBaseline.filter(b => !staleEntries.some(s => s.file === b.file && s.line === b.line && s.pattern === b.pattern));
|
|
208
|
+
pruned = staleEntries.length;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Sort by file then line
|
|
212
|
+
updatedBaseline.sort((a, b) => {
|
|
213
|
+
const fileCompare = a.file.localeCompare(b.file);
|
|
214
|
+
return fileCompare !== 0 ? fileCompare : a.line - b.line;
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Write baseline
|
|
218
|
+
await this.saveBaseline(resolvedPath, updatedBaseline);
|
|
219
|
+
return {
|
|
220
|
+
added,
|
|
221
|
+
pruned,
|
|
222
|
+
total: updatedBaseline.length,
|
|
223
|
+
staleEntries: staleEntries.length,
|
|
224
|
+
newFindings: newFindings.length,
|
|
225
|
+
baselinePath: this._getBaselinePath(resolvedPath)
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Get current baseline contents
|
|
231
|
+
* @param {string} servicePath - Root path
|
|
232
|
+
* @returns {Promise<Array>} Current baseline entries
|
|
233
|
+
*/
|
|
234
|
+
async baselineShow(servicePath) {
|
|
235
|
+
const resolvedPath = path.resolve(servicePath);
|
|
236
|
+
return await this.loadBaseline(resolvedPath);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Load the .secrets.baseline file
|
|
241
|
+
* @param {string} servicePath
|
|
242
|
+
* @returns {Promise<Array>}
|
|
243
|
+
*/
|
|
244
|
+
async loadBaseline(servicePath) {
|
|
245
|
+
const baselinePath = this._getBaselinePath(servicePath);
|
|
246
|
+
try {
|
|
247
|
+
const content = await fs.readFile(baselinePath, 'utf8');
|
|
248
|
+
return JSON.parse(content);
|
|
249
|
+
} catch {
|
|
250
|
+
return [];
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Save the baseline file
|
|
256
|
+
* @param {string} servicePath
|
|
257
|
+
* @param {Array} baseline
|
|
258
|
+
*/
|
|
259
|
+
async saveBaseline(servicePath, baseline) {
|
|
260
|
+
const baselinePath = this._getBaselinePath(servicePath);
|
|
261
|
+
await fs.writeFile(baselinePath, JSON.stringify(baseline, null, 2) + '\n', 'utf8');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Get all files to scan in a directory tree
|
|
266
|
+
* @param {string} dir - Root directory
|
|
267
|
+
* @returns {Promise<string[]>}
|
|
268
|
+
*/
|
|
269
|
+
async getFilesToScan(dir) {
|
|
270
|
+
const files = [];
|
|
271
|
+
const scanDir = async currentDir => {
|
|
272
|
+
let entries;
|
|
273
|
+
try {
|
|
274
|
+
entries = await fs.readdir(currentDir, {
|
|
275
|
+
withFileTypes: true
|
|
276
|
+
});
|
|
277
|
+
} catch {
|
|
278
|
+
return; // Skip unreadable directories
|
|
279
|
+
}
|
|
280
|
+
for (const entry of entries) {
|
|
281
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
282
|
+
if (entry.isDirectory()) {
|
|
283
|
+
if (!this.skipDirs.includes(entry.name)) {
|
|
284
|
+
await scanDir(fullPath);
|
|
285
|
+
}
|
|
286
|
+
} else if (entry.isFile()) {
|
|
287
|
+
const ext = path.extname(entry.name);
|
|
288
|
+
if (this.extensions.includes(ext) || entry.name.startsWith('.env')) {
|
|
289
|
+
files.push(fullPath);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
await scanDir(dir);
|
|
295
|
+
return files;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Get the configured secret patterns
|
|
300
|
+
* @returns {Array} Pattern definitions
|
|
301
|
+
*/
|
|
302
|
+
getPatterns() {
|
|
303
|
+
return this.patterns.map(({
|
|
304
|
+
name,
|
|
305
|
+
severity
|
|
306
|
+
}) => ({
|
|
307
|
+
name,
|
|
308
|
+
severity
|
|
309
|
+
}));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Generate a secure replacement value for a detected secret.
|
|
314
|
+
* Delegates to SecretGenerator for cryptographically safe key generation.
|
|
315
|
+
* @param {string} patternName - The detected pattern type (api_key, jwt_secret, password, etc.)
|
|
316
|
+
* @param {Object} options
|
|
317
|
+
* @param {number} options.length - Key length in bytes (default: 32)
|
|
318
|
+
* @param {string} options.prefix - Optional key prefix
|
|
319
|
+
* @returns {Object} Generated key with metadata
|
|
320
|
+
*/
|
|
321
|
+
generateReplacement(patternName, options = {}) {
|
|
322
|
+
const {
|
|
323
|
+
length = 32,
|
|
324
|
+
prefix = ''
|
|
325
|
+
} = options;
|
|
326
|
+
switch (patternName) {
|
|
327
|
+
case 'jwt_secret':
|
|
328
|
+
return SecretGenerator.generateKeyWithMetadata('jwt', length || 64);
|
|
329
|
+
case 'api_key':
|
|
330
|
+
case 'cloudflare_token':
|
|
331
|
+
case 'github_token':
|
|
332
|
+
case 'slack_token':
|
|
333
|
+
return SecretGenerator.generateKeyWithMetadata(prefix || patternName, length);
|
|
334
|
+
default:
|
|
335
|
+
return SecretGenerator.generateKeyWithMetadata(prefix, length);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Validate the strength of a secret value using SecretGenerator's entropy analysis
|
|
341
|
+
* @param {string} key - The secret value to validate
|
|
342
|
+
* @param {Object} requirements - Strength requirements
|
|
343
|
+
* @returns {Object} Validation result with { valid, issues }
|
|
344
|
+
*/
|
|
345
|
+
validateKeyStrength(key, requirements = {}) {
|
|
346
|
+
return SecretGenerator.validateKeyStrength(key, requirements);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ─── Private helpers ──────────────────────────────────────────
|
|
350
|
+
|
|
351
|
+
_getBaselinePath(servicePath) {
|
|
352
|
+
return path.join(servicePath, '.secrets.baseline');
|
|
353
|
+
}
|
|
354
|
+
_isInBaseline(finding, baseline) {
|
|
355
|
+
return baseline.some(b => b.file === finding.file && b.line === finding.line && b.pattern === finding.pattern);
|
|
356
|
+
}
|
|
357
|
+
_isFalsePositive(line) {
|
|
358
|
+
const lower = line.toLowerCase();
|
|
359
|
+
return FALSE_POSITIVE_INDICATORS.some(indicator => lower.includes(indicator));
|
|
360
|
+
}
|
|
361
|
+
_truncateMatch(matchStr) {
|
|
362
|
+
const maxLen = 50;
|
|
363
|
+
if (matchStr.length <= maxLen) {
|
|
364
|
+
return matchStr + '...';
|
|
365
|
+
}
|
|
366
|
+
return matchStr.substring(0, maxLen) + '...';
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* @typedef {Object} SecretFinding
|
|
372
|
+
* @property {string} file - Relative file path
|
|
373
|
+
* @property {number} line - Line number (1-based)
|
|
374
|
+
* @property {string} pattern - Pattern name that matched
|
|
375
|
+
* @property {string} severity - critical|high|medium
|
|
376
|
+
* @property {string} match - Truncated match string
|
|
377
|
+
* @property {number} column - Column position
|
|
378
|
+
*/
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* @typedef {Object} ValidationResult
|
|
382
|
+
* @property {boolean} passed - Whether validation passed
|
|
383
|
+
* @property {number} totalFindings - Total secrets found
|
|
384
|
+
* @property {number} baselineCount - Baseline entries count
|
|
385
|
+
* @property {Array<SecretFinding>} newFindings - Secrets not in baseline
|
|
386
|
+
* @property {Array} removedFromBaseline - Stale baseline entries
|
|
387
|
+
* @property {string} message - Human-readable summary
|
|
388
|
+
*/
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* @typedef {Object} BaselineUpdateResult
|
|
392
|
+
* @property {number} added - New entries added
|
|
393
|
+
* @property {number} pruned - Stale entries removed
|
|
394
|
+
* @property {number} total - Total baseline entries
|
|
395
|
+
* @property {number} staleEntries - Stale entries found
|
|
396
|
+
* @property {number} newFindings - New findings found
|
|
397
|
+
* @property {string} baselinePath - Path to baseline file
|
|
398
|
+
*/
|
package/dist/security/index.js
CHANGED
|
@@ -6,12 +6,14 @@
|
|
|
6
6
|
import { ConfigurationValidator } from './ConfigurationValidator.js';
|
|
7
7
|
// DeploymentManager removed - replaced by MultiDomainOrchestrator + WranglerConfigManager
|
|
8
8
|
import { SecretGenerator } from './SecretGenerator.js';
|
|
9
|
+
import { SecretsManager } from './SecretsManager.js';
|
|
9
10
|
import { SecurityCLI } from './SecurityCLI.js';
|
|
10
11
|
// InteractiveDeploymentConfigurator removed - replaced by InputCollector
|
|
11
12
|
|
|
12
13
|
export { ConfigurationValidator } from './ConfigurationValidator.js';
|
|
13
14
|
// export { DeploymentManager} - DEPRECATED: Use MultiDomainOrchestrator instead
|
|
14
15
|
export { SecretGenerator } from './SecretGenerator.js';
|
|
16
|
+
export { SecretsManager } from './SecretsManager.js';
|
|
15
17
|
export { SecurityCLI } from './SecurityCLI.js';
|
|
16
18
|
// export { InteractiveDeploymentConfigurator } - DEPRECATED: Use InputCollector instead
|
|
17
19
|
|
|
@@ -37,7 +37,7 @@ import { validateServiceName, validateDomainName } from '../utils/validation.js'
|
|
|
37
37
|
import { NameFormatters, UrlFormatters, ResourceFormatters } from '../utils/formatters.js';
|
|
38
38
|
export class ConfirmationEngine {
|
|
39
39
|
constructor(options = {}) {
|
|
40
|
-
this.interactive = options.interactive
|
|
40
|
+
this.interactive = !!options.interactive;
|
|
41
41
|
this.rl = this.interactive ? createInterface({
|
|
42
42
|
input: process.stdin,
|
|
43
43
|
output: process.stdout
|
|
@@ -48,6 +48,7 @@ export class ConfirmationEngine {
|
|
|
48
48
|
* Generate and confirm all derived values from core inputs
|
|
49
49
|
*/
|
|
50
50
|
async generateAndConfirm(coreInputs) {
|
|
51
|
+
console.error('DEBUG: ConfirmationEngine.generateAndConfirm called');
|
|
51
52
|
console.log(chalk.cyan('\n🔍 Tier 2: Smart Confirmations'));
|
|
52
53
|
console.log(chalk.white('Reviewing and confirming 15 derived configuration values...\n'));
|
|
53
54
|
|
|
@@ -341,6 +342,7 @@ export class ConfirmationEngine {
|
|
|
341
342
|
* Generate features based on service type
|
|
342
343
|
*/
|
|
343
344
|
generateFeaturesForType(serviceType) {
|
|
345
|
+
console.error(`DEBUG: generateFeaturesForType called with serviceType: ${serviceType}`);
|
|
344
346
|
const baseFeatures = {
|
|
345
347
|
logging: true,
|
|
346
348
|
monitoring: true,
|
|
@@ -353,6 +355,7 @@ export class ConfirmationEngine {
|
|
|
353
355
|
authentication: true,
|
|
354
356
|
authorization: true,
|
|
355
357
|
database: true,
|
|
358
|
+
d1: true,
|
|
356
359
|
search: true,
|
|
357
360
|
filtering: true,
|
|
358
361
|
pagination: true,
|
|
@@ -13,14 +13,18 @@ export class ServiceManifestGenerator {
|
|
|
13
13
|
* @returns {Object} Service manifest
|
|
14
14
|
*/
|
|
15
15
|
createManifest(coreInputs, confirmedValues, generatedFiles) {
|
|
16
|
+
console.error('DEBUG: ServiceManifestGenerator.createManifest called');
|
|
17
|
+
console.error('DEBUG: coreInputs.serviceType:', coreInputs.serviceType);
|
|
16
18
|
// Derive explicit top-level feature booleans for quick manifest checks (e.g., D1/KV/R2)
|
|
17
19
|
const features = confirmedValues.features || {};
|
|
20
|
+
console.error('DEBUG: features:', JSON.stringify(features));
|
|
21
|
+
console.error('DEBUG: features.d1:', features.d1);
|
|
18
22
|
return {
|
|
19
23
|
manifestVersion: '1.0.0',
|
|
20
24
|
frameworkVersion: '3.0.0',
|
|
21
25
|
generatedAt: new Date().toISOString(),
|
|
22
26
|
// Top-level feature flags for ConfigurationValidator compatibility
|
|
23
|
-
d1: !!features.d1,
|
|
27
|
+
d1: !!features.d1 || coreInputs.serviceType === 'data-service',
|
|
24
28
|
// kv may be represented via a provider flag (e.g., upstash) - accept either
|
|
25
29
|
kv: !!(features.kv || features.upstash),
|
|
26
30
|
r2: !!features.r2,
|
|
@@ -7,7 +7,7 @@ import { ConfirmationEngine } from '../ConfirmationEngine.js';
|
|
|
7
7
|
import { createPromptHandler } from '../../utils/prompt-handler.js';
|
|
8
8
|
export class ConfirmationHandler {
|
|
9
9
|
constructor(options = {}) {
|
|
10
|
-
this.interactive = options.interactive
|
|
10
|
+
this.interactive = !!options.interactive;
|
|
11
11
|
this.promptHandler = createPromptHandler({
|
|
12
12
|
interactive: this.interactive
|
|
13
13
|
});
|