create-qa-architect 5.0.7 → 5.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/auto-release.yml +49 -0
- package/.github/workflows/quality.yml +11 -11
- package/.github/workflows/shell-ci.yml.example +82 -0
- package/.github/workflows/shell-quality.yml.example +148 -0
- package/README.md +165 -12
- package/config/shell-ci.yml +82 -0
- package/config/shell-quality.yml +148 -0
- package/docs/ADOPTION-SUMMARY.md +41 -0
- package/docs/ARCHITECTURE-REVIEW.md +67 -0
- package/docs/ARCHITECTURE.md +29 -45
- package/docs/CI-COST-ANALYSIS.md +323 -0
- package/docs/CODE-REVIEW.md +100 -0
- package/docs/REQUIREMENTS.md +148 -0
- package/docs/SECURITY-AUDIT.md +68 -0
- package/docs/test-trace-matrix.md +28 -0
- package/eslint.config.cjs +2 -0
- package/lib/commands/analyze-ci.js +616 -0
- package/lib/commands/deps.js +293 -0
- package/lib/commands/index.js +29 -0
- package/lib/commands/validate.js +85 -0
- package/lib/config-validator.js +28 -45
- package/lib/error-reporter.js +14 -2
- package/lib/github-api.js +138 -13
- package/lib/license-signing.js +125 -0
- package/lib/license-validator.js +359 -71
- package/lib/licensing.js +434 -106
- package/lib/package-utils.js +9 -9
- package/lib/prelaunch-validator.js +828 -0
- package/lib/project-maturity.js +58 -6
- package/lib/quality-tools-generator.js +495 -0
- package/lib/result-types.js +112 -0
- package/lib/security-enhancements.js +1 -1
- package/lib/smart-strategy-generator.js +46 -10
- package/lib/telemetry.js +1 -1
- package/lib/template-loader.js +52 -19
- package/lib/ui-helpers.js +1 -1
- package/lib/validation/cache-manager.js +36 -6
- package/lib/validation/config-security.js +100 -33
- package/lib/validation/index.js +68 -97
- package/lib/validation/workflow-validation.js +28 -7
- package/package.json +4 -6
- package/scripts/check-test-coverage.sh +46 -0
- package/scripts/validate-claude-md.js +80 -0
- package/setup.js +923 -301
- package/create-saas-monetization.js +0 -1513
package/lib/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,50 @@ 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
|
-
*
|
|
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
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Make GitHub API request with rate limiting
|
|
152
|
+
* TD5 fix: Added rate limiting to prevent hitting GitHub's API limits
|
|
153
|
+
* DR29 fix: Sanitize errors to prevent token exposure
|
|
55
154
|
*/
|
|
56
|
-
function githubRequest(method, path, token, data = null) {
|
|
155
|
+
async function githubRequest(method, path, token, data = null) {
|
|
156
|
+
// TD5 fix: Acquire rate limit token before making request
|
|
157
|
+
await rateLimiter.acquire()
|
|
158
|
+
|
|
57
159
|
return new Promise((resolve, reject) => {
|
|
58
160
|
const options = {
|
|
59
161
|
hostname: 'api.github.com',
|
|
@@ -77,23 +179,46 @@ function githubRequest(method, path, token, data = null) {
|
|
|
77
179
|
res.on('data', chunk => (body += chunk))
|
|
78
180
|
res.on('end', () => {
|
|
79
181
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
data
|
|
83
|
-
|
|
182
|
+
// DR12 fix: Handle JSON parse errors gracefully
|
|
183
|
+
try {
|
|
184
|
+
const data = body ? JSON.parse(body) : null
|
|
185
|
+
resolve({ status: res.statusCode, data })
|
|
186
|
+
} catch (_error) {
|
|
187
|
+
// DR29 fix: Sanitize error before rejecting
|
|
188
|
+
reject(
|
|
189
|
+
sanitizeError(
|
|
190
|
+
new Error(
|
|
191
|
+
`GitHub API returned invalid JSON (status ${res.statusCode}): ${body.slice(0, 100)}`
|
|
192
|
+
),
|
|
193
|
+
token
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
}
|
|
84
197
|
} else if (res.statusCode === 204) {
|
|
85
198
|
resolve({ status: 204, data: null })
|
|
86
199
|
} else {
|
|
200
|
+
// DR12 fix: GitHub errors are usually JSON, but handle parse failures
|
|
201
|
+
let errorBody = body || res.statusMessage
|
|
202
|
+
try {
|
|
203
|
+
const errorData = JSON.parse(body)
|
|
204
|
+
errorBody = errorData.message || errorBody
|
|
205
|
+
} catch (_error) {
|
|
206
|
+
// Use raw body if JSON parse fails
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// DR29 fix: Sanitize error before rejecting
|
|
87
210
|
reject(
|
|
88
|
-
|
|
89
|
-
`GitHub API error: ${res.statusCode} - ${
|
|
211
|
+
sanitizeError(
|
|
212
|
+
new Error(`GitHub API error: ${res.statusCode} - ${errorBody}`),
|
|
213
|
+
token
|
|
90
214
|
)
|
|
91
215
|
)
|
|
92
216
|
}
|
|
93
217
|
})
|
|
94
218
|
})
|
|
95
219
|
|
|
96
|
-
|
|
220
|
+
// DR29 fix: Sanitize network errors
|
|
221
|
+
req.on('error', error => reject(sanitizeError(error, token)))
|
|
97
222
|
|
|
98
223
|
if (data) {
|
|
99
224
|
req.write(JSON.stringify(data))
|
|
@@ -0,0 +1,125 @@
|
|
|
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
|
+
/**
|
|
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
|
+
*/
|
|
39
|
+
function normalizeEmail(email) {
|
|
40
|
+
if (!email || typeof email !== 'string') return null
|
|
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
|
+
|
|
51
|
+
return normalized.length > 0 ? normalized : null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function hashEmail(email) {
|
|
55
|
+
const normalized = normalizeEmail(email)
|
|
56
|
+
if (!normalized) return null
|
|
57
|
+
return crypto.createHash('sha256').update(normalized).digest('hex')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Build a license payload for signing/verification.
|
|
62
|
+
* TD14 fix: Added input validation to prevent signature mismatches from invalid data.
|
|
63
|
+
*/
|
|
64
|
+
function buildLicensePayload({
|
|
65
|
+
licenseKey,
|
|
66
|
+
tier,
|
|
67
|
+
isFounder,
|
|
68
|
+
emailHash,
|
|
69
|
+
issued,
|
|
70
|
+
}) {
|
|
71
|
+
// TD14 fix: Validate required fields to catch issues early
|
|
72
|
+
if (!licenseKey || typeof licenseKey !== 'string') {
|
|
73
|
+
throw new Error('licenseKey is required and must be a string')
|
|
74
|
+
}
|
|
75
|
+
if (!tier || typeof tier !== 'string') {
|
|
76
|
+
throw new Error('tier is required and must be a string')
|
|
77
|
+
}
|
|
78
|
+
if (!issued || typeof issued !== 'string') {
|
|
79
|
+
throw new Error('issued is required and must be a string')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const payload = {
|
|
83
|
+
licenseKey,
|
|
84
|
+
tier,
|
|
85
|
+
isFounder: Boolean(isFounder),
|
|
86
|
+
issued,
|
|
87
|
+
}
|
|
88
|
+
if (emailHash) {
|
|
89
|
+
payload.emailHash = emailHash
|
|
90
|
+
}
|
|
91
|
+
return payload
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function signPayload(payload, privateKey) {
|
|
95
|
+
const data = Buffer.from(stableStringify(payload))
|
|
96
|
+
const signature = crypto.sign(null, data, privateKey)
|
|
97
|
+
return signature.toString('base64')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function verifyPayload(payload, signature, publicKey) {
|
|
101
|
+
const data = Buffer.from(stableStringify(payload))
|
|
102
|
+
return crypto.verify(null, data, publicKey, Buffer.from(signature, 'base64'))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function loadKeyFromEnv(envValue, envPathValue) {
|
|
106
|
+
if (envValue) return envValue
|
|
107
|
+
if (envPathValue) {
|
|
108
|
+
const fs = require('fs')
|
|
109
|
+
if (fs.existsSync(envPathValue)) {
|
|
110
|
+
return fs.readFileSync(envPathValue, 'utf8')
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = {
|
|
117
|
+
LICENSE_KEY_PATTERN,
|
|
118
|
+
stableStringify,
|
|
119
|
+
normalizeEmail,
|
|
120
|
+
hashEmail,
|
|
121
|
+
buildLicensePayload,
|
|
122
|
+
signPayload,
|
|
123
|
+
verifyPayload,
|
|
124
|
+
loadKeyFromEnv,
|
|
125
|
+
}
|