create-qa-architect 5.0.7 ā 5.4.3
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/quality.yml +11 -11
- package/.github/workflows/shell-ci.yml.example +82 -0
- package/.github/workflows/shell-quality.yml.example +148 -0
- package/README.md +165 -12
- package/config/shell-ci.yml +82 -0
- package/config/shell-quality.yml +148 -0
- package/docs/ADOPTION-SUMMARY.md +41 -0
- package/docs/ARCHITECTURE-REVIEW.md +67 -0
- package/docs/ARCHITECTURE.md +29 -45
- package/docs/CI-COST-ANALYSIS.md +323 -0
- package/docs/CODE-REVIEW.md +100 -0
- package/docs/REQUIREMENTS.md +148 -0
- package/docs/SECURITY-AUDIT.md +68 -0
- package/docs/test-trace-matrix.md +28 -0
- package/eslint.config.cjs +2 -0
- package/lib/commands/analyze-ci.js +616 -0
- package/lib/commands/deps.js +293 -0
- package/lib/commands/index.js +29 -0
- package/lib/commands/validate.js +85 -0
- package/lib/config-validator.js +28 -45
- package/lib/error-reporter.js +14 -2
- package/lib/github-api.js +138 -13
- package/lib/license-signing.js +125 -0
- package/lib/license-validator.js +359 -71
- package/lib/licensing.js +434 -106
- package/lib/package-utils.js +9 -9
- package/lib/prelaunch-validator.js +828 -0
- package/lib/project-maturity.js +58 -6
- 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 +46 -10
- package/lib/telemetry.js +1 -1
- package/lib/template-loader.js +52 -19
- package/lib/ui-helpers.js +1 -1
- package/lib/validation/cache-manager.js +36 -6
- package/lib/validation/config-security.js +100 -33
- package/lib/validation/index.js +68 -97
- package/lib/validation/workflow-validation.js +28 -7
- package/package.json +4 -6
- package/scripts/check-test-coverage.sh +46 -0
- package/scripts/validate-claude-md.js +80 -0
- package/setup.js +923 -301
- 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)
|
|
@@ -42,11 +51,34 @@ Object.defineProperty(exports, 'LICENSE_FILE', {
|
|
|
42
51
|
* - TEAM: Contact us (Organizations) - coming soon
|
|
43
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,28 @@ 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
|
|
125
|
+
// CI/CD optimization
|
|
126
|
+
ciCostAnalysis: false, // ā PRO feature - GitHub Actions cost analysis
|
|
78
127
|
roadmap: [
|
|
79
128
|
'ā
ESLint, Prettier, Stylelint configuration',
|
|
80
129
|
'ā
Basic Husky pre-commit hooks',
|
|
81
130
|
'ā
Basic npm dependency monitoring (10 PRs/month)',
|
|
131
|
+
'ā
Lighthouse CI (basic, no thresholds)',
|
|
132
|
+
'ā
axe-core accessibility testing',
|
|
133
|
+
'ā
Conventional commits (commitlint)',
|
|
82
134
|
'ā ļø Limited: 1 private repo, JS/TS only',
|
|
83
135
|
'ā No security scanning (Gitleaks, ESLint security)',
|
|
84
136
|
'ā No Smart Test Strategy',
|
|
@@ -102,6 +154,21 @@ const FEATURES = {
|
|
|
102
154
|
advancedWorkflows: false,
|
|
103
155
|
notifications: false,
|
|
104
156
|
multiRepo: false,
|
|
157
|
+
// Quality tools - all enabled
|
|
158
|
+
lighthouseCI: true, // ā
Full Lighthouse CI
|
|
159
|
+
lighthouseThresholds: true, // ā
Custom performance/a11y thresholds
|
|
160
|
+
bundleSizeLimits: true, // ā
Bundle size enforcement
|
|
161
|
+
axeAccessibility: true, // ā
Advanced a11y testing
|
|
162
|
+
conventionalCommits: true, // ā
Commit message enforcement
|
|
163
|
+
coverageThresholds: true, // ā
Coverage threshold enforcement
|
|
164
|
+
// Pre-launch validation - all enabled
|
|
165
|
+
prelaunchValidation: true, // ā
Full prelaunch suite
|
|
166
|
+
seoValidation: true, // ā
Sitemap, robots, meta tags
|
|
167
|
+
linkValidation: true, // ā
Broken link detection
|
|
168
|
+
docsValidation: true, // ā
Documentation completeness
|
|
169
|
+
envValidation: true, // ā
Env vars audit
|
|
170
|
+
// CI/CD optimization
|
|
171
|
+
ciCostAnalysis: true, // ā
GitHub Actions cost analysis
|
|
105
172
|
roadmap: [
|
|
106
173
|
'ā
Unlimited repos and runs',
|
|
107
174
|
'ā
Smart Test Strategy (70% faster pre-push validation)',
|
|
@@ -109,8 +176,11 @@ const FEATURES = {
|
|
|
109
176
|
'ā
TypeScript production protection',
|
|
110
177
|
'ā
Multi-language (Python, Rust, Ruby)',
|
|
111
178
|
'ā
Framework-aware dependency grouping',
|
|
179
|
+
'ā
Lighthouse CI with custom thresholds',
|
|
180
|
+
'ā
Bundle size limits (size-limit)',
|
|
181
|
+
'ā
Coverage threshold enforcement',
|
|
182
|
+
'ā
Pre-launch validation with env vars audit',
|
|
112
183
|
'ā
Email support (24-48h response)',
|
|
113
|
-
'š Performance budgets - Coming Q1 2026',
|
|
114
184
|
],
|
|
115
185
|
},
|
|
116
186
|
[LICENSE_TIERS.TEAM]: {
|
|
@@ -139,6 +209,21 @@ const FEATURES = {
|
|
|
139
209
|
advancedWorkflows: true,
|
|
140
210
|
notifications: true,
|
|
141
211
|
multiRepo: true,
|
|
212
|
+
// Quality tools - all enabled (inherited from PRO)
|
|
213
|
+
lighthouseCI: true,
|
|
214
|
+
lighthouseThresholds: true,
|
|
215
|
+
bundleSizeLimits: true,
|
|
216
|
+
axeAccessibility: true,
|
|
217
|
+
conventionalCommits: true,
|
|
218
|
+
coverageThresholds: true,
|
|
219
|
+
// Pre-launch validation - all enabled (inherited from PRO)
|
|
220
|
+
prelaunchValidation: true,
|
|
221
|
+
seoValidation: true,
|
|
222
|
+
linkValidation: true,
|
|
223
|
+
docsValidation: true,
|
|
224
|
+
envValidation: true,
|
|
225
|
+
// CI/CD optimization - inherited from PRO
|
|
226
|
+
ciCostAnalysis: true,
|
|
142
227
|
roadmap: [
|
|
143
228
|
'ā
All PRO features included',
|
|
144
229
|
'ā
Per-seat licensing (5-seat minimum)',
|
|
@@ -175,6 +260,21 @@ const FEATURES = {
|
|
|
175
260
|
advancedWorkflows: true,
|
|
176
261
|
notifications: true,
|
|
177
262
|
multiRepo: true,
|
|
263
|
+
// Quality tools - all enabled (inherited from TEAM)
|
|
264
|
+
lighthouseCI: true,
|
|
265
|
+
lighthouseThresholds: true,
|
|
266
|
+
bundleSizeLimits: true,
|
|
267
|
+
axeAccessibility: true,
|
|
268
|
+
conventionalCommits: true,
|
|
269
|
+
coverageThresholds: true,
|
|
270
|
+
// Pre-launch validation - all enabled (inherited from TEAM)
|
|
271
|
+
prelaunchValidation: true,
|
|
272
|
+
seoValidation: true,
|
|
273
|
+
linkValidation: true,
|
|
274
|
+
docsValidation: true,
|
|
275
|
+
envValidation: true,
|
|
276
|
+
// CI/CD optimization - inherited from TEAM
|
|
277
|
+
ciCostAnalysis: true,
|
|
178
278
|
// Enterprise-specific
|
|
179
279
|
ssoIntegration: true, // SSO/SAML
|
|
180
280
|
scimReady: true,
|
|
@@ -198,13 +298,19 @@ const FEATURES = {
|
|
|
198
298
|
'ā
Optional on-prem license server',
|
|
199
299
|
],
|
|
200
300
|
},
|
|
201
|
-
}
|
|
301
|
+
})
|
|
202
302
|
|
|
203
303
|
/**
|
|
204
304
|
* Check if developer/owner mode is enabled
|
|
205
305
|
* Allows the tool creator to use all features without a license
|
|
306
|
+
* Security: Production mode always enforces license checks
|
|
206
307
|
*/
|
|
207
308
|
function isDeveloperMode() {
|
|
309
|
+
// Security: Production mode never allows developer bypass
|
|
310
|
+
if (process.env.NODE_ENV === 'production') {
|
|
311
|
+
return false
|
|
312
|
+
}
|
|
313
|
+
|
|
208
314
|
// Check environment variable
|
|
209
315
|
if (process.env.QAA_DEVELOPER === 'true') {
|
|
210
316
|
return true
|
|
@@ -216,8 +322,19 @@ function isDeveloperMode() {
|
|
|
216
322
|
if (fs.existsSync(developerMarkerFile)) {
|
|
217
323
|
return true
|
|
218
324
|
}
|
|
219
|
-
} catch {
|
|
220
|
-
//
|
|
325
|
+
} catch (error) {
|
|
326
|
+
// DR10 fix: Halt on ELOOP in production (security issue)
|
|
327
|
+
if (error?.code === 'ELOOP') {
|
|
328
|
+
const message =
|
|
329
|
+
'Symlink loop detected in license directory - possible security issue'
|
|
330
|
+
if (process.env.NODE_ENV === 'production') {
|
|
331
|
+
throw new Error(message)
|
|
332
|
+
} else {
|
|
333
|
+
console.warn(`ā ļø ${message}`)
|
|
334
|
+
}
|
|
335
|
+
} else if (process.env.DEBUG && error?.code !== 'ENOENT') {
|
|
336
|
+
console.warn(`ā ļø Developer mode check failed: ${error.message}`)
|
|
337
|
+
}
|
|
221
338
|
}
|
|
222
339
|
|
|
223
340
|
return false
|
|
@@ -248,23 +365,24 @@ function getLicenseInfo() {
|
|
|
248
365
|
return { tier: LICENSE_TIERS.FREE, valid: true }
|
|
249
366
|
}
|
|
250
367
|
|
|
251
|
-
|
|
252
|
-
|
|
368
|
+
const licenseKey = normalizeLicenseKey(
|
|
369
|
+
localLicense.licenseKey || localLicense.key
|
|
370
|
+
)
|
|
371
|
+
if (!licenseKey || !localLicense.email) {
|
|
253
372
|
return {
|
|
254
373
|
tier: LICENSE_TIERS.FREE,
|
|
255
374
|
valid: true,
|
|
256
|
-
error:
|
|
257
|
-
'License signature verification failed - license may have been tampered with',
|
|
375
|
+
error: 'Invalid license format',
|
|
258
376
|
}
|
|
259
377
|
}
|
|
260
378
|
|
|
261
|
-
// Check
|
|
262
|
-
|
|
263
|
-
if (!licenseKey || !localLicense.email) {
|
|
379
|
+
// Check if license is valid
|
|
380
|
+
if (!localLicense.valid) {
|
|
264
381
|
return {
|
|
265
382
|
tier: LICENSE_TIERS.FREE,
|
|
266
383
|
valid: true,
|
|
267
|
-
error:
|
|
384
|
+
error:
|
|
385
|
+
'License signature verification failed - license may have been tampered with',
|
|
268
386
|
}
|
|
269
387
|
}
|
|
270
388
|
|
|
@@ -309,17 +427,16 @@ function getLicenseInfo() {
|
|
|
309
427
|
* Supports both legacy format and new Stripe-generated keys
|
|
310
428
|
*/
|
|
311
429
|
function validateLicenseKey(key, tier) {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
if (stripeFormat.test(key)) {
|
|
430
|
+
const normalizedKey = normalizeLicenseKey(key)
|
|
431
|
+
// TD15 fix: Use shared constant for license key pattern
|
|
432
|
+
if (LICENSE_KEY_PATTERN.test(normalizedKey)) {
|
|
316
433
|
// Stripe-generated key - always valid if properly formatted
|
|
317
434
|
return true
|
|
318
435
|
}
|
|
319
436
|
|
|
320
437
|
// Legacy format validation for backward compatibility
|
|
321
438
|
const expectedPrefix = `QAA-${tier.toUpperCase()}-`
|
|
322
|
-
return
|
|
439
|
+
return normalizedKey.startsWith(expectedPrefix) && normalizedKey.length > 20
|
|
323
440
|
}
|
|
324
441
|
|
|
325
442
|
/**
|
|
@@ -327,19 +444,27 @@ function validateLicenseKey(key, tier) {
|
|
|
327
444
|
*/
|
|
328
445
|
function verifyLicenseSignature(payload, signature) {
|
|
329
446
|
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)
|
|
447
|
+
const publicKey = loadKeyFromEnv(
|
|
448
|
+
process.env.QAA_LICENSE_PUBLIC_KEY,
|
|
449
|
+
process.env.QAA_LICENSE_PUBLIC_KEY_PATH
|
|
340
450
|
)
|
|
341
|
-
|
|
342
|
-
|
|
451
|
+
if (!publicKey) {
|
|
452
|
+
// TD12 fix: Log warning when public key is missing in non-dev mode
|
|
453
|
+
// Security: Production mode never allows developer bypass
|
|
454
|
+
const isDevMode =
|
|
455
|
+
process.env.NODE_ENV !== 'production' &&
|
|
456
|
+
process.env.QAA_DEVELOPER === 'true'
|
|
457
|
+
if (!isDevMode) {
|
|
458
|
+
console.warn(
|
|
459
|
+
'ā ļø License public key not configured - signature verification skipped'
|
|
460
|
+
)
|
|
461
|
+
}
|
|
462
|
+
return isDevMode
|
|
463
|
+
}
|
|
464
|
+
return verifyPayload(payload, signature, publicKey)
|
|
465
|
+
} catch (error) {
|
|
466
|
+
// TD12 fix: Log verification failures instead of silently returning false
|
|
467
|
+
console.warn(`ā ļø Signature verification failed: ${error.message}`)
|
|
343
468
|
return false
|
|
344
469
|
}
|
|
345
470
|
}
|
|
@@ -376,7 +501,6 @@ function getSupportedLanguages() {
|
|
|
376
501
|
*/
|
|
377
502
|
function showUpgradeMessage(feature) {
|
|
378
503
|
const license = getLicenseInfo()
|
|
379
|
-
const _tierFeatures = FEATURES[license.tier] || FEATURES[LICENSE_TIERS.FREE]
|
|
380
504
|
|
|
381
505
|
console.log(`\nš ${feature} is a premium feature`)
|
|
382
506
|
console.log(`š Current license: ${license.tier.toUpperCase()}`)
|
|
@@ -431,22 +555,68 @@ function showUpgradeMessage(feature) {
|
|
|
431
555
|
*/
|
|
432
556
|
function saveLicense(tier, key, email, expires = null) {
|
|
433
557
|
try {
|
|
558
|
+
// DR24 fix: Validate tier is a valid LICENSE_TIERS value
|
|
559
|
+
const validTiers = Object.values(LICENSE_TIERS)
|
|
560
|
+
if (!validTiers.includes(tier)) {
|
|
561
|
+
return {
|
|
562
|
+
success: false,
|
|
563
|
+
error: `Invalid tier "${tier}". Must be one of: ${validTiers.join(', ')}`,
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// DR21 fix: Validate email format before hashing
|
|
568
|
+
if (email) {
|
|
569
|
+
const normalizedEmail = require('./license-signing').normalizeEmail(email)
|
|
570
|
+
if (!normalizedEmail) {
|
|
571
|
+
return {
|
|
572
|
+
success: false,
|
|
573
|
+
error: `Invalid email format: "${email}". Must be valid email address (e.g., user@example.com)`,
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
434
578
|
const licenseDir = getLicenseDir()
|
|
435
579
|
const licenseFile = getLicenseFile()
|
|
580
|
+
const normalizedKey = normalizeLicenseKey(key)
|
|
581
|
+
const privateKey = loadKeyFromEnv(
|
|
582
|
+
process.env.LICENSE_REGISTRY_PRIVATE_KEY,
|
|
583
|
+
process.env.LICENSE_REGISTRY_PRIVATE_KEY_PATH
|
|
584
|
+
)
|
|
436
585
|
|
|
437
586
|
if (!fs.existsSync(licenseDir)) {
|
|
438
|
-
fs.mkdirSync(licenseDir, { recursive: true })
|
|
587
|
+
fs.mkdirSync(licenseDir, { recursive: true, mode: 0o700 })
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (!privateKey) {
|
|
591
|
+
return {
|
|
592
|
+
success: false,
|
|
593
|
+
error:
|
|
594
|
+
'LICENSE_REGISTRY_PRIVATE_KEY or LICENSE_REGISTRY_PRIVATE_KEY_PATH is required to save a signed license',
|
|
595
|
+
}
|
|
439
596
|
}
|
|
440
597
|
|
|
598
|
+
const payload = buildLicensePayload({
|
|
599
|
+
licenseKey: normalizedKey,
|
|
600
|
+
tier,
|
|
601
|
+
isFounder: false,
|
|
602
|
+
emailHash: hashEmail(email),
|
|
603
|
+
issued: new Date().toISOString(),
|
|
604
|
+
})
|
|
605
|
+
const signature = signPayload(payload, privateKey)
|
|
606
|
+
|
|
441
607
|
const licenseData = {
|
|
442
608
|
tier,
|
|
443
|
-
|
|
609
|
+
licenseKey: normalizedKey,
|
|
444
610
|
email,
|
|
445
611
|
expires,
|
|
446
612
|
activated: new Date().toISOString(),
|
|
613
|
+
payload,
|
|
614
|
+
signature,
|
|
447
615
|
}
|
|
448
616
|
|
|
449
|
-
fs.writeFileSync(licenseFile, JSON.stringify(licenseData, null, 2)
|
|
617
|
+
fs.writeFileSync(licenseFile, JSON.stringify(licenseData, null, 2), {
|
|
618
|
+
mode: 0o600,
|
|
619
|
+
})
|
|
450
620
|
return { success: true }
|
|
451
621
|
} catch (error) {
|
|
452
622
|
return { success: false, error: error.message }
|
|
@@ -458,16 +628,26 @@ function saveLicense(tier, key, email, expires = null) {
|
|
|
458
628
|
*/
|
|
459
629
|
function saveLicenseWithSignature(tier, key, email, validation) {
|
|
460
630
|
try {
|
|
631
|
+
// DR24 fix: Validate tier is a valid LICENSE_TIERS value
|
|
632
|
+
const validTiers = Object.values(LICENSE_TIERS)
|
|
633
|
+
if (!validTiers.includes(tier)) {
|
|
634
|
+
return {
|
|
635
|
+
success: false,
|
|
636
|
+
error: `Invalid tier "${tier}". Must be one of: ${validTiers.join(', ')}`,
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
461
640
|
const licenseDir = getLicenseDir()
|
|
462
641
|
const licenseFile = getLicenseFile()
|
|
642
|
+
const normalizedKey = normalizeLicenseKey(key)
|
|
463
643
|
|
|
464
644
|
if (!fs.existsSync(licenseDir)) {
|
|
465
|
-
fs.mkdirSync(licenseDir, { recursive: true })
|
|
645
|
+
fs.mkdirSync(licenseDir, { recursive: true, mode: 0o700 })
|
|
466
646
|
}
|
|
467
647
|
|
|
468
648
|
const licenseData = {
|
|
469
649
|
tier,
|
|
470
|
-
licenseKey:
|
|
650
|
+
licenseKey: normalizedKey, // ā
Changed from 'key' to 'licenseKey' to match StripeIntegration
|
|
471
651
|
email,
|
|
472
652
|
expires: validation.expires,
|
|
473
653
|
activated: new Date().toISOString(),
|
|
@@ -479,7 +659,9 @@ function saveLicenseWithSignature(tier, key, email, validation) {
|
|
|
479
659
|
issued: validation.issued,
|
|
480
660
|
}
|
|
481
661
|
|
|
482
|
-
fs.writeFileSync(licenseFile, JSON.stringify(licenseData, null, 2)
|
|
662
|
+
fs.writeFileSync(licenseFile, JSON.stringify(licenseData, null, 2), {
|
|
663
|
+
mode: 0o600,
|
|
664
|
+
})
|
|
483
665
|
return { success: true }
|
|
484
666
|
} catch (error) {
|
|
485
667
|
return { success: false, error: error.message }
|
|
@@ -535,11 +717,28 @@ async function addLegitimateKey(
|
|
|
535
717
|
purchaseEmail = null
|
|
536
718
|
) {
|
|
537
719
|
try {
|
|
720
|
+
// DR21 fix: Validate email format before hashing
|
|
721
|
+
if (purchaseEmail) {
|
|
722
|
+
const normalizedEmail =
|
|
723
|
+
require('./license-signing').normalizeEmail(purchaseEmail)
|
|
724
|
+
if (!normalizedEmail) {
|
|
725
|
+
return {
|
|
726
|
+
success: false,
|
|
727
|
+
error: `Invalid email format: "${purchaseEmail}". Must be valid email address (e.g., user@example.com)`,
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const normalizedKey = normalizeLicenseKey(licenseKey)
|
|
538
733
|
// Use the same license directory as the CLI
|
|
539
734
|
const licenseDir =
|
|
540
735
|
process.env.QAA_LICENSE_DIR ||
|
|
541
736
|
path.join(os.homedir(), '.create-qa-architect')
|
|
542
737
|
const legitimateDBFile = path.join(licenseDir, 'legitimate-licenses.json')
|
|
738
|
+
const privateKey = loadKeyFromEnv(
|
|
739
|
+
process.env.LICENSE_REGISTRY_PRIVATE_KEY,
|
|
740
|
+
process.env.LICENSE_REGISTRY_PRIVATE_KEY_PATH
|
|
741
|
+
)
|
|
543
742
|
|
|
544
743
|
// Ensure directory exists
|
|
545
744
|
if (!fs.existsSync(licenseDir)) {
|
|
@@ -551,43 +750,82 @@ async function addLegitimateKey(
|
|
|
551
750
|
if (fs.existsSync(legitimateDBFile)) {
|
|
552
751
|
try {
|
|
553
752
|
database = JSON.parse(fs.readFileSync(legitimateDBFile, 'utf8'))
|
|
554
|
-
} catch {
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
753
|
+
} catch (parseError) {
|
|
754
|
+
// DR8 fix: Return error instead of continuing with corrupted database
|
|
755
|
+
const backupPath = `${legitimateDBFile}.corrupted.${Date.now()}`
|
|
756
|
+
let backupSucceeded = false
|
|
757
|
+
|
|
758
|
+
try {
|
|
759
|
+
fs.copyFileSync(legitimateDBFile, backupPath)
|
|
760
|
+
backupSucceeded = true
|
|
761
|
+
console.error(
|
|
762
|
+
`ā ļø Database corruption detected. Backed up to ${backupPath}`
|
|
763
|
+
)
|
|
764
|
+
} catch (backupError) {
|
|
765
|
+
console.error(
|
|
766
|
+
`ā CRITICAL: Could not backup corrupted database: ${backupError.message}`
|
|
767
|
+
)
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Always return error on corruption - forces investigation
|
|
771
|
+
return {
|
|
772
|
+
success: false,
|
|
773
|
+
error: backupSucceeded
|
|
774
|
+
? `License database corrupted (backup saved to ${backupPath}). Manual review required before adding keys.`
|
|
775
|
+
: `License database corrupted AND backup failed. Cannot proceed without data loss risk. Parse error: ${parseError.message}`,
|
|
776
|
+
}
|
|
558
777
|
}
|
|
559
778
|
}
|
|
560
779
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
description: 'Legitimate license database - populated by admin/webhook',
|
|
780
|
+
if (!privateKey) {
|
|
781
|
+
return {
|
|
782
|
+
success: false,
|
|
783
|
+
error:
|
|
784
|
+
'LICENSE_REGISTRY_PRIVATE_KEY or LICENSE_REGISTRY_PRIVATE_KEY_PATH is required to add legitimate keys',
|
|
567
785
|
}
|
|
568
786
|
}
|
|
569
787
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
788
|
+
const issued = new Date().toISOString()
|
|
789
|
+
const emailHash = hashEmail(purchaseEmail)
|
|
790
|
+
const payload = buildLicensePayload({
|
|
791
|
+
licenseKey: normalizedKey,
|
|
573
792
|
tier,
|
|
574
793
|
isFounder,
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
794
|
+
emailHash,
|
|
795
|
+
issued,
|
|
796
|
+
})
|
|
797
|
+
const signature = signPayload(payload, privateKey)
|
|
798
|
+
|
|
799
|
+
const { _metadata, ...existingLicenses } = database
|
|
800
|
+
const licenses = {
|
|
801
|
+
...existingLicenses,
|
|
802
|
+
[normalizedKey]: {
|
|
803
|
+
tier,
|
|
804
|
+
isFounder,
|
|
805
|
+
issued,
|
|
806
|
+
emailHash,
|
|
807
|
+
signature,
|
|
808
|
+
keyId: process.env.LICENSE_REGISTRY_KEY_ID || 'default',
|
|
809
|
+
},
|
|
578
810
|
}
|
|
579
811
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
database._metadata.totalLicenses = Object.keys(database).length - 1 // Exclude metadata
|
|
583
|
-
|
|
584
|
-
// Calculate SHA256 checksum for integrity verification (MANDATORY)
|
|
585
|
-
const { _metadata, ...licensesOnly } = database
|
|
586
|
-
const sha256 = crypto
|
|
812
|
+
const registrySignature = signPayload(licenses, privateKey)
|
|
813
|
+
const hash = crypto
|
|
587
814
|
.createHash('sha256')
|
|
588
|
-
.update(
|
|
815
|
+
.update(stableStringify(licenses))
|
|
589
816
|
.digest('hex')
|
|
590
|
-
|
|
817
|
+
const metadata = {
|
|
818
|
+
version: '1.0',
|
|
819
|
+
created: _metadata?.created || new Date().toISOString(),
|
|
820
|
+
lastUpdate: new Date().toISOString(),
|
|
821
|
+
description: 'Legitimate license database - populated by admin/webhook',
|
|
822
|
+
algorithm: 'ed25519',
|
|
823
|
+
keyId: process.env.LICENSE_REGISTRY_KEY_ID || 'default',
|
|
824
|
+
registrySignature,
|
|
825
|
+
hash,
|
|
826
|
+
totalLicenses: Object.keys(licenses).length,
|
|
827
|
+
}
|
|
828
|
+
database = { _metadata: metadata, ...licenses }
|
|
591
829
|
|
|
592
830
|
// Save database
|
|
593
831
|
fs.writeFileSync(legitimateDBFile, JSON.stringify(database, null, 2))
|
|
@@ -608,60 +846,60 @@ async function addLegitimateKey(
|
|
|
608
846
|
|
|
609
847
|
/**
|
|
610
848
|
* Interactive license activation prompt
|
|
849
|
+
* DR27 fix: Converted from callback-based readline to async/await pattern
|
|
611
850
|
*/
|
|
612
851
|
async function promptLicenseActivation() {
|
|
613
|
-
const readline = require('readline')
|
|
852
|
+
const readline = require('readline/promises')
|
|
614
853
|
|
|
615
854
|
const rl = readline.createInterface({
|
|
616
855
|
input: process.stdin,
|
|
617
856
|
output: process.stdout,
|
|
618
857
|
})
|
|
619
858
|
|
|
620
|
-
|
|
859
|
+
try {
|
|
621
860
|
console.log('\nš License Activation')
|
|
622
861
|
console.log(
|
|
623
862
|
'Enter your license key from the purchase confirmation email.\n'
|
|
624
863
|
)
|
|
625
864
|
|
|
626
|
-
rl.question(
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
rl.close()
|
|
630
|
-
resolve({ success: false })
|
|
631
|
-
return
|
|
632
|
-
}
|
|
865
|
+
const licenseKey = await rl.question(
|
|
866
|
+
'License key (QAA-XXXX-XXXX-XXXX-XXXX): '
|
|
867
|
+
)
|
|
633
868
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
return
|
|
640
|
-
}
|
|
869
|
+
if (!licenseKey.trim()) {
|
|
870
|
+
console.log('ā License key required')
|
|
871
|
+
rl.close()
|
|
872
|
+
return { success: false }
|
|
873
|
+
}
|
|
641
874
|
|
|
642
|
-
|
|
875
|
+
const email = await rl.question('Email address: ')
|
|
643
876
|
|
|
644
|
-
|
|
877
|
+
if (!email.trim()) {
|
|
878
|
+
console.log('ā Email address required')
|
|
879
|
+
rl.close()
|
|
880
|
+
return { success: false }
|
|
881
|
+
}
|
|
645
882
|
|
|
646
|
-
|
|
647
|
-
!result.success &&
|
|
648
|
-
result.error &&
|
|
649
|
-
result.error.includes('not found')
|
|
650
|
-
) {
|
|
651
|
-
console.log('\nš License activation assistance:')
|
|
652
|
-
console.log(
|
|
653
|
-
' If you purchased this license, please contact support at:'
|
|
654
|
-
)
|
|
655
|
-
console.log(' Email: support@vibebuildlab.com')
|
|
656
|
-
console.log(
|
|
657
|
-
' Include your license key and purchase email for verification.'
|
|
658
|
-
)
|
|
659
|
-
}
|
|
883
|
+
rl.close()
|
|
660
884
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
885
|
+
const result = await activateLicense(licenseKey.trim(), email.trim())
|
|
886
|
+
|
|
887
|
+
if (!result.success && result.error && result.error.includes('not found')) {
|
|
888
|
+
console.log('\nš License activation assistance:')
|
|
889
|
+
console.log(
|
|
890
|
+
' If you purchased this license, please contact support at:'
|
|
891
|
+
)
|
|
892
|
+
console.log(' Email: support@vibebuildlab.com')
|
|
893
|
+
console.log(
|
|
894
|
+
' Include your license key and purchase email for verification.'
|
|
895
|
+
)
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
return result
|
|
899
|
+
} catch (error) {
|
|
900
|
+
rl.close()
|
|
901
|
+
return { success: false, error: error.message }
|
|
902
|
+
}
|
|
665
903
|
}
|
|
666
904
|
|
|
667
905
|
/**
|
|
@@ -743,14 +981,74 @@ function loadUsage() {
|
|
|
743
981
|
|
|
744
982
|
return data
|
|
745
983
|
}
|
|
746
|
-
} catch {
|
|
747
|
-
//
|
|
984
|
+
} catch (error) {
|
|
985
|
+
// DR8 fix: Prevent quota bypass through file corruption
|
|
986
|
+
if (error instanceof SyntaxError) {
|
|
987
|
+
const usageFile = getUsageFile()
|
|
988
|
+
console.error(`\nā CRITICAL: Usage tracking file is corrupted`)
|
|
989
|
+
console.error(` File: ${usageFile}`)
|
|
990
|
+
console.error(` Parse error: ${error.message}\n`)
|
|
991
|
+
|
|
992
|
+
// Backup corrupted file for forensics
|
|
993
|
+
const backupPath = `${usageFile}.corrupted.${Date.now()}`
|
|
994
|
+
try {
|
|
995
|
+
fs.copyFileSync(usageFile, backupPath)
|
|
996
|
+
console.log(` ā
Backup saved: ${backupPath}`)
|
|
997
|
+
} catch (_backupError) {
|
|
998
|
+
console.error(` ā Could not create backup`)
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
const license = getLicenseInfo()
|
|
1002
|
+
|
|
1003
|
+
if (license.tier === LICENSE_TIERS.FREE) {
|
|
1004
|
+
console.error(`\nā ļø FREE TIER CORRUPTION POLICY:`)
|
|
1005
|
+
console.error(
|
|
1006
|
+
` To prevent quota bypass, your usage has been reset to maximum.`
|
|
1007
|
+
)
|
|
1008
|
+
console.error(` This is a security measure, not a penalty.\n`)
|
|
1009
|
+
console.error(` To restore your usage:`)
|
|
1010
|
+
console.error(` 1. Review the backup file: ${backupPath}`)
|
|
1011
|
+
console.error(` 2. If data looks correct, manually fix JSON syntax`)
|
|
1012
|
+
console.error(` 3. Copy corrected JSON back to: ${usageFile}`)
|
|
1013
|
+
console.error(
|
|
1014
|
+
` 4. Or delete ${usageFile} to start fresh this month\n`
|
|
1015
|
+
)
|
|
1016
|
+
console.error(` If this keeps happening, please report the issue.`)
|
|
1017
|
+
|
|
1018
|
+
// Provide clear recovery path
|
|
1019
|
+
console.error(`\nš§ Quick fix: rm ${usageFile}`)
|
|
1020
|
+
console.error(
|
|
1021
|
+
` This will reset your usage to 0 for the current month.\n`
|
|
1022
|
+
)
|
|
1023
|
+
|
|
1024
|
+
const caps = FEATURES[LICENSE_TIERS.FREE]
|
|
1025
|
+
return {
|
|
1026
|
+
month: getCurrentMonth(),
|
|
1027
|
+
prePushRuns: caps.maxPrePushRunsPerMonth,
|
|
1028
|
+
dependencyPRs: caps.maxDependencyPRsPerMonth,
|
|
1029
|
+
repos: Array.from(
|
|
1030
|
+
{ length: caps.maxPrivateRepos },
|
|
1031
|
+
(_item, index) => `corrupted-${index + 1}`
|
|
1032
|
+
),
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
} else if (process.env.DEBUG && error?.code !== 'ENOENT') {
|
|
1036
|
+
console.warn(`ā ļø Could not read usage file: ${error.message}`)
|
|
1037
|
+
}
|
|
748
1038
|
}
|
|
749
1039
|
|
|
750
1040
|
// Default usage data
|
|
1041
|
+
return getDefaultUsage()
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function getCurrentMonth() {
|
|
751
1045
|
const now = new Date()
|
|
1046
|
+
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
function getDefaultUsage() {
|
|
752
1050
|
return {
|
|
753
|
-
month:
|
|
1051
|
+
month: getCurrentMonth(),
|
|
754
1052
|
prePushRuns: 0,
|
|
755
1053
|
dependencyPRs: 0,
|
|
756
1054
|
repos: [],
|
|
@@ -764,12 +1062,42 @@ function saveUsage(usage) {
|
|
|
764
1062
|
try {
|
|
765
1063
|
const licenseDir = getLicenseDir()
|
|
766
1064
|
if (!fs.existsSync(licenseDir)) {
|
|
767
|
-
fs.mkdirSync(licenseDir, { recursive: true })
|
|
1065
|
+
fs.mkdirSync(licenseDir, { recursive: true, mode: 0o700 })
|
|
768
1066
|
}
|
|
769
|
-
fs.writeFileSync(getUsageFile(), JSON.stringify(usage, null, 2)
|
|
1067
|
+
fs.writeFileSync(getUsageFile(), JSON.stringify(usage, null, 2), {
|
|
1068
|
+
mode: 0o600,
|
|
1069
|
+
})
|
|
770
1070
|
return true
|
|
771
|
-
} catch {
|
|
772
|
-
|
|
1071
|
+
} catch (error) {
|
|
1072
|
+
const license = getLicenseInfo()
|
|
1073
|
+
const usageFile = getUsageFile()
|
|
1074
|
+
|
|
1075
|
+
// For FREE tier, this is critical - can't track quota
|
|
1076
|
+
if (license.tier === LICENSE_TIERS.FREE) {
|
|
1077
|
+
console.error(`\nā CRITICAL: Cannot save usage tracking data`)
|
|
1078
|
+
console.error(` File: ${usageFile}`)
|
|
1079
|
+
console.error(` Error: ${error.message} (${error.code})`)
|
|
1080
|
+
console.error(`\n FREE tier quota enforcement requires usage tracking.`)
|
|
1081
|
+
console.error(` Please fix this filesystem issue:\n`)
|
|
1082
|
+
|
|
1083
|
+
if (error.code === 'ENOSPC') {
|
|
1084
|
+
console.error(` ⢠Disk full - free up space`)
|
|
1085
|
+
} else if (error.code === 'EACCES') {
|
|
1086
|
+
console.error(` ⢠Permission denied - check directory permissions`)
|
|
1087
|
+
console.error(` ⢠Try: chmod 700 ${getLicenseDir()}`)
|
|
1088
|
+
} else if (error.code === 'EROFS') {
|
|
1089
|
+
console.error(` ⢠Filesystem is readonly - remount as read-write`)
|
|
1090
|
+
} else {
|
|
1091
|
+
console.error(` ⢠Unexpected error - please report this issue`)
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
throw error // Don't allow FREE tier to continue without tracking
|
|
1095
|
+
} else {
|
|
1096
|
+
// Pro/Team/Enterprise - warn but don't fail
|
|
1097
|
+
console.warn(`ā ļø Failed to save usage data: ${error.message}`)
|
|
1098
|
+
console.warn(` This won't affect Pro/Team/Enterprise functionality`)
|
|
1099
|
+
return false
|
|
1100
|
+
}
|
|
773
1101
|
}
|
|
774
1102
|
}
|
|
775
1103
|
|