create-qa-architect 5.0.6 → 5.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/auto-release.yml +49 -0
- package/.github/workflows/dependabot-auto-merge.yml +32 -0
- package/LICENSE +3 -3
- package/README.md +54 -15
- package/docs/ADOPTION-SUMMARY.md +41 -0
- package/docs/ARCHITECTURE-REVIEW.md +67 -0
- package/docs/ARCHITECTURE.md +29 -41
- package/docs/CODE-REVIEW.md +100 -0
- package/docs/PREFLIGHT_REPORT.md +32 -40
- package/docs/REQUIREMENTS.md +148 -0
- package/docs/SECURITY-AUDIT.md +68 -0
- package/docs/TESTING.md +3 -4
- package/docs/test-trace-matrix.md +28 -0
- package/lib/billing-dashboard.html +6 -12
- package/lib/commands/deps.js +245 -0
- package/lib/commands/index.js +25 -0
- package/lib/commands/validate.js +85 -0
- package/lib/error-reporter.js +13 -1
- package/lib/github-api.js +108 -13
- package/lib/license-signing.js +110 -0
- package/lib/license-validator.js +359 -71
- package/lib/licensing.js +343 -111
- package/lib/prelaunch-validator.js +828 -0
- package/lib/quality-tools-generator.js +495 -0
- package/lib/result-types.js +112 -0
- package/lib/security-enhancements.js +1 -1
- package/lib/smart-strategy-generator.js +28 -9
- package/lib/template-loader.js +52 -19
- package/lib/validation/cache-manager.js +36 -6
- package/lib/validation/config-security.js +82 -15
- package/lib/validation/workflow-validation.js +49 -23
- package/package.json +8 -10
- package/scripts/check-test-coverage.sh +46 -0
- package/setup.js +356 -285
- package/templates/QUALITY_TROUBLESHOOTING.md +32 -33
- package/templates/scripts/smart-test-strategy.sh +1 -1
- package/create-saas-monetization.js +0 -1513
package/lib/licensing.js
CHANGED
|
@@ -7,6 +7,15 @@ const fs = require('fs')
|
|
|
7
7
|
const path = require('path')
|
|
8
8
|
const os = require('os')
|
|
9
9
|
const crypto = require('crypto')
|
|
10
|
+
const {
|
|
11
|
+
LICENSE_KEY_PATTERN,
|
|
12
|
+
buildLicensePayload,
|
|
13
|
+
hashEmail,
|
|
14
|
+
signPayload,
|
|
15
|
+
stableStringify,
|
|
16
|
+
verifyPayload,
|
|
17
|
+
loadKeyFromEnv,
|
|
18
|
+
} = require('./license-signing')
|
|
10
19
|
|
|
11
20
|
// License storage locations
|
|
12
21
|
// Support environment variable override for testing (like telemetry/error-reporter)
|
|
@@ -36,17 +45,40 @@ Object.defineProperty(exports, 'LICENSE_FILE', {
|
|
|
36
45
|
* Standardized to use SCREAMING_SNAKE_CASE for both keys and values
|
|
37
46
|
* for consistency with ErrorCategory and other enums in the codebase.
|
|
38
47
|
*
|
|
39
|
-
* Pricing
|
|
48
|
+
* Pricing:
|
|
40
49
|
* - FREE: $0 (Hobby/OSS - capped)
|
|
41
|
-
* - PRO: $
|
|
42
|
-
* - TEAM:
|
|
43
|
-
* - ENTERPRISE:
|
|
50
|
+
* - PRO: $19/mo or $190/yr (Solo Devs/Small Teams)
|
|
51
|
+
* - TEAM: Contact us (Organizations) - coming soon
|
|
52
|
+
* - ENTERPRISE: Contact us (Large Orgs) - coming soon
|
|
44
53
|
*/
|
|
45
|
-
|
|
54
|
+
// DR23 fix: Freeze object to prevent accidental or malicious mutation
|
|
55
|
+
const LICENSE_TIERS = Object.freeze({
|
|
46
56
|
FREE: 'FREE',
|
|
47
57
|
PRO: 'PRO',
|
|
48
58
|
TEAM: 'TEAM',
|
|
49
59
|
ENTERPRISE: 'ENTERPRISE',
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
function normalizeLicenseKey(key) {
|
|
63
|
+
if (typeof key !== 'string') return ''
|
|
64
|
+
return key.trim().toUpperCase()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* DR23 fix: Deep freeze helper to prevent mutation at any level
|
|
69
|
+
*/
|
|
70
|
+
function deepFreeze(obj) {
|
|
71
|
+
Object.freeze(obj)
|
|
72
|
+
Object.getOwnPropertyNames(obj).forEach(prop => {
|
|
73
|
+
if (
|
|
74
|
+
obj[prop] !== null &&
|
|
75
|
+
(typeof obj[prop] === 'object' || typeof obj[prop] === 'function') &&
|
|
76
|
+
!Object.isFrozen(obj[prop])
|
|
77
|
+
) {
|
|
78
|
+
deepFreeze(obj[prop])
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
return obj
|
|
50
82
|
}
|
|
51
83
|
|
|
52
84
|
/**
|
|
@@ -56,8 +88,10 @@ const LICENSE_TIERS = {
|
|
|
56
88
|
* PRO: Solo devs/small teams - unlimited, full features
|
|
57
89
|
* TEAM: Organizations - per-seat, shared policies
|
|
58
90
|
* ENTERPRISE: Large orgs - SSO, compliance, SLA
|
|
91
|
+
*
|
|
92
|
+
* DR23 fix: Deep frozen to prevent accidental or malicious mutation
|
|
59
93
|
*/
|
|
60
|
-
const FEATURES = {
|
|
94
|
+
const FEATURES = deepFreeze({
|
|
61
95
|
[LICENSE_TIERS.FREE]: {
|
|
62
96
|
// Caps (enforced in setup.js and CLI)
|
|
63
97
|
maxPrivateRepos: 1,
|
|
@@ -75,10 +109,26 @@ const FEATURES = {
|
|
|
75
109
|
advancedWorkflows: false,
|
|
76
110
|
notifications: false,
|
|
77
111
|
multiRepo: false,
|
|
112
|
+
// Quality tools
|
|
113
|
+
lighthouseCI: true, // ✅ Basic Lighthouse (no thresholds)
|
|
114
|
+
lighthouseThresholds: false, // ❌ PRO feature - custom thresholds
|
|
115
|
+
bundleSizeLimits: false, // ❌ PRO feature
|
|
116
|
+
axeAccessibility: true, // ✅ Basic a11y testing
|
|
117
|
+
conventionalCommits: true, // ✅ Commit message enforcement
|
|
118
|
+
coverageThresholds: false, // ❌ PRO feature
|
|
119
|
+
// Pre-launch validation
|
|
120
|
+
prelaunchValidation: true, // ✅ Basic prelaunch checks
|
|
121
|
+
seoValidation: true, // ✅ Sitemap, robots, meta tags
|
|
122
|
+
linkValidation: true, // ✅ Broken link detection
|
|
123
|
+
docsValidation: true, // ✅ Documentation completeness
|
|
124
|
+
envValidation: false, // ❌ PRO feature - env vars audit
|
|
78
125
|
roadmap: [
|
|
79
126
|
'✅ ESLint, Prettier, Stylelint configuration',
|
|
80
127
|
'✅ Basic Husky pre-commit hooks',
|
|
81
128
|
'✅ Basic npm dependency monitoring (10 PRs/month)',
|
|
129
|
+
'✅ Lighthouse CI (basic, no thresholds)',
|
|
130
|
+
'✅ axe-core accessibility testing',
|
|
131
|
+
'✅ Conventional commits (commitlint)',
|
|
82
132
|
'⚠️ Limited: 1 private repo, JS/TS only',
|
|
83
133
|
'❌ No security scanning (Gitleaks, ESLint security)',
|
|
84
134
|
'❌ No Smart Test Strategy',
|
|
@@ -102,6 +152,19 @@ const FEATURES = {
|
|
|
102
152
|
advancedWorkflows: false,
|
|
103
153
|
notifications: false,
|
|
104
154
|
multiRepo: false,
|
|
155
|
+
// Quality tools - all enabled
|
|
156
|
+
lighthouseCI: true, // ✅ Full Lighthouse CI
|
|
157
|
+
lighthouseThresholds: true, // ✅ Custom performance/a11y thresholds
|
|
158
|
+
bundleSizeLimits: true, // ✅ Bundle size enforcement
|
|
159
|
+
axeAccessibility: true, // ✅ Advanced a11y testing
|
|
160
|
+
conventionalCommits: true, // ✅ Commit message enforcement
|
|
161
|
+
coverageThresholds: true, // ✅ Coverage threshold enforcement
|
|
162
|
+
// Pre-launch validation - all enabled
|
|
163
|
+
prelaunchValidation: true, // ✅ Full prelaunch suite
|
|
164
|
+
seoValidation: true, // ✅ Sitemap, robots, meta tags
|
|
165
|
+
linkValidation: true, // ✅ Broken link detection
|
|
166
|
+
docsValidation: true, // ✅ Documentation completeness
|
|
167
|
+
envValidation: true, // ✅ Env vars audit
|
|
105
168
|
roadmap: [
|
|
106
169
|
'✅ Unlimited repos and runs',
|
|
107
170
|
'✅ Smart Test Strategy (70% faster pre-push validation)',
|
|
@@ -109,8 +172,11 @@ const FEATURES = {
|
|
|
109
172
|
'✅ TypeScript production protection',
|
|
110
173
|
'✅ Multi-language (Python, Rust, Ruby)',
|
|
111
174
|
'✅ Framework-aware dependency grouping',
|
|
175
|
+
'✅ Lighthouse CI with custom thresholds',
|
|
176
|
+
'✅ Bundle size limits (size-limit)',
|
|
177
|
+
'✅ Coverage threshold enforcement',
|
|
178
|
+
'✅ Pre-launch validation with env vars audit',
|
|
112
179
|
'✅ Email support (24-48h response)',
|
|
113
|
-
'🔄 Performance budgets - Coming Q1 2026',
|
|
114
180
|
],
|
|
115
181
|
},
|
|
116
182
|
[LICENSE_TIERS.TEAM]: {
|
|
@@ -139,6 +205,19 @@ const FEATURES = {
|
|
|
139
205
|
advancedWorkflows: true,
|
|
140
206
|
notifications: true,
|
|
141
207
|
multiRepo: true,
|
|
208
|
+
// Quality tools - all enabled (inherited from PRO)
|
|
209
|
+
lighthouseCI: true,
|
|
210
|
+
lighthouseThresholds: true,
|
|
211
|
+
bundleSizeLimits: true,
|
|
212
|
+
axeAccessibility: true,
|
|
213
|
+
conventionalCommits: true,
|
|
214
|
+
coverageThresholds: true,
|
|
215
|
+
// Pre-launch validation - all enabled (inherited from PRO)
|
|
216
|
+
prelaunchValidation: true,
|
|
217
|
+
seoValidation: true,
|
|
218
|
+
linkValidation: true,
|
|
219
|
+
docsValidation: true,
|
|
220
|
+
envValidation: true,
|
|
142
221
|
roadmap: [
|
|
143
222
|
'✅ All PRO features included',
|
|
144
223
|
'✅ Per-seat licensing (5-seat minimum)',
|
|
@@ -175,6 +254,19 @@ const FEATURES = {
|
|
|
175
254
|
advancedWorkflows: true,
|
|
176
255
|
notifications: true,
|
|
177
256
|
multiRepo: true,
|
|
257
|
+
// Quality tools - all enabled (inherited from TEAM)
|
|
258
|
+
lighthouseCI: true,
|
|
259
|
+
lighthouseThresholds: true,
|
|
260
|
+
bundleSizeLimits: true,
|
|
261
|
+
axeAccessibility: true,
|
|
262
|
+
conventionalCommits: true,
|
|
263
|
+
coverageThresholds: true,
|
|
264
|
+
// Pre-launch validation - all enabled (inherited from TEAM)
|
|
265
|
+
prelaunchValidation: true,
|
|
266
|
+
seoValidation: true,
|
|
267
|
+
linkValidation: true,
|
|
268
|
+
docsValidation: true,
|
|
269
|
+
envValidation: true,
|
|
178
270
|
// Enterprise-specific
|
|
179
271
|
ssoIntegration: true, // SSO/SAML
|
|
180
272
|
scimReady: true,
|
|
@@ -198,13 +290,19 @@ const FEATURES = {
|
|
|
198
290
|
'✅ Optional on-prem license server',
|
|
199
291
|
],
|
|
200
292
|
},
|
|
201
|
-
}
|
|
293
|
+
})
|
|
202
294
|
|
|
203
295
|
/**
|
|
204
296
|
* Check if developer/owner mode is enabled
|
|
205
297
|
* Allows the tool creator to use all features without a license
|
|
298
|
+
* Security: Production mode always enforces license checks
|
|
206
299
|
*/
|
|
207
300
|
function isDeveloperMode() {
|
|
301
|
+
// Security: Production mode never allows developer bypass
|
|
302
|
+
if (process.env.NODE_ENV === 'production') {
|
|
303
|
+
return false
|
|
304
|
+
}
|
|
305
|
+
|
|
208
306
|
// Check environment variable
|
|
209
307
|
if (process.env.QAA_DEVELOPER === 'true') {
|
|
210
308
|
return true
|
|
@@ -216,8 +314,19 @@ function isDeveloperMode() {
|
|
|
216
314
|
if (fs.existsSync(developerMarkerFile)) {
|
|
217
315
|
return true
|
|
218
316
|
}
|
|
219
|
-
} catch {
|
|
220
|
-
//
|
|
317
|
+
} catch (error) {
|
|
318
|
+
// DR10 fix: Halt on ELOOP in production (security issue)
|
|
319
|
+
if (error?.code === 'ELOOP') {
|
|
320
|
+
const message =
|
|
321
|
+
'Symlink loop detected in license directory - possible security issue'
|
|
322
|
+
if (process.env.NODE_ENV === 'production') {
|
|
323
|
+
throw new Error(message)
|
|
324
|
+
} else {
|
|
325
|
+
console.warn(`⚠️ ${message}`)
|
|
326
|
+
}
|
|
327
|
+
} else if (process.env.DEBUG && error?.code !== 'ENOENT') {
|
|
328
|
+
console.warn(`⚠️ Developer mode check failed: ${error.message}`)
|
|
329
|
+
}
|
|
221
330
|
}
|
|
222
331
|
|
|
223
332
|
return false
|
|
@@ -248,23 +357,24 @@ function getLicenseInfo() {
|
|
|
248
357
|
return { tier: LICENSE_TIERS.FREE, valid: true }
|
|
249
358
|
}
|
|
250
359
|
|
|
251
|
-
|
|
252
|
-
|
|
360
|
+
const licenseKey = normalizeLicenseKey(
|
|
361
|
+
localLicense.licenseKey || localLicense.key
|
|
362
|
+
)
|
|
363
|
+
if (!licenseKey || !localLicense.email) {
|
|
253
364
|
return {
|
|
254
365
|
tier: LICENSE_TIERS.FREE,
|
|
255
366
|
valid: true,
|
|
256
|
-
error:
|
|
257
|
-
'License signature verification failed - license may have been tampered with',
|
|
367
|
+
error: 'Invalid license format',
|
|
258
368
|
}
|
|
259
369
|
}
|
|
260
370
|
|
|
261
|
-
// Check
|
|
262
|
-
|
|
263
|
-
if (!licenseKey || !localLicense.email) {
|
|
371
|
+
// Check if license is valid
|
|
372
|
+
if (!localLicense.valid) {
|
|
264
373
|
return {
|
|
265
374
|
tier: LICENSE_TIERS.FREE,
|
|
266
375
|
valid: true,
|
|
267
|
-
error:
|
|
376
|
+
error:
|
|
377
|
+
'License signature verification failed - license may have been tampered with',
|
|
268
378
|
}
|
|
269
379
|
}
|
|
270
380
|
|
|
@@ -309,17 +419,16 @@ function getLicenseInfo() {
|
|
|
309
419
|
* Supports both legacy format and new Stripe-generated keys
|
|
310
420
|
*/
|
|
311
421
|
function validateLicenseKey(key, tier) {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
if (stripeFormat.test(key)) {
|
|
422
|
+
const normalizedKey = normalizeLicenseKey(key)
|
|
423
|
+
// TD15 fix: Use shared constant for license key pattern
|
|
424
|
+
if (LICENSE_KEY_PATTERN.test(normalizedKey)) {
|
|
316
425
|
// Stripe-generated key - always valid if properly formatted
|
|
317
426
|
return true
|
|
318
427
|
}
|
|
319
428
|
|
|
320
429
|
// Legacy format validation for backward compatibility
|
|
321
430
|
const expectedPrefix = `QAA-${tier.toUpperCase()}-`
|
|
322
|
-
return
|
|
431
|
+
return normalizedKey.startsWith(expectedPrefix) && normalizedKey.length > 20
|
|
323
432
|
}
|
|
324
433
|
|
|
325
434
|
/**
|
|
@@ -327,19 +436,27 @@ function validateLicenseKey(key, tier) {
|
|
|
327
436
|
*/
|
|
328
437
|
function verifyLicenseSignature(payload, signature) {
|
|
329
438
|
try {
|
|
330
|
-
const
|
|
331
|
-
process.env.
|
|
332
|
-
|
|
333
|
-
.createHmac('sha256', secret)
|
|
334
|
-
.update(JSON.stringify(payload))
|
|
335
|
-
.digest('hex')
|
|
336
|
-
|
|
337
|
-
return crypto.timingSafeEqual(
|
|
338
|
-
Buffer.from(signature),
|
|
339
|
-
Buffer.from(expectedSignature)
|
|
439
|
+
const publicKey = loadKeyFromEnv(
|
|
440
|
+
process.env.QAA_LICENSE_PUBLIC_KEY,
|
|
441
|
+
process.env.QAA_LICENSE_PUBLIC_KEY_PATH
|
|
340
442
|
)
|
|
341
|
-
|
|
342
|
-
|
|
443
|
+
if (!publicKey) {
|
|
444
|
+
// TD12 fix: Log warning when public key is missing in non-dev mode
|
|
445
|
+
// Security: Production mode never allows developer bypass
|
|
446
|
+
const isDevMode =
|
|
447
|
+
process.env.NODE_ENV !== 'production' &&
|
|
448
|
+
process.env.QAA_DEVELOPER === 'true'
|
|
449
|
+
if (!isDevMode) {
|
|
450
|
+
console.warn(
|
|
451
|
+
'⚠️ License public key not configured - signature verification skipped'
|
|
452
|
+
)
|
|
453
|
+
}
|
|
454
|
+
return isDevMode
|
|
455
|
+
}
|
|
456
|
+
return verifyPayload(payload, signature, publicKey)
|
|
457
|
+
} catch (error) {
|
|
458
|
+
// TD12 fix: Log verification failures instead of silently returning false
|
|
459
|
+
console.warn(`⚠️ Signature verification failed: ${error.message}`)
|
|
343
460
|
return false
|
|
344
461
|
}
|
|
345
462
|
}
|
|
@@ -376,7 +493,6 @@ function getSupportedLanguages() {
|
|
|
376
493
|
*/
|
|
377
494
|
function showUpgradeMessage(feature) {
|
|
378
495
|
const license = getLicenseInfo()
|
|
379
|
-
const _tierFeatures = FEATURES[license.tier] || FEATURES[LICENSE_TIERS.FREE]
|
|
380
496
|
|
|
381
497
|
console.log(`\n🔒 ${feature} is a premium feature`)
|
|
382
498
|
console.log(`📊 Current license: ${license.tier.toUpperCase()}`)
|
|
@@ -384,7 +500,7 @@ function showUpgradeMessage(feature) {
|
|
|
384
500
|
if (license.tier === LICENSE_TIERS.FREE) {
|
|
385
501
|
console.log('\n🚀 Upgrade to PRO')
|
|
386
502
|
console.log('')
|
|
387
|
-
console.log(' 💰 $
|
|
503
|
+
console.log(' 💰 $19/month or $190/year (save $38)')
|
|
388
504
|
console.log('')
|
|
389
505
|
console.log(' ✅ Unlimited repos, LOC, and runs')
|
|
390
506
|
console.log(' ✅ Smart Test Strategy (70% faster pre-push)')
|
|
@@ -396,16 +512,14 @@ function showUpgradeMessage(feature) {
|
|
|
396
512
|
console.log('')
|
|
397
513
|
console.log(' 🎁 Start 14-day free trial - no credit card required')
|
|
398
514
|
console.log('')
|
|
399
|
-
console.log('🚀 Upgrade: https://vibebuildlab.com/
|
|
515
|
+
console.log('🚀 Upgrade: https://vibebuildlab.com/qa-architect')
|
|
400
516
|
console.log(
|
|
401
517
|
'🔑 Activate: npx create-qa-architect@latest --activate-license'
|
|
402
518
|
)
|
|
403
519
|
} else if (license.tier === LICENSE_TIERS.PRO) {
|
|
404
520
|
console.log('\n👥 Upgrade to TEAM')
|
|
405
521
|
console.log('')
|
|
406
|
-
console.log(
|
|
407
|
-
' 💰 $15/user/month (5-seat min) or $150/user/year (save $30/user)'
|
|
408
|
-
)
|
|
522
|
+
console.log(' 💰 Contact us for Team pricing')
|
|
409
523
|
console.log('')
|
|
410
524
|
console.log(' ✅ All PRO features included')
|
|
411
525
|
console.log(' ✅ Per-seat licensing for your org')
|
|
@@ -414,9 +528,9 @@ function showUpgradeMessage(feature) {
|
|
|
414
528
|
console.log(' ✅ Slack/email alerts for failures')
|
|
415
529
|
console.log(' ✅ Priority support (business hours)')
|
|
416
530
|
console.log('')
|
|
417
|
-
console.log('👥 Upgrade: https://vibebuildlab.com/
|
|
531
|
+
console.log('👥 Upgrade: https://vibebuildlab.com/qa-architect')
|
|
418
532
|
} else if (license.tier === LICENSE_TIERS.TEAM) {
|
|
419
|
-
console.log('\n🏢 Upgrade to ENTERPRISE -
|
|
533
|
+
console.log('\n🏢 Upgrade to ENTERPRISE - Contact us for pricing')
|
|
420
534
|
console.log('')
|
|
421
535
|
console.log(' ✅ All TEAM features included')
|
|
422
536
|
console.log(' ✅ SSO/SAML integration')
|
|
@@ -433,19 +547,52 @@ function showUpgradeMessage(feature) {
|
|
|
433
547
|
*/
|
|
434
548
|
function saveLicense(tier, key, email, expires = null) {
|
|
435
549
|
try {
|
|
550
|
+
// DR24 fix: Validate tier is a valid LICENSE_TIERS value
|
|
551
|
+
const validTiers = Object.values(LICENSE_TIERS)
|
|
552
|
+
if (!validTiers.includes(tier)) {
|
|
553
|
+
return {
|
|
554
|
+
success: false,
|
|
555
|
+
error: `Invalid tier "${tier}". Must be one of: ${validTiers.join(', ')}`,
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
436
559
|
const licenseDir = getLicenseDir()
|
|
437
560
|
const licenseFile = getLicenseFile()
|
|
561
|
+
const normalizedKey = normalizeLicenseKey(key)
|
|
562
|
+
const privateKey = loadKeyFromEnv(
|
|
563
|
+
process.env.LICENSE_REGISTRY_PRIVATE_KEY,
|
|
564
|
+
process.env.LICENSE_REGISTRY_PRIVATE_KEY_PATH
|
|
565
|
+
)
|
|
438
566
|
|
|
439
567
|
if (!fs.existsSync(licenseDir)) {
|
|
440
568
|
fs.mkdirSync(licenseDir, { recursive: true })
|
|
441
569
|
}
|
|
442
570
|
|
|
571
|
+
if (!privateKey) {
|
|
572
|
+
return {
|
|
573
|
+
success: false,
|
|
574
|
+
error:
|
|
575
|
+
'LICENSE_REGISTRY_PRIVATE_KEY or LICENSE_REGISTRY_PRIVATE_KEY_PATH is required to save a signed license',
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const payload = buildLicensePayload({
|
|
580
|
+
licenseKey: normalizedKey,
|
|
581
|
+
tier,
|
|
582
|
+
isFounder: false,
|
|
583
|
+
emailHash: hashEmail(email),
|
|
584
|
+
issued: new Date().toISOString(),
|
|
585
|
+
})
|
|
586
|
+
const signature = signPayload(payload, privateKey)
|
|
587
|
+
|
|
443
588
|
const licenseData = {
|
|
444
589
|
tier,
|
|
445
|
-
|
|
590
|
+
licenseKey: normalizedKey,
|
|
446
591
|
email,
|
|
447
592
|
expires,
|
|
448
593
|
activated: new Date().toISOString(),
|
|
594
|
+
payload,
|
|
595
|
+
signature,
|
|
449
596
|
}
|
|
450
597
|
|
|
451
598
|
fs.writeFileSync(licenseFile, JSON.stringify(licenseData, null, 2))
|
|
@@ -460,8 +607,18 @@ function saveLicense(tier, key, email, expires = null) {
|
|
|
460
607
|
*/
|
|
461
608
|
function saveLicenseWithSignature(tier, key, email, validation) {
|
|
462
609
|
try {
|
|
610
|
+
// DR24 fix: Validate tier is a valid LICENSE_TIERS value
|
|
611
|
+
const validTiers = Object.values(LICENSE_TIERS)
|
|
612
|
+
if (!validTiers.includes(tier)) {
|
|
613
|
+
return {
|
|
614
|
+
success: false,
|
|
615
|
+
error: `Invalid tier "${tier}". Must be one of: ${validTiers.join(', ')}`,
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
463
619
|
const licenseDir = getLicenseDir()
|
|
464
620
|
const licenseFile = getLicenseFile()
|
|
621
|
+
const normalizedKey = normalizeLicenseKey(key)
|
|
465
622
|
|
|
466
623
|
if (!fs.existsSync(licenseDir)) {
|
|
467
624
|
fs.mkdirSync(licenseDir, { recursive: true })
|
|
@@ -469,7 +626,7 @@ function saveLicenseWithSignature(tier, key, email, validation) {
|
|
|
469
626
|
|
|
470
627
|
const licenseData = {
|
|
471
628
|
tier,
|
|
472
|
-
licenseKey:
|
|
629
|
+
licenseKey: normalizedKey, // ✅ Changed from 'key' to 'licenseKey' to match StripeIntegration
|
|
473
630
|
email,
|
|
474
631
|
expires: validation.expires,
|
|
475
632
|
activated: new Date().toISOString(),
|
|
@@ -537,11 +694,16 @@ async function addLegitimateKey(
|
|
|
537
694
|
purchaseEmail = null
|
|
538
695
|
) {
|
|
539
696
|
try {
|
|
697
|
+
const normalizedKey = normalizeLicenseKey(licenseKey)
|
|
540
698
|
// Use the same license directory as the CLI
|
|
541
699
|
const licenseDir =
|
|
542
700
|
process.env.QAA_LICENSE_DIR ||
|
|
543
701
|
path.join(os.homedir(), '.create-qa-architect')
|
|
544
702
|
const legitimateDBFile = path.join(licenseDir, 'legitimate-licenses.json')
|
|
703
|
+
const privateKey = loadKeyFromEnv(
|
|
704
|
+
process.env.LICENSE_REGISTRY_PRIVATE_KEY,
|
|
705
|
+
process.env.LICENSE_REGISTRY_PRIVATE_KEY_PATH
|
|
706
|
+
)
|
|
545
707
|
|
|
546
708
|
// Ensure directory exists
|
|
547
709
|
if (!fs.existsSync(licenseDir)) {
|
|
@@ -553,43 +715,72 @@ async function addLegitimateKey(
|
|
|
553
715
|
if (fs.existsSync(legitimateDBFile)) {
|
|
554
716
|
try {
|
|
555
717
|
database = JSON.parse(fs.readFileSync(legitimateDBFile, 'utf8'))
|
|
556
|
-
} catch {
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
718
|
+
} catch (parseError) {
|
|
719
|
+
// Silent failure fix: Backup corrupt file before overwriting
|
|
720
|
+
const backupPath = `${legitimateDBFile}.corrupted.${Date.now()}`
|
|
721
|
+
try {
|
|
722
|
+
fs.copyFileSync(legitimateDBFile, backupPath)
|
|
723
|
+
console.error(
|
|
724
|
+
`Warning: Could not parse existing database. Backed up to ${backupPath}`
|
|
725
|
+
)
|
|
726
|
+
} catch {
|
|
727
|
+
console.error(
|
|
728
|
+
'Warning: Could not parse or backup existing database, creating new one'
|
|
729
|
+
)
|
|
730
|
+
}
|
|
731
|
+
console.error(`Parse error: ${parseError.message}`)
|
|
560
732
|
}
|
|
561
733
|
}
|
|
562
734
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
description: 'Legitimate license database - populated by admin/webhook',
|
|
735
|
+
if (!privateKey) {
|
|
736
|
+
return {
|
|
737
|
+
success: false,
|
|
738
|
+
error:
|
|
739
|
+
'LICENSE_REGISTRY_PRIVATE_KEY or LICENSE_REGISTRY_PRIVATE_KEY_PATH is required to add legitimate keys',
|
|
569
740
|
}
|
|
570
741
|
}
|
|
571
742
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
743
|
+
const issued = new Date().toISOString()
|
|
744
|
+
const emailHash = hashEmail(purchaseEmail)
|
|
745
|
+
const payload = buildLicensePayload({
|
|
746
|
+
licenseKey: normalizedKey,
|
|
575
747
|
tier,
|
|
576
748
|
isFounder,
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
749
|
+
emailHash,
|
|
750
|
+
issued,
|
|
751
|
+
})
|
|
752
|
+
const signature = signPayload(payload, privateKey)
|
|
753
|
+
|
|
754
|
+
const { _metadata, ...existingLicenses } = database
|
|
755
|
+
const licenses = {
|
|
756
|
+
...existingLicenses,
|
|
757
|
+
[normalizedKey]: {
|
|
758
|
+
tier,
|
|
759
|
+
isFounder,
|
|
760
|
+
issued,
|
|
761
|
+
emailHash,
|
|
762
|
+
signature,
|
|
763
|
+
keyId: process.env.LICENSE_REGISTRY_KEY_ID || 'default',
|
|
764
|
+
},
|
|
580
765
|
}
|
|
581
766
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
database._metadata.totalLicenses = Object.keys(database).length - 1 // Exclude metadata
|
|
585
|
-
|
|
586
|
-
// Calculate SHA256 checksum for integrity verification (MANDATORY)
|
|
587
|
-
const { _metadata, ...licensesOnly } = database
|
|
588
|
-
const sha256 = crypto
|
|
767
|
+
const registrySignature = signPayload(licenses, privateKey)
|
|
768
|
+
const hash = crypto
|
|
589
769
|
.createHash('sha256')
|
|
590
|
-
.update(
|
|
770
|
+
.update(stableStringify(licenses))
|
|
591
771
|
.digest('hex')
|
|
592
|
-
|
|
772
|
+
const metadata = {
|
|
773
|
+
version: '1.0',
|
|
774
|
+
created: _metadata?.created || new Date().toISOString(),
|
|
775
|
+
lastUpdate: new Date().toISOString(),
|
|
776
|
+
description: 'Legitimate license database - populated by admin/webhook',
|
|
777
|
+
algorithm: 'ed25519',
|
|
778
|
+
keyId: process.env.LICENSE_REGISTRY_KEY_ID || 'default',
|
|
779
|
+
registrySignature,
|
|
780
|
+
hash,
|
|
781
|
+
totalLicenses: Object.keys(licenses).length,
|
|
782
|
+
}
|
|
783
|
+
database = { _metadata: metadata, ...licenses }
|
|
593
784
|
|
|
594
785
|
// Save database
|
|
595
786
|
fs.writeFileSync(legitimateDBFile, JSON.stringify(database, null, 2))
|
|
@@ -610,60 +801,60 @@ async function addLegitimateKey(
|
|
|
610
801
|
|
|
611
802
|
/**
|
|
612
803
|
* Interactive license activation prompt
|
|
804
|
+
* DR27 fix: Converted from callback-based readline to async/await pattern
|
|
613
805
|
*/
|
|
614
806
|
async function promptLicenseActivation() {
|
|
615
|
-
const readline = require('readline')
|
|
807
|
+
const readline = require('readline/promises')
|
|
616
808
|
|
|
617
809
|
const rl = readline.createInterface({
|
|
618
810
|
input: process.stdin,
|
|
619
811
|
output: process.stdout,
|
|
620
812
|
})
|
|
621
813
|
|
|
622
|
-
|
|
814
|
+
try {
|
|
623
815
|
console.log('\n🔑 License Activation')
|
|
624
816
|
console.log(
|
|
625
817
|
'Enter your license key from the purchase confirmation email.\n'
|
|
626
818
|
)
|
|
627
819
|
|
|
628
|
-
rl.question(
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
rl.close()
|
|
632
|
-
resolve({ success: false })
|
|
633
|
-
return
|
|
634
|
-
}
|
|
820
|
+
const licenseKey = await rl.question(
|
|
821
|
+
'License key (QAA-XXXX-XXXX-XXXX-XXXX): '
|
|
822
|
+
)
|
|
635
823
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
return
|
|
642
|
-
}
|
|
824
|
+
if (!licenseKey.trim()) {
|
|
825
|
+
console.log('❌ License key required')
|
|
826
|
+
rl.close()
|
|
827
|
+
return { success: false }
|
|
828
|
+
}
|
|
643
829
|
|
|
644
|
-
|
|
830
|
+
const email = await rl.question('Email address: ')
|
|
645
831
|
|
|
646
|
-
|
|
832
|
+
if (!email.trim()) {
|
|
833
|
+
console.log('❌ Email address required')
|
|
834
|
+
rl.close()
|
|
835
|
+
return { success: false }
|
|
836
|
+
}
|
|
647
837
|
|
|
648
|
-
|
|
649
|
-
!result.success &&
|
|
650
|
-
result.error &&
|
|
651
|
-
result.error.includes('not found')
|
|
652
|
-
) {
|
|
653
|
-
console.log('\n📞 License activation assistance:')
|
|
654
|
-
console.log(
|
|
655
|
-
' If you purchased this license, please contact support at:'
|
|
656
|
-
)
|
|
657
|
-
console.log(' Email: support@vibebuildlab.com')
|
|
658
|
-
console.log(
|
|
659
|
-
' Include your license key and purchase email for verification.'
|
|
660
|
-
)
|
|
661
|
-
}
|
|
838
|
+
rl.close()
|
|
662
839
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
840
|
+
const result = await activateLicense(licenseKey.trim(), email.trim())
|
|
841
|
+
|
|
842
|
+
if (!result.success && result.error && result.error.includes('not found')) {
|
|
843
|
+
console.log('\n📞 License activation assistance:')
|
|
844
|
+
console.log(
|
|
845
|
+
' If you purchased this license, please contact support at:'
|
|
846
|
+
)
|
|
847
|
+
console.log(' Email: support@vibebuildlab.com')
|
|
848
|
+
console.log(
|
|
849
|
+
' Include your license key and purchase email for verification.'
|
|
850
|
+
)
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
return result
|
|
854
|
+
} catch (error) {
|
|
855
|
+
rl.close()
|
|
856
|
+
return { success: false, error: error.message }
|
|
857
|
+
}
|
|
667
858
|
}
|
|
668
859
|
|
|
669
860
|
/**
|
|
@@ -745,14 +936,53 @@ function loadUsage() {
|
|
|
745
936
|
|
|
746
937
|
return data
|
|
747
938
|
}
|
|
748
|
-
} catch {
|
|
749
|
-
//
|
|
939
|
+
} catch (error) {
|
|
940
|
+
// DR8 fix: Prevent quota bypass through file corruption
|
|
941
|
+
if (error instanceof SyntaxError) {
|
|
942
|
+
const usageFile = getUsageFile()
|
|
943
|
+
console.warn('⚠️ Usage tracking file corrupted')
|
|
944
|
+
console.warn(' Creating backup and resetting to current month start')
|
|
945
|
+
|
|
946
|
+
// Backup corrupted file for forensics
|
|
947
|
+
const backupPath = `${usageFile}.corrupted.${Date.now()}`
|
|
948
|
+
try {
|
|
949
|
+
fs.copyFileSync(usageFile, backupPath)
|
|
950
|
+
console.warn(` Backup saved: ${backupPath}`)
|
|
951
|
+
} catch (_backupError) {
|
|
952
|
+
// Ignore backup failures
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// DR8 fix: For FREE tier, reset to max usage to prevent quota bypass
|
|
956
|
+
const license = getLicenseInfo()
|
|
957
|
+
if (license.tier === LICENSE_TIERS.FREE) {
|
|
958
|
+
console.warn(
|
|
959
|
+
' ⚠️ FREE tier: Starting with maximum usage for security'
|
|
960
|
+
)
|
|
961
|
+
const caps = FEATURES[LICENSE_TIERS.FREE]
|
|
962
|
+
return {
|
|
963
|
+
month: getCurrentMonth(),
|
|
964
|
+
prePushRuns: caps.maxPrePushRunsPerMonth,
|
|
965
|
+
dependencyPRs: caps.maxDependencyPRsPerMonth,
|
|
966
|
+
repos: [],
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
} else if (process.env.DEBUG && error?.code !== 'ENOENT') {
|
|
970
|
+
console.warn(`⚠️ Could not read usage file: ${error.message}`)
|
|
971
|
+
}
|
|
750
972
|
}
|
|
751
973
|
|
|
752
974
|
// Default usage data
|
|
975
|
+
return getDefaultUsage()
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function getCurrentMonth() {
|
|
753
979
|
const now = new Date()
|
|
980
|
+
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function getDefaultUsage() {
|
|
754
984
|
return {
|
|
755
|
-
month:
|
|
985
|
+
month: getCurrentMonth(),
|
|
756
986
|
prePushRuns: 0,
|
|
757
987
|
dependencyPRs: 0,
|
|
758
988
|
repos: [],
|
|
@@ -770,7 +1000,9 @@ function saveUsage(usage) {
|
|
|
770
1000
|
}
|
|
771
1001
|
fs.writeFileSync(getUsageFile(), JSON.stringify(usage, null, 2))
|
|
772
1002
|
return true
|
|
773
|
-
} catch {
|
|
1003
|
+
} catch (error) {
|
|
1004
|
+
// TD12 fix: Log errors saving usage data instead of silently failing
|
|
1005
|
+
console.warn(`⚠️ Failed to save usage data: ${error.message}`)
|
|
774
1006
|
return false
|
|
775
1007
|
}
|
|
776
1008
|
}
|
|
@@ -958,7 +1190,7 @@ function showLicenseStatus() {
|
|
|
958
1190
|
// Show upgrade path
|
|
959
1191
|
if (license.tier === LICENSE_TIERS.FREE) {
|
|
960
1192
|
console.log('\n💡 Upgrade to PRO for unlimited access + security scanning')
|
|
961
|
-
console.log(' → https://vibebuildlab.com/
|
|
1193
|
+
console.log(' → https://vibebuildlab.com/qa-architect')
|
|
962
1194
|
}
|
|
963
1195
|
}
|
|
964
1196
|
|