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/.github/workflows/auto-release.yml +49 -0
- package/README.md +46 -1
- package/docs/ADOPTION-SUMMARY.md +41 -0
- package/docs/ARCHITECTURE-REVIEW.md +67 -0
- package/docs/ARCHITECTURE.md +29 -45
- 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/lib/commands/deps.js +245 -0
- package/lib/commands/index.js +25 -0
- package/lib/commands/validate.js +85 -0
- package/lib/error-reporter.js +13 -1
- package/lib/github-api.js +108 -13
- package/lib/license-signing.js +110 -0
- package/lib/license-validator.js +359 -71
- package/lib/licensing.js +333 -99
- package/lib/prelaunch-validator.js +828 -0
- package/lib/quality-tools-generator.js +495 -0
- package/lib/result-types.js +112 -0
- package/lib/security-enhancements.js +1 -1
- package/lib/smart-strategy-generator.js +28 -9
- package/lib/template-loader.js +52 -19
- package/lib/validation/cache-manager.js +36 -6
- package/lib/validation/config-security.js +78 -15
- package/lib/validation/workflow-validation.js +28 -7
- package/package.json +2 -4
- package/scripts/check-test-coverage.sh +46 -0
- package/setup.js +350 -284
- package/create-saas-monetization.js +0 -1513
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# qa-architect - Test Traceability Matrix
|
|
2
|
+
|
|
3
|
+
**Generated:** 2025-12-27
|
|
4
|
+
**Coverage Target:** 50%
|
|
5
|
+
|
|
6
|
+
## Coverage Summary
|
|
7
|
+
|
|
8
|
+
| Metric | Value |
|
|
9
|
+
| ------------ | ----- |
|
|
10
|
+
| Requirements | 0 |
|
|
11
|
+
| Covered | 0 |
|
|
12
|
+
| Coverage | 0% |
|
|
13
|
+
|
|
14
|
+
## Requirement → Test Mapping
|
|
15
|
+
|
|
16
|
+
| REQ-ID | Description | Test File | Status |
|
|
17
|
+
| ----------- | ------------- | --------- | ---------- |
|
|
18
|
+
| REQ-F.01.01 | [Description] | - | ⚠️ Missing |
|
|
19
|
+
|
|
20
|
+
## Test → Requirement Mapping
|
|
21
|
+
|
|
22
|
+
| Test File | Tests | REQ-IDs | Status |
|
|
23
|
+
| --------- | ----- | ------- | ------ |
|
|
24
|
+
| - | - | - | - |
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
_Run `vbl qa` to regenerate this matrix_
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency monitoring command handler
|
|
3
|
+
*
|
|
4
|
+
* Extracted from setup.js to improve maintainability.
|
|
5
|
+
* Handles --deps, --dependency-monitoring commands.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs')
|
|
9
|
+
const path = require('path')
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
hasNpmProject,
|
|
13
|
+
generateBasicDependabotConfig,
|
|
14
|
+
writeBasicDependabotConfig,
|
|
15
|
+
} = require('../dependency-monitoring-basic')
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
generatePremiumDependabotConfig,
|
|
19
|
+
writePremiumDependabotConfig,
|
|
20
|
+
} = require('../dependency-monitoring-premium')
|
|
21
|
+
|
|
22
|
+
const {
|
|
23
|
+
getLicenseInfo,
|
|
24
|
+
showUpgradeMessage,
|
|
25
|
+
checkUsageCaps,
|
|
26
|
+
incrementUsage,
|
|
27
|
+
} = require('../licensing')
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Detect Python project
|
|
31
|
+
* @param {string} projectPath - Path to project
|
|
32
|
+
* @returns {boolean} True if Python project detected
|
|
33
|
+
*/
|
|
34
|
+
function detectPythonProject(projectPath) {
|
|
35
|
+
const pythonFiles = [
|
|
36
|
+
'pyproject.toml',
|
|
37
|
+
'requirements.txt',
|
|
38
|
+
'setup.py',
|
|
39
|
+
'Pipfile',
|
|
40
|
+
]
|
|
41
|
+
return pythonFiles.some(file => fs.existsSync(path.join(projectPath, file)))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Detect Rust project
|
|
46
|
+
* @param {string} projectPath - Path to project
|
|
47
|
+
* @returns {boolean} True if Rust project detected
|
|
48
|
+
*/
|
|
49
|
+
function detectRustProject(projectPath) {
|
|
50
|
+
return fs.existsSync(path.join(projectPath, 'Cargo.toml'))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Detect Ruby project
|
|
55
|
+
* @param {string} projectPath - Path to project
|
|
56
|
+
* @returns {boolean} True if Ruby project detected
|
|
57
|
+
*/
|
|
58
|
+
function detectRubyProject(projectPath) {
|
|
59
|
+
return fs.existsSync(path.join(projectPath, 'Gemfile'))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Handle dependency monitoring command (Free/Pro/Team/Enterprise)
|
|
64
|
+
*/
|
|
65
|
+
async function handleDependencyMonitoring() {
|
|
66
|
+
const projectPath = process.cwd()
|
|
67
|
+
const license = getLicenseInfo()
|
|
68
|
+
|
|
69
|
+
// Detect all supported ecosystems (npm, Python, Ruby, Rust, etc.)
|
|
70
|
+
const hasNpm = hasNpmProject(projectPath)
|
|
71
|
+
const hasPython = detectPythonProject(projectPath)
|
|
72
|
+
const hasRust = detectRustProject(projectPath)
|
|
73
|
+
const hasRuby = detectRubyProject(projectPath)
|
|
74
|
+
|
|
75
|
+
if (!hasNpm && !hasPython && !hasRust && !hasRuby) {
|
|
76
|
+
console.error(
|
|
77
|
+
'❌ No supported dependency file found (package.json, pyproject.toml, requirements.txt, Gemfile, Cargo.toml).'
|
|
78
|
+
)
|
|
79
|
+
console.log("💡 Make sure you're in a directory with dependency files.")
|
|
80
|
+
process.exit(1)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (hasNpm) console.log('📦 Detected: npm project')
|
|
84
|
+
if (hasPython) console.log('🐍 Detected: Python project')
|
|
85
|
+
if (hasRust) console.log('🦀 Detected: Rust project')
|
|
86
|
+
if (hasRuby) console.log('💎 Detected: Ruby project')
|
|
87
|
+
console.log(`📋 License tier: ${license.tier.toUpperCase()}`)
|
|
88
|
+
|
|
89
|
+
// Enforce Free tier caps for dependency monitoring (counted as dependency PRs)
|
|
90
|
+
if (license.tier === 'FREE') {
|
|
91
|
+
const capCheck = checkUsageCaps('dependency-pr')
|
|
92
|
+
if (!capCheck.allowed) {
|
|
93
|
+
console.error(`❌ ${capCheck.reason}`)
|
|
94
|
+
console.error(
|
|
95
|
+
' Upgrade to Pro, Team, or Enterprise for unlimited runs: https://vibebuildlab.com/qa-architect'
|
|
96
|
+
)
|
|
97
|
+
process.exit(1)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const increment = incrementUsage('dependency-pr')
|
|
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
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const dependabotPath = path.join(projectPath, '.github', 'dependabot.yml')
|
|
111
|
+
|
|
112
|
+
// Use premium or basic config based on license tier
|
|
113
|
+
const shouldUsePremium =
|
|
114
|
+
license.tier === 'PRO' ||
|
|
115
|
+
license.tier === 'TEAM' ||
|
|
116
|
+
license.tier === 'ENTERPRISE'
|
|
117
|
+
|
|
118
|
+
// Free tier only supports npm projects. Fail fast with a clear message.
|
|
119
|
+
if (!shouldUsePremium && !hasNpm && (hasPython || hasRust || hasRuby)) {
|
|
120
|
+
console.error(
|
|
121
|
+
'❌ Dependency monitoring for this project requires a Pro, Team, or Enterprise license.'
|
|
122
|
+
)
|
|
123
|
+
console.error(
|
|
124
|
+
' Free tier supports npm projects only. Detected non-npm ecosystems.'
|
|
125
|
+
)
|
|
126
|
+
console.error(
|
|
127
|
+
' Options: add npm/package.json, or upgrade and re-run: npx create-qa-architect@latest --deps after activation.'
|
|
128
|
+
)
|
|
129
|
+
process.exit(1)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (shouldUsePremium) {
|
|
133
|
+
console.log(
|
|
134
|
+
'\n🚀 Setting up framework-aware dependency monitoring (Premium)...\n'
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
const configData = generatePremiumDependabotConfig({
|
|
138
|
+
projectPath,
|
|
139
|
+
schedule: 'weekly',
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
if (configData) {
|
|
143
|
+
const { ecosystems } = configData
|
|
144
|
+
const ecosystemNames = Object.keys(ecosystems)
|
|
145
|
+
|
|
146
|
+
if (ecosystemNames.length > 0) {
|
|
147
|
+
console.log('🔍 Detected ecosystems:')
|
|
148
|
+
|
|
149
|
+
let primaryEcosystem = null
|
|
150
|
+
ecosystemNames.forEach(ecoName => {
|
|
151
|
+
const eco = ecosystems[ecoName]
|
|
152
|
+
const frameworks = Object.keys(eco.detected || {})
|
|
153
|
+
const totalPackages = frameworks.reduce((sum, fw) => {
|
|
154
|
+
return sum + (eco.detected[fw]?.count || 0)
|
|
155
|
+
}, 0)
|
|
156
|
+
|
|
157
|
+
console.log(` • ${ecoName}: ${totalPackages} packages`)
|
|
158
|
+
|
|
159
|
+
if (eco.primary) {
|
|
160
|
+
primaryEcosystem = ecoName
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
if (primaryEcosystem) {
|
|
165
|
+
console.log(`\n🎯 Primary ecosystem: ${primaryEcosystem}`)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
writePremiumDependabotConfig(configData, dependabotPath)
|
|
170
|
+
console.log('\n✅ Created .github/dependabot.yml with framework grouping')
|
|
171
|
+
|
|
172
|
+
console.log('\n🎉 Premium dependency monitoring setup complete!')
|
|
173
|
+
console.log('\n📋 What was added (Pro Tier):')
|
|
174
|
+
console.log(' • Framework-aware dependency grouping')
|
|
175
|
+
console.log(
|
|
176
|
+
` • ${Object.keys(configData.config.updates[0].groups || {}).length} dependency groups created`
|
|
177
|
+
)
|
|
178
|
+
console.log(' • Intelligent update batching (reduces PRs by 60%+)')
|
|
179
|
+
console.log(' • GitHub Actions dependency monitoring')
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
console.log('\n🔍 Setting up basic dependency monitoring (Free Tier)...\n')
|
|
183
|
+
|
|
184
|
+
const dependabotConfig = generateBasicDependabotConfig({
|
|
185
|
+
projectPath,
|
|
186
|
+
schedule: 'weekly',
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
if (dependabotConfig) {
|
|
190
|
+
writeBasicDependabotConfig(dependabotConfig, dependabotPath)
|
|
191
|
+
console.log('✅ Created .github/dependabot.yml')
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
console.log('\n🎉 Basic dependency monitoring setup complete!')
|
|
195
|
+
console.log('\n📋 What was added (Free Tier):')
|
|
196
|
+
console.log(' • Basic Dependabot configuration for npm packages')
|
|
197
|
+
console.log(' • Weekly dependency updates on Monday 9am')
|
|
198
|
+
console.log(' • GitHub Actions dependency monitoring')
|
|
199
|
+
|
|
200
|
+
// Show upgrade message for premium features
|
|
201
|
+
console.log('\n🔒 Premium features now available:')
|
|
202
|
+
console.log(' ✅ Framework-aware package grouping (React, Vue, Angular)')
|
|
203
|
+
console.log(' • Coming soon: Multi-language support (Python, Rust, Go)')
|
|
204
|
+
console.log(' • Planned: Advanced security audit workflows')
|
|
205
|
+
console.log(' • Planned: Custom update schedules and notifications')
|
|
206
|
+
|
|
207
|
+
showUpgradeMessage('Framework-Aware Dependency Grouping')
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Auto-enable Dependabot on GitHub if token available
|
|
211
|
+
console.log('\n🔧 Attempting to enable Dependabot on GitHub...')
|
|
212
|
+
try {
|
|
213
|
+
const { setupDependabot } = require('../github-api')
|
|
214
|
+
const result = await setupDependabot(projectPath, { verbose: true })
|
|
215
|
+
|
|
216
|
+
if (result.success) {
|
|
217
|
+
console.log('✅ Dependabot alerts and security updates enabled!')
|
|
218
|
+
} else if (result.errors.length > 0) {
|
|
219
|
+
console.log('⚠️ Could not auto-enable Dependabot:')
|
|
220
|
+
result.errors.forEach(err => console.log(` • ${err}`))
|
|
221
|
+
console.log('\n💡 Manual steps needed:')
|
|
222
|
+
console.log(' • Go to GitHub repo → Settings → Code security')
|
|
223
|
+
console.log(
|
|
224
|
+
' • Enable "Dependabot alerts" and "Dependabot security updates"'
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
} catch (error) {
|
|
228
|
+
console.log('⚠️ Could not auto-enable Dependabot:', error.message)
|
|
229
|
+
console.log('\n💡 Manual steps:')
|
|
230
|
+
console.log(' • Enable Dependabot in GitHub repo settings')
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
console.log('\n💡 Next steps:')
|
|
234
|
+
console.log(' • Review and commit .github/dependabot.yml')
|
|
235
|
+
console.log(
|
|
236
|
+
' • Dependabot will start monitoring weekly for dependency updates'
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
module.exports = {
|
|
241
|
+
handleDependencyMonitoring,
|
|
242
|
+
detectPythonProject,
|
|
243
|
+
detectRustProject,
|
|
244
|
+
detectRubyProject,
|
|
245
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command handlers index
|
|
3
|
+
*
|
|
4
|
+
* Centralizes CLI command handlers for better maintainability.
|
|
5
|
+
* Each command has its own module with focused functionality.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { handleValidationCommands } = require('./validate')
|
|
9
|
+
const {
|
|
10
|
+
handleDependencyMonitoring,
|
|
11
|
+
detectPythonProject,
|
|
12
|
+
detectRustProject,
|
|
13
|
+
detectRubyProject,
|
|
14
|
+
} = require('./deps')
|
|
15
|
+
|
|
16
|
+
module.exports = {
|
|
17
|
+
// Validation commands
|
|
18
|
+
handleValidationCommands,
|
|
19
|
+
|
|
20
|
+
// Dependency monitoring commands
|
|
21
|
+
handleDependencyMonitoring,
|
|
22
|
+
detectPythonProject,
|
|
23
|
+
detectRustProject,
|
|
24
|
+
detectRubyProject,
|
|
25
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation command handlers
|
|
3
|
+
*
|
|
4
|
+
* Extracted from setup.js to improve maintainability.
|
|
5
|
+
* Handles --validate, --comprehensive, --security-config, --validate-docs commands.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { ValidationRunner } = require('../validation')
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Handle validation-only commands
|
|
12
|
+
*
|
|
13
|
+
* @param {Object} options - Validation options
|
|
14
|
+
* @param {boolean} options.isConfigSecurityMode - Run config security check only
|
|
15
|
+
* @param {boolean} options.isDocsValidationMode - Run docs validation only
|
|
16
|
+
* @param {boolean} options.isComprehensiveMode - Run comprehensive validation
|
|
17
|
+
* @param {boolean} options.isValidationMode - Run validation mode
|
|
18
|
+
* @param {boolean} options.disableNpmAudit - Skip npm audit
|
|
19
|
+
* @param {boolean} options.disableGitleaks - Skip gitleaks
|
|
20
|
+
* @param {boolean} options.disableActionlint - Skip actionlint
|
|
21
|
+
* @param {boolean} options.disableMarkdownlint - Skip markdownlint
|
|
22
|
+
* @param {boolean} options.disableEslintSecurity - Skip ESLint security
|
|
23
|
+
* @param {boolean} options.allowLatestGitleaks - Allow latest gitleaks version
|
|
24
|
+
*/
|
|
25
|
+
async function handleValidationCommands(options) {
|
|
26
|
+
const {
|
|
27
|
+
isConfigSecurityMode,
|
|
28
|
+
isDocsValidationMode,
|
|
29
|
+
isComprehensiveMode,
|
|
30
|
+
isValidationMode,
|
|
31
|
+
disableNpmAudit,
|
|
32
|
+
disableGitleaks,
|
|
33
|
+
disableActionlint,
|
|
34
|
+
disableMarkdownlint,
|
|
35
|
+
disableEslintSecurity,
|
|
36
|
+
allowLatestGitleaks,
|
|
37
|
+
} = options
|
|
38
|
+
|
|
39
|
+
const validationOptions = {
|
|
40
|
+
disableNpmAudit,
|
|
41
|
+
disableGitleaks,
|
|
42
|
+
disableActionlint,
|
|
43
|
+
disableMarkdownlint,
|
|
44
|
+
disableEslintSecurity,
|
|
45
|
+
allowLatestGitleaks,
|
|
46
|
+
}
|
|
47
|
+
const validator = new ValidationRunner(validationOptions)
|
|
48
|
+
|
|
49
|
+
if (isConfigSecurityMode) {
|
|
50
|
+
try {
|
|
51
|
+
await validator.runConfigSecurity()
|
|
52
|
+
process.exit(0)
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error(
|
|
55
|
+
`\n❌ Configuration security validation failed:\n${error.message}`
|
|
56
|
+
)
|
|
57
|
+
process.exit(1)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (isDocsValidationMode) {
|
|
62
|
+
try {
|
|
63
|
+
await validator.runDocumentationValidation()
|
|
64
|
+
process.exit(0)
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error(`\n❌ Documentation validation failed:\n${error.message}`)
|
|
67
|
+
process.exit(1)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (isComprehensiveMode || isValidationMode) {
|
|
72
|
+
try {
|
|
73
|
+
// Use parallel validation for 3-5x speedup (runs checks concurrently)
|
|
74
|
+
await validator.runComprehensiveCheckParallel()
|
|
75
|
+
process.exit(0)
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error(`\n❌ Comprehensive validation failed:\n${error.message}`)
|
|
78
|
+
process.exit(1)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = {
|
|
84
|
+
handleValidationCommands,
|
|
85
|
+
}
|
package/lib/error-reporter.js
CHANGED
|
@@ -253,13 +253,25 @@ class ErrorReporter {
|
|
|
253
253
|
const category = categorizeError(error)
|
|
254
254
|
const reportId = generateReportId()
|
|
255
255
|
|
|
256
|
+
// DR20 fix: Limit stack trace exposure in production mode
|
|
257
|
+
const isProduction = process.env.NODE_ENV === 'production'
|
|
258
|
+
const fullStack = sanitizeStackTrace(error?.stack || '')
|
|
259
|
+
|
|
260
|
+
// In production: only include first 3 lines of stack (error + top 2 frames)
|
|
261
|
+
// In dev/test: include full sanitized stack for debugging
|
|
262
|
+
const stackToInclude =
|
|
263
|
+
isProduction && fullStack
|
|
264
|
+
? fullStack.split('\n').slice(0, 3).join('\n')
|
|
265
|
+
: fullStack
|
|
266
|
+
|
|
256
267
|
const report = {
|
|
257
268
|
id: reportId,
|
|
258
269
|
timestamp: new Date().toISOString(),
|
|
259
270
|
category,
|
|
260
271
|
errorType: error?.constructor?.name || 'Error',
|
|
261
272
|
message: sanitizeMessage(error?.message || 'Unknown error'),
|
|
262
|
-
sanitizedStack:
|
|
273
|
+
sanitizedStack: stackToInclude,
|
|
274
|
+
stackTruncated: isProduction && fullStack.split('\n').length > 3,
|
|
263
275
|
operation: this.operation,
|
|
264
276
|
context: {
|
|
265
277
|
nodeVersion: process.version,
|
package/lib/github-api.js
CHANGED
|
@@ -6,6 +6,63 @@
|
|
|
6
6
|
const https = require('https')
|
|
7
7
|
const { execSync } = require('child_process')
|
|
8
8
|
|
|
9
|
+
// TD5 fix: Simple rate limiter for GitHub API
|
|
10
|
+
// GitHub allows 5000 requests/hour for authenticated requests
|
|
11
|
+
const rateLimiter = {
|
|
12
|
+
tokens: 100, // Start with 100 tokens
|
|
13
|
+
maxTokens: 100,
|
|
14
|
+
refillRate: 100 / 3600, // Refill ~100 tokens per hour
|
|
15
|
+
lastRefill: Date.now(),
|
|
16
|
+
minDelayMs: 100, // Minimum delay between requests
|
|
17
|
+
|
|
18
|
+
async acquire() {
|
|
19
|
+
try {
|
|
20
|
+
// Refill tokens based on time elapsed
|
|
21
|
+
const now = Date.now()
|
|
22
|
+
const elapsed = (now - this.lastRefill) / 1000
|
|
23
|
+
const refilled = elapsed * this.refillRate
|
|
24
|
+
|
|
25
|
+
// DR5 fix: Validate math to prevent NaN/Infinity
|
|
26
|
+
if (!Number.isFinite(refilled) || refilled < 0) {
|
|
27
|
+
console.warn('⚠️ Rate limiter math error, resetting')
|
|
28
|
+
this.tokens = this.maxTokens
|
|
29
|
+
this.lastRefill = now
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
this.tokens = Math.min(this.maxTokens, this.tokens + refilled)
|
|
34
|
+
this.lastRefill = now
|
|
35
|
+
|
|
36
|
+
// If we have tokens, use one
|
|
37
|
+
if (this.tokens >= 1) {
|
|
38
|
+
this.tokens -= 1
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Wait before allowing request
|
|
43
|
+
const waitTime = Math.max(
|
|
44
|
+
this.minDelayMs,
|
|
45
|
+
((1 - this.tokens) / this.refillRate) * 1000
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
// DR5 fix: Validate waitTime before setTimeout
|
|
49
|
+
if (!Number.isFinite(waitTime) || waitTime < 0 || waitTime > 60000) {
|
|
50
|
+
console.warn(
|
|
51
|
+
`⚠️ Rate limiter computed invalid wait time: ${waitTime}ms, using minimum`
|
|
52
|
+
)
|
|
53
|
+
await new Promise(resolve => setTimeout(resolve, this.minDelayMs))
|
|
54
|
+
} else {
|
|
55
|
+
await new Promise(resolve => setTimeout(resolve, waitTime))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.tokens = 0
|
|
59
|
+
} catch (error) {
|
|
60
|
+
// DR5 fix: Don't block on rate limiter errors, just log and proceed
|
|
61
|
+
console.error(`❌ Rate limiter error: ${error.message}`)
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
|
|
9
66
|
/**
|
|
10
67
|
* Get GitHub token from environment or gh CLI
|
|
11
68
|
*/
|
|
@@ -15,12 +72,21 @@ function getGitHubToken() {
|
|
|
15
72
|
return process.env.GITHUB_TOKEN
|
|
16
73
|
}
|
|
17
74
|
|
|
18
|
-
// Try to get from gh CLI
|
|
75
|
+
// Try to get from gh CLI (hardcoded command - no injection risk)
|
|
19
76
|
try {
|
|
20
77
|
const token = execSync('gh auth token', { encoding: 'utf8' }).trim()
|
|
21
78
|
if (token) return token
|
|
22
|
-
} catch {
|
|
23
|
-
//
|
|
79
|
+
} catch (error) {
|
|
80
|
+
// Silent failure fix: Log unexpected errors for debugging
|
|
81
|
+
// ENOENT = gh not installed (expected), other errors should be visible in DEBUG mode
|
|
82
|
+
if (
|
|
83
|
+
error?.code !== 'ENOENT' &&
|
|
84
|
+
!error?.message?.includes('command not found')
|
|
85
|
+
) {
|
|
86
|
+
if (process.env.DEBUG) {
|
|
87
|
+
console.warn(`⚠️ gh auth token failed: ${error.message}`)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
24
90
|
}
|
|
25
91
|
|
|
26
92
|
return null
|
|
@@ -28,6 +94,7 @@ function getGitHubToken() {
|
|
|
28
94
|
|
|
29
95
|
/**
|
|
30
96
|
* Get repository info from git remote
|
|
97
|
+
* Uses hardcoded git command - no injection risk
|
|
31
98
|
*/
|
|
32
99
|
function getRepoInfo(projectPath = '.') {
|
|
33
100
|
try {
|
|
@@ -45,15 +112,29 @@ function getRepoInfo(projectPath = '.') {
|
|
|
45
112
|
}
|
|
46
113
|
|
|
47
114
|
return null
|
|
48
|
-
} catch {
|
|
115
|
+
} catch (error) {
|
|
116
|
+
// Silent failure fix: Log unexpected errors for debugging
|
|
117
|
+
// "No such remote" is expected when origin isn't configured
|
|
118
|
+
if (
|
|
119
|
+
!error?.stderr?.includes('No such remote') &&
|
|
120
|
+
error?.code !== 'ENOENT'
|
|
121
|
+
) {
|
|
122
|
+
if (process.env.DEBUG) {
|
|
123
|
+
console.warn(`⚠️ git remote get-url failed: ${error.message}`)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
49
126
|
return null
|
|
50
127
|
}
|
|
51
128
|
}
|
|
52
129
|
|
|
53
130
|
/**
|
|
54
|
-
* Make GitHub API request
|
|
131
|
+
* Make GitHub API request with rate limiting
|
|
132
|
+
* TD5 fix: Added rate limiting to prevent hitting GitHub's API limits
|
|
55
133
|
*/
|
|
56
|
-
function githubRequest(method, path, token, data = null) {
|
|
134
|
+
async function githubRequest(method, path, token, data = null) {
|
|
135
|
+
// TD5 fix: Acquire rate limit token before making request
|
|
136
|
+
await rateLimiter.acquire()
|
|
137
|
+
|
|
57
138
|
return new Promise((resolve, reject) => {
|
|
58
139
|
const options = {
|
|
59
140
|
hostname: 'api.github.com',
|
|
@@ -77,17 +158,31 @@ function githubRequest(method, path, token, data = null) {
|
|
|
77
158
|
res.on('data', chunk => (body += chunk))
|
|
78
159
|
res.on('end', () => {
|
|
79
160
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
data
|
|
83
|
-
|
|
161
|
+
// DR12 fix: Handle JSON parse errors gracefully
|
|
162
|
+
try {
|
|
163
|
+
const data = body ? JSON.parse(body) : null
|
|
164
|
+
resolve({ status: res.statusCode, data })
|
|
165
|
+
} catch (_error) {
|
|
166
|
+
reject(
|
|
167
|
+
new Error(
|
|
168
|
+
`GitHub API returned invalid JSON (status ${res.statusCode}): ${body.slice(0, 100)}`
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
}
|
|
84
172
|
} else if (res.statusCode === 204) {
|
|
85
173
|
resolve({ status: 204, data: null })
|
|
86
174
|
} else {
|
|
175
|
+
// DR12 fix: GitHub errors are usually JSON, but handle parse failures
|
|
176
|
+
let errorBody = body || res.statusMessage
|
|
177
|
+
try {
|
|
178
|
+
const errorData = JSON.parse(body)
|
|
179
|
+
errorBody = errorData.message || errorBody
|
|
180
|
+
} catch (_error) {
|
|
181
|
+
// Use raw body if JSON parse fails
|
|
182
|
+
}
|
|
183
|
+
|
|
87
184
|
reject(
|
|
88
|
-
new Error(
|
|
89
|
-
`GitHub API error: ${res.statusCode} - ${body || res.statusMessage}`
|
|
90
|
-
)
|
|
185
|
+
new Error(`GitHub API error: ${res.statusCode} - ${errorBody}`)
|
|
91
186
|
)
|
|
92
187
|
}
|
|
93
188
|
})
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto')
|
|
4
|
+
|
|
5
|
+
// TD15 fix: Single source of truth for license key format validation
|
|
6
|
+
const LICENSE_KEY_PATTERN =
|
|
7
|
+
/^QAA-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Deterministic JSON stringify with sorted keys for signature verification.
|
|
11
|
+
* TD13 fix: Added circular reference protection using WeakSet.
|
|
12
|
+
*/
|
|
13
|
+
function stableStringify(value, seen = new WeakSet()) {
|
|
14
|
+
if (value === null || typeof value !== 'object') {
|
|
15
|
+
return JSON.stringify(value)
|
|
16
|
+
}
|
|
17
|
+
// TD13 fix: Detect circular references to prevent stack overflow
|
|
18
|
+
if (seen.has(value)) {
|
|
19
|
+
throw new Error('Circular reference detected in payload - cannot serialize')
|
|
20
|
+
}
|
|
21
|
+
seen.add(value)
|
|
22
|
+
|
|
23
|
+
if (Array.isArray(value)) {
|
|
24
|
+
return `[${value.map(item => stableStringify(item, seen)).join(',')}]`
|
|
25
|
+
}
|
|
26
|
+
const keys = Object.keys(value).sort()
|
|
27
|
+
const entries = keys.map(
|
|
28
|
+
key => `${JSON.stringify(key)}:${stableStringify(value[key], seen)}`
|
|
29
|
+
)
|
|
30
|
+
return `{${entries.join(',')}}`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeEmail(email) {
|
|
34
|
+
if (!email || typeof email !== 'string') return null
|
|
35
|
+
const normalized = email.trim().toLowerCase()
|
|
36
|
+
return normalized.length > 0 ? normalized : null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function hashEmail(email) {
|
|
40
|
+
const normalized = normalizeEmail(email)
|
|
41
|
+
if (!normalized) return null
|
|
42
|
+
return crypto.createHash('sha256').update(normalized).digest('hex')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build a license payload for signing/verification.
|
|
47
|
+
* TD14 fix: Added input validation to prevent signature mismatches from invalid data.
|
|
48
|
+
*/
|
|
49
|
+
function buildLicensePayload({
|
|
50
|
+
licenseKey,
|
|
51
|
+
tier,
|
|
52
|
+
isFounder,
|
|
53
|
+
emailHash,
|
|
54
|
+
issued,
|
|
55
|
+
}) {
|
|
56
|
+
// TD14 fix: Validate required fields to catch issues early
|
|
57
|
+
if (!licenseKey || typeof licenseKey !== 'string') {
|
|
58
|
+
throw new Error('licenseKey is required and must be a string')
|
|
59
|
+
}
|
|
60
|
+
if (!tier || typeof tier !== 'string') {
|
|
61
|
+
throw new Error('tier is required and must be a string')
|
|
62
|
+
}
|
|
63
|
+
if (!issued || typeof issued !== 'string') {
|
|
64
|
+
throw new Error('issued is required and must be a string')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const payload = {
|
|
68
|
+
licenseKey,
|
|
69
|
+
tier,
|
|
70
|
+
isFounder: Boolean(isFounder),
|
|
71
|
+
issued,
|
|
72
|
+
}
|
|
73
|
+
if (emailHash) {
|
|
74
|
+
payload.emailHash = emailHash
|
|
75
|
+
}
|
|
76
|
+
return payload
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function signPayload(payload, privateKey) {
|
|
80
|
+
const data = Buffer.from(stableStringify(payload))
|
|
81
|
+
const signature = crypto.sign(null, data, privateKey)
|
|
82
|
+
return signature.toString('base64')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function verifyPayload(payload, signature, publicKey) {
|
|
86
|
+
const data = Buffer.from(stableStringify(payload))
|
|
87
|
+
return crypto.verify(null, data, publicKey, Buffer.from(signature, 'base64'))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function loadKeyFromEnv(envValue, envPathValue) {
|
|
91
|
+
if (envValue) return envValue
|
|
92
|
+
if (envPathValue) {
|
|
93
|
+
const fs = require('fs')
|
|
94
|
+
if (fs.existsSync(envPathValue)) {
|
|
95
|
+
return fs.readFileSync(envPathValue, 'utf8')
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = {
|
|
102
|
+
LICENSE_KEY_PATTERN,
|
|
103
|
+
stableStringify,
|
|
104
|
+
normalizeEmail,
|
|
105
|
+
hashEmail,
|
|
106
|
+
buildLicensePayload,
|
|
107
|
+
signPayload,
|
|
108
|
+
verifyPayload,
|
|
109
|
+
loadKeyFromEnv,
|
|
110
|
+
}
|