create-qa-architect 5.3.1 ā 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/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 +120 -12
- package/config/shell-ci.yml +82 -0
- package/config/shell-quality.yml +148 -0
- package/docs/CI-COST-ANALYSIS.md +323 -0
- package/eslint.config.cjs +2 -0
- package/lib/commands/analyze-ci.js +616 -0
- package/lib/commands/deps.js +70 -22
- package/lib/commands/index.js +4 -0
- package/lib/config-validator.js +28 -45
- package/lib/error-reporter.js +1 -1
- package/lib/github-api.js +34 -4
- package/lib/license-signing.js +15 -0
- package/lib/licensing.js +116 -22
- package/lib/package-utils.js +9 -9
- package/lib/project-maturity.js +58 -6
- package/lib/smart-strategy-generator.js +20 -3
- package/lib/telemetry.js +1 -1
- package/lib/ui-helpers.js +1 -1
- package/lib/validation/config-security.js +22 -18
- package/lib/validation/index.js +68 -97
- package/package.json +3 -3
- package/scripts/validate-claude-md.js +80 -0
- package/setup.js +607 -51
package/lib/commands/deps.js
CHANGED
|
@@ -86,25 +86,18 @@ async function handleDependencyMonitoring() {
|
|
|
86
86
|
if (hasRuby) console.log('š Detected: Ruby project')
|
|
87
87
|
console.log(`š License tier: ${license.tier.toUpperCase()}`)
|
|
88
88
|
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const usage = increment.usage || capCheck.usage
|
|
102
|
-
const caps = capCheck.caps
|
|
103
|
-
if (usage && caps && caps.maxDependencyPRsPerMonth !== undefined) {
|
|
104
|
-
console.log(
|
|
105
|
-
`š§® Usage: ${usage.dependencyPRs}/${caps.maxDependencyPRsPerMonth} dependency monitoring runs used this month`
|
|
106
|
-
)
|
|
107
|
-
}
|
|
89
|
+
// Use sentinel value instead of null for consistent access patterns
|
|
90
|
+
const capCheck =
|
|
91
|
+
license.tier === 'FREE'
|
|
92
|
+
? checkUsageCaps('dependency-pr')
|
|
93
|
+
: { allowed: true, usage: {}, caps: {} }
|
|
94
|
+
|
|
95
|
+
if (!capCheck.allowed) {
|
|
96
|
+
console.error(`ā ${capCheck.reason}`)
|
|
97
|
+
console.error(
|
|
98
|
+
' Upgrade to Pro, Team, or Enterprise for unlimited runs: https://vibebuildlab.com/qa-architect'
|
|
99
|
+
)
|
|
100
|
+
process.exit(1)
|
|
108
101
|
}
|
|
109
102
|
|
|
110
103
|
const dependabotPath = path.join(projectPath, '.github', 'dependabot.yml')
|
|
@@ -207,6 +200,17 @@ async function handleDependencyMonitoring() {
|
|
|
207
200
|
showUpgradeMessage('Framework-Aware Dependency Grouping')
|
|
208
201
|
}
|
|
209
202
|
|
|
203
|
+
if (license.tier === 'FREE') {
|
|
204
|
+
const increment = incrementUsage('dependency-pr')
|
|
205
|
+
const usage = increment.usage || capCheck.usage
|
|
206
|
+
const caps = capCheck.caps
|
|
207
|
+
if (usage && caps && caps.maxDependencyPRsPerMonth !== undefined) {
|
|
208
|
+
console.log(
|
|
209
|
+
`š§® Usage: ${usage.dependencyPRs}/${caps.maxDependencyPRsPerMonth} dependency monitoring runs used this month`
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
210
214
|
// Auto-enable Dependabot on GitHub if token available
|
|
211
215
|
console.log('\nš§ Attempting to enable Dependabot on GitHub...')
|
|
212
216
|
try {
|
|
@@ -225,9 +229,53 @@ async function handleDependencyMonitoring() {
|
|
|
225
229
|
)
|
|
226
230
|
}
|
|
227
231
|
} catch (error) {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
232
|
+
const errorId = 'DEPENDABOT_AUTO_ENABLE_FAILED'
|
|
233
|
+
|
|
234
|
+
// Diagnose specific error types
|
|
235
|
+
let diagnosis = 'Unknown error'
|
|
236
|
+
let remedy = 'Check the error message for details'
|
|
237
|
+
|
|
238
|
+
if (
|
|
239
|
+
error.message.includes('401') ||
|
|
240
|
+
error.message.includes('authentication')
|
|
241
|
+
) {
|
|
242
|
+
diagnosis = 'GitHub token is invalid or missing'
|
|
243
|
+
remedy = 'Set GITHUB_TOKEN environment variable with a valid token'
|
|
244
|
+
} else if (error.message.includes('404')) {
|
|
245
|
+
diagnosis = 'Repository not found or insufficient permissions'
|
|
246
|
+
remedy = 'Ensure token has repo:write access to this repository'
|
|
247
|
+
} else if (error.message.includes('rate limit')) {
|
|
248
|
+
diagnosis = 'GitHub API rate limit exceeded'
|
|
249
|
+
remedy = 'Wait 1 hour or use authenticated token for higher limits'
|
|
250
|
+
} else if (
|
|
251
|
+
error.code === 'ENOTFOUND' ||
|
|
252
|
+
error.message.includes('network')
|
|
253
|
+
) {
|
|
254
|
+
diagnosis = 'Network connectivity issue'
|
|
255
|
+
remedy = 'Check internet connection and GitHub API status'
|
|
256
|
+
} else if (error.message.includes('timeout')) {
|
|
257
|
+
diagnosis = 'Request timed out'
|
|
258
|
+
remedy = 'Retry the operation or check network connectivity'
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
console.error(`\nā [${errorId}] Could not auto-enable Dependabot`)
|
|
262
|
+
console.error(` Diagnosis: ${diagnosis}`)
|
|
263
|
+
console.error(` Error: ${error.message}`)
|
|
264
|
+
console.error(`\n š§ Recommended fix: ${remedy}`)
|
|
265
|
+
|
|
266
|
+
if (process.env.DEBUG) {
|
|
267
|
+
console.error(`\n Debug info:`)
|
|
268
|
+
console.error(` ⢠Error code: ${error.code || 'N/A'}`)
|
|
269
|
+
console.error(` ⢠Stack: ${error.stack}`)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
console.log('\nš” Alternative: Enable manually:')
|
|
273
|
+
console.log(' 1. Go to GitHub repo ā Settings ā Code security')
|
|
274
|
+
console.log(' 2. Enable "Dependabot alerts"')
|
|
275
|
+
console.log(' 3. Enable "Dependabot security updates"')
|
|
276
|
+
console.log(
|
|
277
|
+
`\n ⢠Report issue: https://github.com/vibebuildlab/qa-architect/issues/new?title=${errorId}`
|
|
278
|
+
)
|
|
231
279
|
}
|
|
232
280
|
|
|
233
281
|
console.log('\nš” Next steps:')
|
package/lib/commands/index.js
CHANGED
|
@@ -12,6 +12,7 @@ const {
|
|
|
12
12
|
detectRustProject,
|
|
13
13
|
detectRubyProject,
|
|
14
14
|
} = require('./deps')
|
|
15
|
+
const { handleAnalyzeCi } = require('./analyze-ci')
|
|
15
16
|
|
|
16
17
|
module.exports = {
|
|
17
18
|
// Validation commands
|
|
@@ -22,4 +23,7 @@ module.exports = {
|
|
|
22
23
|
detectPythonProject,
|
|
23
24
|
detectRustProject,
|
|
24
25
|
detectRubyProject,
|
|
26
|
+
|
|
27
|
+
// CI/CD optimization commands
|
|
28
|
+
handleAnalyzeCi,
|
|
25
29
|
}
|
package/lib/config-validator.js
CHANGED
|
@@ -11,6 +11,33 @@ const addFormats = /** @type {(ajv: any) => void} */ (
|
|
|
11
11
|
addFormatsImport.default || addFormatsImport
|
|
12
12
|
)
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Format a single AJV validation error into a human-readable message
|
|
16
|
+
*/
|
|
17
|
+
function formatValidationError(error) {
|
|
18
|
+
const errorPath = error.instancePath || '(root)'
|
|
19
|
+
const message = error.message || 'validation failed'
|
|
20
|
+
|
|
21
|
+
const errorFormatters = {
|
|
22
|
+
required: () =>
|
|
23
|
+
`Missing required field: ${error.params?.missingProperty || 'unknown'}`,
|
|
24
|
+
enum: () =>
|
|
25
|
+
`Invalid value at ${errorPath}: must be one of ${error.params?.allowedValues?.join(', ') || 'unknown values'}`,
|
|
26
|
+
type: () =>
|
|
27
|
+
`Invalid type at ${errorPath}: expected ${error.params?.type || 'unknown'}`,
|
|
28
|
+
pattern: () => `Invalid format at ${errorPath}: ${message}`,
|
|
29
|
+
minimum: () =>
|
|
30
|
+
`Invalid value at ${errorPath}: must be >= ${error.params?.limit ?? 'unknown'}`,
|
|
31
|
+
additionalProperties: () =>
|
|
32
|
+
`Unknown property at ${errorPath}: ${error.params?.additionalProperty || 'unknown'}`,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const formatter = errorFormatters[error.keyword]
|
|
36
|
+
return formatter
|
|
37
|
+
? formatter()
|
|
38
|
+
: `Validation error at ${errorPath}: ${message}`
|
|
39
|
+
}
|
|
40
|
+
|
|
14
41
|
function validateQualityConfig(configPath) {
|
|
15
42
|
const result = {
|
|
16
43
|
valid: false,
|
|
@@ -61,51 +88,7 @@ function validateQualityConfig(configPath) {
|
|
|
61
88
|
|
|
62
89
|
if (!valid) {
|
|
63
90
|
if (validate.errors) {
|
|
64
|
-
validate.errors.
|
|
65
|
-
const errorPath = error.instancePath || '(root)'
|
|
66
|
-
const message = error.message || 'validation failed'
|
|
67
|
-
|
|
68
|
-
if (error.keyword === 'required') {
|
|
69
|
-
result.errors.push(
|
|
70
|
-
'Missing required field: ' +
|
|
71
|
-
(error.params?.missingProperty || 'unknown')
|
|
72
|
-
)
|
|
73
|
-
} else if (error.keyword === 'enum') {
|
|
74
|
-
result.errors.push(
|
|
75
|
-
'Invalid value at ' +
|
|
76
|
-
errorPath +
|
|
77
|
-
': must be one of ' +
|
|
78
|
-
(error.params?.allowedValues?.join(', ') || 'unknown values')
|
|
79
|
-
)
|
|
80
|
-
} else if (error.keyword === 'type') {
|
|
81
|
-
result.errors.push(
|
|
82
|
-
'Invalid type at ' +
|
|
83
|
-
errorPath +
|
|
84
|
-
': expected ' +
|
|
85
|
-
(error.params?.type || 'unknown')
|
|
86
|
-
)
|
|
87
|
-
} else if (error.keyword === 'pattern') {
|
|
88
|
-
result.errors.push('Invalid format at ' + errorPath + ': ' + message)
|
|
89
|
-
} else if (error.keyword === 'minimum') {
|
|
90
|
-
result.errors.push(
|
|
91
|
-
'Invalid value at ' +
|
|
92
|
-
errorPath +
|
|
93
|
-
': must be >= ' +
|
|
94
|
-
(error.params?.limit ?? 'unknown')
|
|
95
|
-
)
|
|
96
|
-
} else if (error.keyword === 'additionalProperties') {
|
|
97
|
-
result.errors.push(
|
|
98
|
-
'Unknown property at ' +
|
|
99
|
-
errorPath +
|
|
100
|
-
': ' +
|
|
101
|
-
(error.params?.additionalProperty || 'unknown')
|
|
102
|
-
)
|
|
103
|
-
} else {
|
|
104
|
-
result.errors.push(
|
|
105
|
-
'Validation error at ' + errorPath + ': ' + message
|
|
106
|
-
)
|
|
107
|
-
}
|
|
108
|
-
})
|
|
91
|
+
result.errors = validate.errors.map(formatValidationError)
|
|
109
92
|
}
|
|
110
93
|
} else {
|
|
111
94
|
result.valid = true
|
package/lib/error-reporter.js
CHANGED
|
@@ -190,7 +190,7 @@ function loadErrorReports() {
|
|
|
190
190
|
const data = fs.readFileSync(ERROR_REPORTS_FILE, 'utf8')
|
|
191
191
|
return JSON.parse(data)
|
|
192
192
|
}
|
|
193
|
-
} catch {
|
|
193
|
+
} catch (_error) {
|
|
194
194
|
// If corrupted, start fresh
|
|
195
195
|
console.warn('ā ļø Error reports data corrupted, starting fresh')
|
|
196
196
|
}
|
package/lib/github-api.js
CHANGED
|
@@ -127,9 +127,30 @@ function getRepoInfo(projectPath = '.') {
|
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Sanitize error messages to remove sensitive tokens
|
|
132
|
+
* DR29 fix: Prevent token exposure in error messages
|
|
133
|
+
*/
|
|
134
|
+
function sanitizeError(error, token) {
|
|
135
|
+
if (!error || !token) return error
|
|
136
|
+
|
|
137
|
+
const message = error.message || String(error)
|
|
138
|
+
// Use string replace with global flag instead of RegExp to avoid security warning
|
|
139
|
+
const sanitized = message.split(token).join('***REDACTED***')
|
|
140
|
+
|
|
141
|
+
if (error instanceof Error) {
|
|
142
|
+
const sanitizedError = new Error(sanitized)
|
|
143
|
+
sanitizedError.stack = error.stack?.split(token).join('***REDACTED***')
|
|
144
|
+
return sanitizedError
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return new Error(sanitized)
|
|
148
|
+
}
|
|
149
|
+
|
|
130
150
|
/**
|
|
131
151
|
* Make GitHub API request with rate limiting
|
|
132
152
|
* TD5 fix: Added rate limiting to prevent hitting GitHub's API limits
|
|
153
|
+
* DR29 fix: Sanitize errors to prevent token exposure
|
|
133
154
|
*/
|
|
134
155
|
async function githubRequest(method, path, token, data = null) {
|
|
135
156
|
// TD5 fix: Acquire rate limit token before making request
|
|
@@ -163,9 +184,13 @@ async function githubRequest(method, path, token, data = null) {
|
|
|
163
184
|
const data = body ? JSON.parse(body) : null
|
|
164
185
|
resolve({ status: res.statusCode, data })
|
|
165
186
|
} catch (_error) {
|
|
187
|
+
// DR29 fix: Sanitize error before rejecting
|
|
166
188
|
reject(
|
|
167
|
-
|
|
168
|
-
|
|
189
|
+
sanitizeError(
|
|
190
|
+
new Error(
|
|
191
|
+
`GitHub API returned invalid JSON (status ${res.statusCode}): ${body.slice(0, 100)}`
|
|
192
|
+
),
|
|
193
|
+
token
|
|
169
194
|
)
|
|
170
195
|
)
|
|
171
196
|
}
|
|
@@ -181,14 +206,19 @@ async function githubRequest(method, path, token, data = null) {
|
|
|
181
206
|
// Use raw body if JSON parse fails
|
|
182
207
|
}
|
|
183
208
|
|
|
209
|
+
// DR29 fix: Sanitize error before rejecting
|
|
184
210
|
reject(
|
|
185
|
-
|
|
211
|
+
sanitizeError(
|
|
212
|
+
new Error(`GitHub API error: ${res.statusCode} - ${errorBody}`),
|
|
213
|
+
token
|
|
214
|
+
)
|
|
186
215
|
)
|
|
187
216
|
}
|
|
188
217
|
})
|
|
189
218
|
})
|
|
190
219
|
|
|
191
|
-
|
|
220
|
+
// DR29 fix: Sanitize network errors
|
|
221
|
+
req.on('error', error => reject(sanitizeError(error, token)))
|
|
192
222
|
|
|
193
223
|
if (data) {
|
|
194
224
|
req.write(JSON.stringify(data))
|
package/lib/license-signing.js
CHANGED
|
@@ -30,9 +30,24 @@ function stableStringify(value, seen = new WeakSet()) {
|
|
|
30
30
|
return `{${entries.join(',')}}`
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Normalize and validate email format
|
|
35
|
+
* DR21 fix: Added email format validation before hashing
|
|
36
|
+
* @param {string} email - Email address to normalize
|
|
37
|
+
* @returns {string|null} - Normalized email or null if invalid
|
|
38
|
+
*/
|
|
33
39
|
function normalizeEmail(email) {
|
|
34
40
|
if (!email || typeof email !== 'string') return null
|
|
35
41
|
const normalized = email.trim().toLowerCase()
|
|
42
|
+
|
|
43
|
+
// Basic email format validation (RFC 5322 simplified)
|
|
44
|
+
// Must have: local@domain.tld format
|
|
45
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
46
|
+
|
|
47
|
+
if (!emailRegex.test(normalized)) {
|
|
48
|
+
return null
|
|
49
|
+
}
|
|
50
|
+
|
|
36
51
|
return normalized.length > 0 ? normalized : null
|
|
37
52
|
}
|
|
38
53
|
|
package/lib/licensing.js
CHANGED
|
@@ -122,6 +122,8 @@ const FEATURES = deepFreeze({
|
|
|
122
122
|
linkValidation: true, // ā
Broken link detection
|
|
123
123
|
docsValidation: true, // ā
Documentation completeness
|
|
124
124
|
envValidation: false, // ā PRO feature - env vars audit
|
|
125
|
+
// CI/CD optimization
|
|
126
|
+
ciCostAnalysis: false, // ā PRO feature - GitHub Actions cost analysis
|
|
125
127
|
roadmap: [
|
|
126
128
|
'ā
ESLint, Prettier, Stylelint configuration',
|
|
127
129
|
'ā
Basic Husky pre-commit hooks',
|
|
@@ -165,6 +167,8 @@ const FEATURES = deepFreeze({
|
|
|
165
167
|
linkValidation: true, // ā
Broken link detection
|
|
166
168
|
docsValidation: true, // ā
Documentation completeness
|
|
167
169
|
envValidation: true, // ā
Env vars audit
|
|
170
|
+
// CI/CD optimization
|
|
171
|
+
ciCostAnalysis: true, // ā
GitHub Actions cost analysis
|
|
168
172
|
roadmap: [
|
|
169
173
|
'ā
Unlimited repos and runs',
|
|
170
174
|
'ā
Smart Test Strategy (70% faster pre-push validation)',
|
|
@@ -218,6 +222,8 @@ const FEATURES = deepFreeze({
|
|
|
218
222
|
linkValidation: true,
|
|
219
223
|
docsValidation: true,
|
|
220
224
|
envValidation: true,
|
|
225
|
+
// CI/CD optimization - inherited from PRO
|
|
226
|
+
ciCostAnalysis: true,
|
|
221
227
|
roadmap: [
|
|
222
228
|
'ā
All PRO features included',
|
|
223
229
|
'ā
Per-seat licensing (5-seat minimum)',
|
|
@@ -267,6 +273,8 @@ const FEATURES = deepFreeze({
|
|
|
267
273
|
linkValidation: true,
|
|
268
274
|
docsValidation: true,
|
|
269
275
|
envValidation: true,
|
|
276
|
+
// CI/CD optimization - inherited from TEAM
|
|
277
|
+
ciCostAnalysis: true,
|
|
270
278
|
// Enterprise-specific
|
|
271
279
|
ssoIntegration: true, // SSO/SAML
|
|
272
280
|
scimReady: true,
|
|
@@ -556,6 +564,17 @@ function saveLicense(tier, key, email, expires = null) {
|
|
|
556
564
|
}
|
|
557
565
|
}
|
|
558
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
|
+
|
|
559
578
|
const licenseDir = getLicenseDir()
|
|
560
579
|
const licenseFile = getLicenseFile()
|
|
561
580
|
const normalizedKey = normalizeLicenseKey(key)
|
|
@@ -565,7 +584,7 @@ function saveLicense(tier, key, email, expires = null) {
|
|
|
565
584
|
)
|
|
566
585
|
|
|
567
586
|
if (!fs.existsSync(licenseDir)) {
|
|
568
|
-
fs.mkdirSync(licenseDir, { recursive: true })
|
|
587
|
+
fs.mkdirSync(licenseDir, { recursive: true, mode: 0o700 })
|
|
569
588
|
}
|
|
570
589
|
|
|
571
590
|
if (!privateKey) {
|
|
@@ -595,7 +614,9 @@ function saveLicense(tier, key, email, expires = null) {
|
|
|
595
614
|
signature,
|
|
596
615
|
}
|
|
597
616
|
|
|
598
|
-
fs.writeFileSync(licenseFile, JSON.stringify(licenseData, null, 2)
|
|
617
|
+
fs.writeFileSync(licenseFile, JSON.stringify(licenseData, null, 2), {
|
|
618
|
+
mode: 0o600,
|
|
619
|
+
})
|
|
599
620
|
return { success: true }
|
|
600
621
|
} catch (error) {
|
|
601
622
|
return { success: false, error: error.message }
|
|
@@ -621,7 +642,7 @@ function saveLicenseWithSignature(tier, key, email, validation) {
|
|
|
621
642
|
const normalizedKey = normalizeLicenseKey(key)
|
|
622
643
|
|
|
623
644
|
if (!fs.existsSync(licenseDir)) {
|
|
624
|
-
fs.mkdirSync(licenseDir, { recursive: true })
|
|
645
|
+
fs.mkdirSync(licenseDir, { recursive: true, mode: 0o700 })
|
|
625
646
|
}
|
|
626
647
|
|
|
627
648
|
const licenseData = {
|
|
@@ -638,7 +659,9 @@ function saveLicenseWithSignature(tier, key, email, validation) {
|
|
|
638
659
|
issued: validation.issued,
|
|
639
660
|
}
|
|
640
661
|
|
|
641
|
-
fs.writeFileSync(licenseFile, JSON.stringify(licenseData, null, 2)
|
|
662
|
+
fs.writeFileSync(licenseFile, JSON.stringify(licenseData, null, 2), {
|
|
663
|
+
mode: 0o600,
|
|
664
|
+
})
|
|
642
665
|
return { success: true }
|
|
643
666
|
} catch (error) {
|
|
644
667
|
return { success: false, error: error.message }
|
|
@@ -694,6 +717,18 @@ async function addLegitimateKey(
|
|
|
694
717
|
purchaseEmail = null
|
|
695
718
|
) {
|
|
696
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
|
+
|
|
697
732
|
const normalizedKey = normalizeLicenseKey(licenseKey)
|
|
698
733
|
// Use the same license directory as the CLI
|
|
699
734
|
const licenseDir =
|
|
@@ -716,19 +751,29 @@ async function addLegitimateKey(
|
|
|
716
751
|
try {
|
|
717
752
|
database = JSON.parse(fs.readFileSync(legitimateDBFile, 'utf8'))
|
|
718
753
|
} catch (parseError) {
|
|
719
|
-
//
|
|
754
|
+
// DR8 fix: Return error instead of continuing with corrupted database
|
|
720
755
|
const backupPath = `${legitimateDBFile}.corrupted.${Date.now()}`
|
|
756
|
+
let backupSucceeded = false
|
|
757
|
+
|
|
721
758
|
try {
|
|
722
759
|
fs.copyFileSync(legitimateDBFile, backupPath)
|
|
760
|
+
backupSucceeded = true
|
|
723
761
|
console.error(
|
|
724
|
-
|
|
762
|
+
`ā ļø Database corruption detected. Backed up to ${backupPath}`
|
|
725
763
|
)
|
|
726
|
-
} catch {
|
|
764
|
+
} catch (backupError) {
|
|
727
765
|
console.error(
|
|
728
|
-
|
|
766
|
+
`ā CRITICAL: Could not backup corrupted database: ${backupError.message}`
|
|
729
767
|
)
|
|
730
768
|
}
|
|
731
|
-
|
|
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
|
+
}
|
|
732
777
|
}
|
|
733
778
|
}
|
|
734
779
|
|
|
@@ -940,30 +985,51 @@ function loadUsage() {
|
|
|
940
985
|
// DR8 fix: Prevent quota bypass through file corruption
|
|
941
986
|
if (error instanceof SyntaxError) {
|
|
942
987
|
const usageFile = getUsageFile()
|
|
943
|
-
console.
|
|
944
|
-
console.
|
|
988
|
+
console.error(`\nā CRITICAL: Usage tracking file is corrupted`)
|
|
989
|
+
console.error(` File: ${usageFile}`)
|
|
990
|
+
console.error(` Parse error: ${error.message}\n`)
|
|
945
991
|
|
|
946
992
|
// Backup corrupted file for forensics
|
|
947
993
|
const backupPath = `${usageFile}.corrupted.${Date.now()}`
|
|
948
994
|
try {
|
|
949
995
|
fs.copyFileSync(usageFile, backupPath)
|
|
950
|
-
console.
|
|
996
|
+
console.log(` ā
Backup saved: ${backupPath}`)
|
|
951
997
|
} catch (_backupError) {
|
|
952
|
-
|
|
998
|
+
console.error(` ā Could not create backup`)
|
|
953
999
|
}
|
|
954
1000
|
|
|
955
|
-
// DR8 fix: For FREE tier, reset to max usage to prevent quota bypass
|
|
956
1001
|
const license = getLicenseInfo()
|
|
1002
|
+
|
|
957
1003
|
if (license.tier === LICENSE_TIERS.FREE) {
|
|
958
|
-
console.
|
|
959
|
-
|
|
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`
|
|
960
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
|
+
|
|
961
1024
|
const caps = FEATURES[LICENSE_TIERS.FREE]
|
|
962
1025
|
return {
|
|
963
1026
|
month: getCurrentMonth(),
|
|
964
1027
|
prePushRuns: caps.maxPrePushRunsPerMonth,
|
|
965
1028
|
dependencyPRs: caps.maxDependencyPRsPerMonth,
|
|
966
|
-
repos:
|
|
1029
|
+
repos: Array.from(
|
|
1030
|
+
{ length: caps.maxPrivateRepos },
|
|
1031
|
+
(_item, index) => `corrupted-${index + 1}`
|
|
1032
|
+
),
|
|
967
1033
|
}
|
|
968
1034
|
}
|
|
969
1035
|
} else if (process.env.DEBUG && error?.code !== 'ENOENT') {
|
|
@@ -996,14 +1062,42 @@ function saveUsage(usage) {
|
|
|
996
1062
|
try {
|
|
997
1063
|
const licenseDir = getLicenseDir()
|
|
998
1064
|
if (!fs.existsSync(licenseDir)) {
|
|
999
|
-
fs.mkdirSync(licenseDir, { recursive: true })
|
|
1065
|
+
fs.mkdirSync(licenseDir, { recursive: true, mode: 0o700 })
|
|
1000
1066
|
}
|
|
1001
|
-
fs.writeFileSync(getUsageFile(), JSON.stringify(usage, null, 2)
|
|
1067
|
+
fs.writeFileSync(getUsageFile(), JSON.stringify(usage, null, 2), {
|
|
1068
|
+
mode: 0o600,
|
|
1069
|
+
})
|
|
1002
1070
|
return true
|
|
1003
1071
|
} catch (error) {
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
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
|
+
}
|
|
1007
1101
|
}
|
|
1008
1102
|
}
|
|
1009
1103
|
|
package/lib/package-utils.js
CHANGED
|
@@ -136,7 +136,7 @@ function detectPackageManager(projectPath = process.cwd()) {
|
|
|
136
136
|
return pmName
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
|
-
} catch {
|
|
139
|
+
} catch (_error) {
|
|
140
140
|
// Ignore parse errors
|
|
141
141
|
}
|
|
142
142
|
}
|
|
@@ -204,7 +204,7 @@ function detectMonorepoType(projectPath = process.cwd()) {
|
|
|
204
204
|
try {
|
|
205
205
|
const nxJson = JSON.parse(fs.readFileSync(nxJsonPath, 'utf8'))
|
|
206
206
|
result.config = nxJson
|
|
207
|
-
} catch {
|
|
207
|
+
} catch (_error) {
|
|
208
208
|
// Ignore parse errors
|
|
209
209
|
}
|
|
210
210
|
}
|
|
@@ -218,7 +218,7 @@ function detectMonorepoType(projectPath = process.cwd()) {
|
|
|
218
218
|
try {
|
|
219
219
|
const turboJson = JSON.parse(fs.readFileSync(turboJsonPath, 'utf8'))
|
|
220
220
|
result.config = turboJson
|
|
221
|
-
} catch {
|
|
221
|
+
} catch (_error) {
|
|
222
222
|
// Ignore parse errors
|
|
223
223
|
}
|
|
224
224
|
}
|
|
@@ -240,7 +240,7 @@ function detectMonorepoType(projectPath = process.cwd()) {
|
|
|
240
240
|
try {
|
|
241
241
|
const lernaJson = JSON.parse(fs.readFileSync(lernaJsonPath, 'utf8'))
|
|
242
242
|
result.packages = lernaJson.packages || ['packages/*']
|
|
243
|
-
} catch {
|
|
243
|
+
} catch (_error) {
|
|
244
244
|
result.packages = ['packages/*']
|
|
245
245
|
}
|
|
246
246
|
}
|
|
@@ -280,7 +280,7 @@ function detectMonorepoType(projectPath = process.cwd()) {
|
|
|
280
280
|
if (packages.length > 0) {
|
|
281
281
|
result.packages = packages
|
|
282
282
|
}
|
|
283
|
-
} catch {
|
|
283
|
+
} catch (_error) {
|
|
284
284
|
// Ignore parse errors
|
|
285
285
|
}
|
|
286
286
|
}
|
|
@@ -303,7 +303,7 @@ function detectMonorepoType(projectPath = process.cwd()) {
|
|
|
303
303
|
result.packageManager === 'yarn' ? 'yarn' : result.packageManager
|
|
304
304
|
}
|
|
305
305
|
}
|
|
306
|
-
} catch {
|
|
306
|
+
} catch (_error) {
|
|
307
307
|
// Ignore parse errors
|
|
308
308
|
}
|
|
309
309
|
}
|
|
@@ -351,7 +351,7 @@ function resolveWorkspacePackages(projectPath, patterns) {
|
|
|
351
351
|
path: pkgPath,
|
|
352
352
|
relativePath: path.relative(projectPath, pkgPath),
|
|
353
353
|
})
|
|
354
|
-
} catch {
|
|
354
|
+
} catch (_error) {
|
|
355
355
|
packages.push({
|
|
356
356
|
name: entry.name,
|
|
357
357
|
path: pkgPath,
|
|
@@ -361,7 +361,7 @@ function resolveWorkspacePackages(projectPath, patterns) {
|
|
|
361
361
|
}
|
|
362
362
|
}
|
|
363
363
|
}
|
|
364
|
-
} catch {
|
|
364
|
+
} catch (_error) {
|
|
365
365
|
// Ignore read errors
|
|
366
366
|
}
|
|
367
367
|
}
|
|
@@ -377,7 +377,7 @@ function resolveWorkspacePackages(projectPath, patterns) {
|
|
|
377
377
|
path: pkgPath,
|
|
378
378
|
relativePath: pattern,
|
|
379
379
|
})
|
|
380
|
-
} catch {
|
|
380
|
+
} catch (_error) {
|
|
381
381
|
packages.push({
|
|
382
382
|
name: path.basename(pkgPath),
|
|
383
383
|
path: pkgPath,
|