create-qa-architect 5.0.7 → 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/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,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
- // Ignore errors checking for marker file
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
- // Check if license is valid
252
- if (!localLicense.valid) {
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 for required fields
262
- const licenseKey = localLicense.licenseKey || localLicense.key
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: 'Invalid license format',
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
- // 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)) {
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 key.startsWith(expectedPrefix) && key.length > 20
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 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)
439
+ const publicKey = loadKeyFromEnv(
440
+ process.env.QAA_LICENSE_PUBLIC_KEY,
441
+ process.env.QAA_LICENSE_PUBLIC_KEY_PATH
340
442
  )
341
- } catch {
342
- // If signature verification fails, treat as invalid
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()}`)
@@ -431,19 +547,52 @@ function showUpgradeMessage(feature) {
431
547
  */
432
548
  function saveLicense(tier, key, email, expires = null) {
433
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
+
434
559
  const licenseDir = getLicenseDir()
435
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
+ )
436
566
 
437
567
  if (!fs.existsSync(licenseDir)) {
438
568
  fs.mkdirSync(licenseDir, { recursive: true })
439
569
  }
440
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
+
441
588
  const licenseData = {
442
589
  tier,
443
- key,
590
+ licenseKey: normalizedKey,
444
591
  email,
445
592
  expires,
446
593
  activated: new Date().toISOString(),
594
+ payload,
595
+ signature,
447
596
  }
448
597
 
449
598
  fs.writeFileSync(licenseFile, JSON.stringify(licenseData, null, 2))
@@ -458,8 +607,18 @@ function saveLicense(tier, key, email, expires = null) {
458
607
  */
459
608
  function saveLicenseWithSignature(tier, key, email, validation) {
460
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
+
461
619
  const licenseDir = getLicenseDir()
462
620
  const licenseFile = getLicenseFile()
621
+ const normalizedKey = normalizeLicenseKey(key)
463
622
 
464
623
  if (!fs.existsSync(licenseDir)) {
465
624
  fs.mkdirSync(licenseDir, { recursive: true })
@@ -467,7 +626,7 @@ function saveLicenseWithSignature(tier, key, email, validation) {
467
626
 
468
627
  const licenseData = {
469
628
  tier,
470
- licenseKey: key, // ✅ Changed from 'key' to 'licenseKey' to match StripeIntegration
629
+ licenseKey: normalizedKey, // ✅ Changed from 'key' to 'licenseKey' to match StripeIntegration
471
630
  email,
472
631
  expires: validation.expires,
473
632
  activated: new Date().toISOString(),
@@ -535,11 +694,16 @@ async function addLegitimateKey(
535
694
  purchaseEmail = null
536
695
  ) {
537
696
  try {
697
+ const normalizedKey = normalizeLicenseKey(licenseKey)
538
698
  // Use the same license directory as the CLI
539
699
  const licenseDir =
540
700
  process.env.QAA_LICENSE_DIR ||
541
701
  path.join(os.homedir(), '.create-qa-architect')
542
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
+ )
543
707
 
544
708
  // Ensure directory exists
545
709
  if (!fs.existsSync(licenseDir)) {
@@ -551,43 +715,72 @@ async function addLegitimateKey(
551
715
  if (fs.existsSync(legitimateDBFile)) {
552
716
  try {
553
717
  database = JSON.parse(fs.readFileSync(legitimateDBFile, 'utf8'))
554
- } catch {
555
- console.error(
556
- 'Warning: Could not parse existing database, creating new one'
557
- )
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}`)
558
732
  }
559
733
  }
560
734
 
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',
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',
567
740
  }
568
741
  }
569
742
 
570
- // Add license
571
- database[licenseKey] = {
572
- customerId,
743
+ const issued = new Date().toISOString()
744
+ const emailHash = hashEmail(purchaseEmail)
745
+ const payload = buildLicensePayload({
746
+ licenseKey: normalizedKey,
573
747
  tier,
574
748
  isFounder,
575
- email: purchaseEmail,
576
- addedDate: new Date().toISOString(),
577
- addedBy: 'admin_tool',
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
+ },
578
765
  }
579
766
 
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
767
+ const registrySignature = signPayload(licenses, privateKey)
768
+ const hash = crypto
587
769
  .createHash('sha256')
588
- .update(JSON.stringify(licensesOnly))
770
+ .update(stableStringify(licenses))
589
771
  .digest('hex')
590
- database._metadata.sha256 = sha256
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 }
591
784
 
592
785
  // Save database
593
786
  fs.writeFileSync(legitimateDBFile, JSON.stringify(database, null, 2))
@@ -608,60 +801,60 @@ async function addLegitimateKey(
608
801
 
609
802
  /**
610
803
  * Interactive license activation prompt
804
+ * DR27 fix: Converted from callback-based readline to async/await pattern
611
805
  */
612
806
  async function promptLicenseActivation() {
613
- const readline = require('readline')
807
+ const readline = require('readline/promises')
614
808
 
615
809
  const rl = readline.createInterface({
616
810
  input: process.stdin,
617
811
  output: process.stdout,
618
812
  })
619
813
 
620
- return new Promise(resolve => {
814
+ try {
621
815
  console.log('\n🔑 License Activation')
622
816
  console.log(
623
817
  'Enter your license key from the purchase confirmation email.\n'
624
818
  )
625
819
 
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
- }
820
+ const licenseKey = await rl.question(
821
+ 'License key (QAA-XXXX-XXXX-XXXX-XXXX): '
822
+ )
633
823
 
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
- }
824
+ if (!licenseKey.trim()) {
825
+ console.log('❌ License key required')
826
+ rl.close()
827
+ return { success: false }
828
+ }
641
829
 
642
- rl.close()
830
+ const email = await rl.question('Email address: ')
643
831
 
644
- const result = await activateLicense(licenseKey.trim(), email.trim())
832
+ if (!email.trim()) {
833
+ console.log('❌ Email address required')
834
+ rl.close()
835
+ return { success: false }
836
+ }
645
837
 
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
- }
838
+ rl.close()
660
839
 
661
- resolve(result)
662
- })
663
- })
664
- })
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
+ }
665
858
  }
666
859
 
667
860
  /**
@@ -743,14 +936,53 @@ function loadUsage() {
743
936
 
744
937
  return data
745
938
  }
746
- } catch {
747
- // Ignore errors reading usage file
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
+ }
748
972
  }
749
973
 
750
974
  // Default usage data
975
+ return getDefaultUsage()
976
+ }
977
+
978
+ function getCurrentMonth() {
751
979
  const now = new Date()
980
+ return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`
981
+ }
982
+
983
+ function getDefaultUsage() {
752
984
  return {
753
- month: `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`,
985
+ month: getCurrentMonth(),
754
986
  prePushRuns: 0,
755
987
  dependencyPRs: 0,
756
988
  repos: [],
@@ -768,7 +1000,9 @@ function saveUsage(usage) {
768
1000
  }
769
1001
  fs.writeFileSync(getUsageFile(), JSON.stringify(usage, null, 2))
770
1002
  return true
771
- } 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}`)
772
1006
  return false
773
1007
  }
774
1008
  }