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.
Files changed (45) hide show
  1. package/.github/workflows/auto-release.yml +49 -0
  2. package/.github/workflows/quality.yml +11 -11
  3. package/.github/workflows/shell-ci.yml.example +82 -0
  4. package/.github/workflows/shell-quality.yml.example +148 -0
  5. package/README.md +165 -12
  6. package/config/shell-ci.yml +82 -0
  7. package/config/shell-quality.yml +148 -0
  8. package/docs/ADOPTION-SUMMARY.md +41 -0
  9. package/docs/ARCHITECTURE-REVIEW.md +67 -0
  10. package/docs/ARCHITECTURE.md +29 -45
  11. package/docs/CI-COST-ANALYSIS.md +323 -0
  12. package/docs/CODE-REVIEW.md +100 -0
  13. package/docs/REQUIREMENTS.md +148 -0
  14. package/docs/SECURITY-AUDIT.md +68 -0
  15. package/docs/test-trace-matrix.md +28 -0
  16. package/eslint.config.cjs +2 -0
  17. package/lib/commands/analyze-ci.js +616 -0
  18. package/lib/commands/deps.js +293 -0
  19. package/lib/commands/index.js +29 -0
  20. package/lib/commands/validate.js +85 -0
  21. package/lib/config-validator.js +28 -45
  22. package/lib/error-reporter.js +14 -2
  23. package/lib/github-api.js +138 -13
  24. package/lib/license-signing.js +125 -0
  25. package/lib/license-validator.js +359 -71
  26. package/lib/licensing.js +434 -106
  27. package/lib/package-utils.js +9 -9
  28. package/lib/prelaunch-validator.js +828 -0
  29. package/lib/project-maturity.js +58 -6
  30. package/lib/quality-tools-generator.js +495 -0
  31. package/lib/result-types.js +112 -0
  32. package/lib/security-enhancements.js +1 -1
  33. package/lib/smart-strategy-generator.js +46 -10
  34. package/lib/telemetry.js +1 -1
  35. package/lib/template-loader.js +52 -19
  36. package/lib/ui-helpers.js +1 -1
  37. package/lib/validation/cache-manager.js +36 -6
  38. package/lib/validation/config-security.js +100 -33
  39. package/lib/validation/index.js +68 -97
  40. package/lib/validation/workflow-validation.js +28 -7
  41. package/package.json +4 -6
  42. package/scripts/check-test-coverage.sh +46 -0
  43. package/scripts/validate-claude-md.js +80 -0
  44. package/setup.js +923 -301
  45. 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
- const LICENSE_TIERS = {
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
- // Ignore errors checking for marker file
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
- // Check if license is valid
252
- if (!localLicense.valid) {
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 for required fields
262
- const licenseKey = localLicense.licenseKey || localLicense.key
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: 'Invalid license format',
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
- // New Stripe format: QAA-XXXX-XXXX-XXXX-XXXX
313
- const stripeFormat = /^QAA-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}$/
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 key.startsWith(expectedPrefix) && key.length > 20
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 secret =
331
- process.env.LICENSE_SIGNING_SECRET || 'cqa-dev-secret-change-in-prod'
332
- const expectedSignature = crypto
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
- } catch {
342
- // If signature verification fails, treat as invalid
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
- key,
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: key, // āœ… Changed from 'key' to 'licenseKey' to match StripeIntegration
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
- console.error(
556
- 'Warning: Could not parse existing database, creating new one'
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
- // Initialize metadata if needed
562
- if (!database._metadata) {
563
- database._metadata = {
564
- version: '1.0',
565
- created: new Date().toISOString(),
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
- // Add license
571
- database[licenseKey] = {
572
- customerId,
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
- email: purchaseEmail,
576
- addedDate: new Date().toISOString(),
577
- addedBy: 'admin_tool',
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
- // Update metadata
581
- database._metadata.lastUpdate = new Date().toISOString()
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(JSON.stringify(licensesOnly))
815
+ .update(stableStringify(licenses))
589
816
  .digest('hex')
590
- database._metadata.sha256 = sha256
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
- return new Promise(resolve => {
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('License key (QAA-XXXX-XXXX-XXXX-XXXX): ', licenseKey => {
627
- if (!licenseKey.trim()) {
628
- console.log('āŒ License key required')
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
- rl.question('Email address: ', async email => {
635
- if (!email.trim()) {
636
- console.log('āŒ Email address required')
637
- rl.close()
638
- resolve({ success: false })
639
- return
640
- }
869
+ if (!licenseKey.trim()) {
870
+ console.log('āŒ License key required')
871
+ rl.close()
872
+ return { success: false }
873
+ }
641
874
 
642
- rl.close()
875
+ const email = await rl.question('Email address: ')
643
876
 
644
- const result = await activateLicense(licenseKey.trim(), email.trim())
877
+ if (!email.trim()) {
878
+ console.log('āŒ Email address required')
879
+ rl.close()
880
+ return { success: false }
881
+ }
645
882
 
646
- if (
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
- resolve(result)
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
- // Ignore errors reading usage file
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: `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`,
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
- return false
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