@vibecheckai/cli 3.0.9 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,553 @@
1
+ /**
2
+ * Preflight Validation Command
3
+ *
4
+ * Validates environment, migrations, storage, and security
5
+ * before deployment. Fails fast on critical issues.
6
+ */
7
+
8
+ "use strict";
9
+
10
+ const { execSync } = require("child_process");
11
+ const fs = require("fs");
12
+ const path = require("path");
13
+ const https = require("https");
14
+ const http = require("http");
15
+
16
+ const c = {
17
+ reset: "\x1b[0m",
18
+ bold: "\x1b[1m",
19
+ dim: "\x1b[2m",
20
+ red: "\x1b[31m",
21
+ green: "\x1b[32m",
22
+ yellow: "\x1b[33m",
23
+ blue: "\x1b[34m",
24
+ cyan: "\x1b[36m",
25
+ };
26
+
27
+ const checks = {
28
+ env: validateEnvironment,
29
+ secrets: validateSecrets,
30
+ database: validateDatabase,
31
+ storage: validateStorage,
32
+ ssl: validateSSL,
33
+ webhooks: validateWebhooks,
34
+ tiers: validateTierEnforcement,
35
+ services: validateExternalServices,
36
+ security: validateSecurityConfig
37
+ };
38
+
39
+ async function runPreflight(args) {
40
+ console.log(`${c.cyan}${c.bold}🚀 Vibecheck Preflight Check${c.reset}\n`);
41
+
42
+ const env = process.env.NODE_ENV || "development";
43
+ console.log(`${c.dim}Environment: ${env.toUpperCase()}${c.reset}\n`);
44
+
45
+ // Determine which checks to run
46
+ const checksToRun = args.includes("--all")
47
+ ? Object.keys(checks)
48
+ : args.filter(arg => arg.startsWith("--")).map(arg => arg.substring(2)).filter(arg => checks[arg]);
49
+
50
+ if (checksToRun.length === 0) {
51
+ checksToRun.push(...Object.keys(checks));
52
+ }
53
+
54
+ console.log(`${c.blue}Running checks:${c.reset} ${checksToRun.join(", ")}\n`);
55
+
56
+ const results = {
57
+ passed: [],
58
+ failed: [],
59
+ warnings: []
60
+ };
61
+
62
+ // Run each check
63
+ for (const checkName of checksToRun) {
64
+ console.log(`${c.yellow}⏳ Running ${checkName} check...${c.reset}`);
65
+
66
+ try {
67
+ const result = await checks[checkName]();
68
+
69
+ if (result.passed) {
70
+ console.log(`${c.green}✅ ${checkName}: PASSED${c.reset}`);
71
+ results.passed.push(checkName);
72
+ if (result.message) {
73
+ console.log(` ${c.dim}${result.message}${c.reset}`);
74
+ }
75
+ } else {
76
+ console.log(`${c.red}❌ ${checkName}: FAILED${c.reset}`);
77
+ console.log(` ${result.error}`);
78
+ results.failed.push({ name: checkName, error: result.error });
79
+ }
80
+ } catch (err) {
81
+ console.log(`${c.red}❌ ${checkName}: ERROR${c.reset}`);
82
+ console.log(` ${err.message}`);
83
+ results.failed.push({ name: checkName, error: err.message });
84
+ }
85
+
86
+ console.log();
87
+ }
88
+
89
+ // Summary
90
+ console.log(`${c.bold}═`.repeat(50));
91
+ console.log(`${c.bold}PREFLIGHT CHECK SUMMARY${c.reset}`);
92
+ console.log(`${c.bold}═`.repeat(50)}`);
93
+ console.log(`${c.green}✅ Passed: ${results.passed.length}${c.reset}`);
94
+ console.log(`${c.red}❌ Failed: ${results.failed.length}${c.reset}`);
95
+ console.log(`${c.yellow}⚠️ Warnings: ${results.warnings.length}${c.reset}\n`);
96
+
97
+ // Fail if any critical checks failed
98
+ const criticalFailures = results.failed.filter(f =>
99
+ ["env", "secrets", "database", "webhooks", "tiers"].includes(f.name)
100
+ );
101
+
102
+ if (criticalFailures.length > 0) {
103
+ console.log(`${c.red}${c.bold}💥 CRITICAL FAILURES DETECTED${c.reset}`);
104
+ console.log("Fix the following issues before deploying:\n");
105
+
106
+ for (const failure of criticalFailures) {
107
+ console.log(`${c.red}• ${failure.name}: ${failure.error}${c.reset}`);
108
+ }
109
+
110
+ console.log(`\n${c.yellow}Run 'vibecheck preflight --fix' to attempt auto-fixes${c.reset}`);
111
+ return 2; // BLOCK
112
+ }
113
+
114
+ if (results.failed.length > 0) {
115
+ console.log(`${c.yellow}⚠️ Some non-critical checks failed${c.reset}`);
116
+ console.log("Review and fix if necessary\n");
117
+ return 1; // WARN
118
+ }
119
+
120
+ console.log(`${c.green}🎉 All checks passed! Ready to deploy.${c.reset}`);
121
+ return 0; // SHIP
122
+ }
123
+
124
+ async function validateEnvironment() {
125
+ const { validateEnvironment } = require("../../shared/env-validator");
126
+
127
+ try {
128
+ const { validated } = validateEnvironment();
129
+
130
+ // Check for production-specific requirements
131
+ if (process.env.NODE_ENV === "production") {
132
+ const prodRequired = [
133
+ "VIBECHECK_ENFORCE_HTTPS",
134
+ "VIBECHECK_CORS_ORIGIN",
135
+ "SENTRY_DSN",
136
+ "VIBECHECK_RATE_LIMIT_STRICT"
137
+ ];
138
+
139
+ for (const key of prodRequired) {
140
+ if (!validated[key]) {
141
+ return { passed: false, error: `Missing production variable: ${key}` };
142
+ }
143
+ }
144
+
145
+ // Check that dangerous dev flags are NOT set
146
+ const forbidden = [
147
+ "VIBECHECK_SKIP_AUTH",
148
+ "VIBECHECK_FAKE_BILLING",
149
+ "VIBECHECK_MOCK_AI"
150
+ ];
151
+
152
+ for (const key of forbidden) {
153
+ if (validated[key] === "true") {
154
+ return { passed: false, error: `Dangerous flag set in production: ${key}` };
155
+ }
156
+ }
157
+ }
158
+
159
+ return { passed: true, message: "All environment variables valid" };
160
+ } catch (err) {
161
+ return { passed: false, error: err.message };
162
+ }
163
+ }
164
+
165
+ async function validateSecrets() {
166
+ const secrets = [
167
+ "VIBECHECK_JWT_SECRET",
168
+ "VIBECHECK_REFRESH_SECRET",
169
+ "VIBECHECK_API_SECRET"
170
+ ];
171
+
172
+ for (const secret of secrets) {
173
+ const value = process.env[secret];
174
+
175
+ if (!value) {
176
+ return { passed: false, error: `Missing secret: ${secret}` };
177
+ }
178
+
179
+ if (value.length < 32) {
180
+ return { passed: false, error: `${secret} too short (min 32 chars)` };
181
+ }
182
+
183
+ // Check for weak patterns
184
+ if (value === "secret" || value === "password" || value.startsWith("test")) {
185
+ return { passed: false, error: `${secret} appears to be weak` };
186
+ }
187
+ }
188
+
189
+ return { passed: true, message: "All secrets are strong" };
190
+ }
191
+
192
+ async function validateDatabase() {
193
+ const databaseUrl = process.env.DATABASE_URL;
194
+
195
+ if (!databaseUrl) {
196
+ return { passed: false, error: "DATABASE_URL not set" };
197
+ }
198
+
199
+ try {
200
+ // Parse database URL to check SSL
201
+ const url = new URL(databaseUrl);
202
+
203
+ if (process.env.NODE_ENV === "production" && url.searchParams.get("sslmode") !== "require") {
204
+ return { passed: false, error: "Database must use SSL in production" };
205
+ }
206
+
207
+ // Test connectivity
208
+ const { Client } = require("pg");
209
+ const client = new Client({ connectionString: databaseUrl });
210
+
211
+ await client.connect();
212
+
213
+ // Check migrations
214
+ const result = await client.query(`
215
+ SELECT COUNT(*) as count FROM schema_migrations
216
+ `);
217
+
218
+ await client.end();
219
+
220
+ const migrationCount = parseInt(result.rows[0].count);
221
+ if (migrationCount === 0) {
222
+ return { passed: false, error: "No migrations found - run 'npm run db:migrate'" };
223
+ }
224
+
225
+ return { passed: true, message: `${migrationCount} migrations applied` };
226
+ } catch (err) {
227
+ return { passed: false, error: `Database connection failed: ${err.message}` };
228
+ }
229
+ }
230
+
231
+ async function validateStorage() {
232
+ const storageType = process.env.STORAGE_TYPE || "local";
233
+
234
+ if (storageType === "s3") {
235
+ const required = ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_S3_BUCKET"];
236
+
237
+ for (const key of required) {
238
+ if (!process.env[key]) {
239
+ return { passed: false, error: `Missing S3 config: ${key}` };
240
+ }
241
+ }
242
+
243
+ // Test S3 connectivity
244
+ try {
245
+ const AWS = require("aws-sdk");
246
+ const s3 = new AWS.S3({
247
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID,
248
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
249
+ region: process.env.AWS_REGION || "us-east-1"
250
+ });
251
+
252
+ await s3.headBucket({ Bucket: process.env.AWS_S3_BUCKET }).promise();
253
+
254
+ return { passed: true, message: "S3 bucket accessible" };
255
+ } catch (err) {
256
+ return { passed: false, error: `S3 access failed: ${err.message}` };
257
+ }
258
+ }
259
+
260
+ // For local storage, check directory exists
261
+ if (storageType === "local") {
262
+ const uploadDir = process.env.UPLOAD_DIR || "./uploads";
263
+
264
+ if (!fs.existsSync(uploadDir)) {
265
+ try {
266
+ fs.mkdirSync(uploadDir, { recursive: true });
267
+ } catch (err) {
268
+ return { passed: false, error: `Cannot create upload directory: ${err.message}` };
269
+ }
270
+ }
271
+
272
+ return { passed: true, message: "Local storage ready" };
273
+ }
274
+
275
+ return { passed: true, message: `${storageType} storage configured` };
276
+ }
277
+
278
+ async function validateSSL() {
279
+ if (process.env.NODE_ENV !== "production") {
280
+ return { passed: true, message: "SSL not required in development" };
281
+ }
282
+
283
+ const url = process.env.VIBECHECK_PUBLIC_URL || "https://api.vibecheck.ai";
284
+
285
+ try {
286
+ return new Promise((resolve) => {
287
+ const req = https.get(url, (res) => {
288
+ const cert = res.socket.getPeerCertificate();
289
+
290
+ if (!cert) {
291
+ resolve({ passed: false, error: "No SSL certificate found" });
292
+ return;
293
+ }
294
+
295
+ // Check certificate expiry
296
+ const now = new Date();
297
+ const expiry = new Date(cert.valid_to);
298
+
299
+ if (expiry < now) {
300
+ resolve({ passed: false, error: "SSL certificate expired" });
301
+ return;
302
+ }
303
+
304
+ // Check expiry within 30 days
305
+ const thirtyDays = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000);
306
+ if (expiry < thirtyDays) {
307
+ resolve({
308
+ passed: false,
309
+ error: `SSL certificate expires soon: ${cert.valid_to}`
310
+ });
311
+ return;
312
+ }
313
+
314
+ resolve({
315
+ passed: true,
316
+ message: `SSL valid until ${cert.valid_to}`
317
+ });
318
+ });
319
+
320
+ req.on("error", (err) => {
321
+ resolve({ passed: false, error: `SSL check failed: ${err.message}` });
322
+ });
323
+
324
+ req.setTimeout(5000, () => {
325
+ req.destroy();
326
+ resolve({ passed: false, error: "SSL check timeout" });
327
+ });
328
+ });
329
+ } catch (err) {
330
+ return { passed: false, error: err.message };
331
+ }
332
+ }
333
+
334
+ async function validateWebhooks() {
335
+ const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
336
+
337
+ if (!webhookSecret) {
338
+ return { passed: false, error: "STRIPE_WEBHOOK_SECRET not set" };
339
+ }
340
+
341
+ if (!webhookSecret.startsWith("whsec_")) {
342
+ return { passed: false, error: "Invalid webhook secret format" };
343
+ }
344
+
345
+ // Test webhook endpoint
346
+ const testPayload = JSON.stringify({
347
+ id: `evt_test_${Date.now()}`,
348
+ type: "test",
349
+ data: { object: { test: true } }
350
+ });
351
+
352
+ const timestamp = Math.floor(Date.now() / 1000);
353
+ const signedPayload = `${timestamp}.${testPayload}`;
354
+
355
+ const crypto = require("crypto");
356
+ const signature = crypto
357
+ .createHmac("sha256", webhookSecret)
358
+ .update(signedPayload)
359
+ .digest("hex");
360
+
361
+ const stripeSignature = `t=${timestamp},v1=${signature}`;
362
+
363
+ try {
364
+ const webhookUrl = process.env.VIBECHECK_WEBHOOK_URL || "http://localhost:3000/webhooks/stripe";
365
+
366
+ return new Promise((resolve) => {
367
+ const postData = testPayload;
368
+
369
+ const req = http.request(webhookUrl, {
370
+ method: "POST",
371
+ headers: {
372
+ "Content-Type": "application/json",
373
+ "Content-Length": Buffer.byteLength(postData),
374
+ "Stripe-Signature": stripeSignature
375
+ }
376
+ }, (res) => {
377
+ let data = "";
378
+ res.on("data", chunk => data += chunk);
379
+ res.on("end", () => {
380
+ if (res.statusCode === 200) {
381
+ resolve({ passed: true, message: "Webhook signature verification working" });
382
+ } else {
383
+ resolve({ passed: false, error: `Webhook returned ${res.statusCode}` });
384
+ }
385
+ });
386
+ });
387
+
388
+ req.on("error", (err) => {
389
+ resolve({ passed: false, error: `Webhook test failed: ${err.message}` });
390
+ });
391
+
392
+ req.setTimeout(5000, () => {
393
+ req.destroy();
394
+ resolve({ passed: false, error: "Webhook test timeout" });
395
+ });
396
+
397
+ req.write(postData);
398
+ req.end();
399
+ });
400
+ } catch (err) {
401
+ return { passed: false, error: err.message };
402
+ }
403
+ }
404
+
405
+ async function validateTierEnforcement() {
406
+ // Check CLI tier enforcement
407
+ try {
408
+ const { enforce } = require("../lib/entitlements-v2");
409
+
410
+ // Test free tier
411
+ const freeResult = await enforce("scan", { silent: true });
412
+ if (!freeResult.allowed) {
413
+ return { passed: false, error: "CLI incorrectly blocks free tier features" };
414
+ }
415
+
416
+ // Test pro tier enforcement
417
+ const proResult = await enforce("prove", { silent: true });
418
+ if (proResult.allowed && process.env.NODE_ENV === "production") {
419
+ return { passed: false, error: "CLI incorrectly allows pro features without auth" };
420
+ }
421
+ } catch (err) {
422
+ return { passed: false, error: `CLI tier check failed: ${err.message}` };
423
+ }
424
+
425
+ // Check API tier enforcement if server is running
426
+ const apiUrl = process.env.VIBECHECK_API_URL || "http://localhost:3000";
427
+
428
+ try {
429
+ // Test without auth
430
+ const response = await fetch(`${apiUrl}/api/v1/ship`, {
431
+ method: "POST",
432
+ headers: { "Content-Type": "application/json" }
433
+ });
434
+
435
+ if (response.status !== 401 && response.status !== 403) {
436
+ return { passed: false, error: "API not enforcing authentication" };
437
+ }
438
+ } catch (err) {
439
+ // Server might not be running - that's OK for preflight
440
+ return { passed: true, message: "API not running - skip check" };
441
+ }
442
+
443
+ return { passed: true, message: "Tier enforcement consistent" };
444
+ }
445
+
446
+ async function validateExternalServices() {
447
+ const services = [
448
+ { name: "License API", url: process.env.VIBECHECK_LICENSE_API },
449
+ { name: "AI Endpoint", url: process.env.VIBECHECK_AI_ENDPOINT }
450
+ ];
451
+
452
+ for (const service of services) {
453
+ if (!service.url) {
454
+ return { passed: false, error: `${service.name} URL not configured` };
455
+ }
456
+
457
+ try {
458
+ const response = await fetch(`${service.url}/health`, {
459
+ method: "GET",
460
+ timeout: 5000
461
+ });
462
+
463
+ if (!response.ok) {
464
+ return { passed: false, error: `${service.name} unhealthy: ${response.status}` };
465
+ }
466
+ } catch (err) {
467
+ return { passed: false, error: `${service.name} unreachable: ${err.message}` };
468
+ }
469
+ }
470
+
471
+ return { passed: true, message: "All external services healthy" };
472
+ }
473
+
474
+ async function validateSecurityConfig() {
475
+ const issues = [];
476
+
477
+ // Check for secure headers in production
478
+ if (process.env.NODE_ENV === "production") {
479
+ if (!process.env.VIBECHECK_ENFORCE_HTTPS) {
480
+ issues.push("HTTPS enforcement not enabled");
481
+ }
482
+
483
+ if (!process.env.VIBECHECK_RATE_LIMIT_STRICT) {
484
+ issues.push("Strict rate limiting not enabled");
485
+ }
486
+
487
+ const corsOrigin = process.env.VIBECHECK_CORS_ORIGIN;
488
+ if (!corsOrigin || corsOrigin.includes("*") || corsOrigin.includes("localhost")) {
489
+ issues.push("CORS origin not properly restricted");
490
+ }
491
+ }
492
+
493
+ // Check for required security headers
494
+ const requiredHeaders = [
495
+ "helmet",
496
+ "cors",
497
+ "rateLimit"
498
+ ];
499
+
500
+ // Check if security middleware is configured
501
+ try {
502
+ const apiFile = fs.readFileSync("./apps/api/src/app.js", "utf8");
503
+
504
+ for (const header of requiredHeaders) {
505
+ if (!apiFile.includes(header)) {
506
+ issues.push(`Security middleware '${header}' not found`);
507
+ }
508
+ }
509
+ } catch (err) {
510
+ // API file might not exist in this context
511
+ }
512
+
513
+ if (issues.length > 0) {
514
+ return { passed: false, error: issues.join("; ") };
515
+ }
516
+
517
+ return { passed: true, message: "Security configuration valid" };
518
+ }
519
+
520
+ function printHelp() {
521
+ console.log(`
522
+ ${c.cyan}${c.bold}vibecheck preflight${c.reset} - Deployment validation
523
+
524
+ ${c.bold}USAGE${c.reset}
525
+ vibecheck preflight [options]
526
+
527
+ ${c.bold}OPTIONS${c.reset}
528
+ --all Run all checks
529
+ --env Validate environment variables
530
+ --secrets Validate secrets strength
531
+ --database Validate database connectivity
532
+ --storage Validate storage configuration
533
+ --ssl Validate SSL certificates
534
+ --webhooks Validate webhook signatures
535
+ --tiers Validate tier enforcement
536
+ --services Validate external services
537
+ --security Validate security configuration
538
+ --fix Attempt to fix issues (where possible)
539
+ --help, -h Show this help
540
+
541
+ ${c.bold}EXAMPLES${c.reset}
542
+ vibecheck preflight --all # Run all checks
543
+ vibecheck preflight --env --secrets # Run specific checks
544
+ vibecheck preflight # Run all checks (default)
545
+
546
+ ${c.bold}EXIT CODES${c.reset}
547
+ 0 = All checks passed
548
+ 1 = Non-critical warnings
549
+ 2 = Critical failures (block deployment)
550
+ `);
551
+ }
552
+
553
+ module.exports = { runPreflight, printHelp };