create-qa-architect 5.0.7 → 5.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/.github/workflows/auto-release.yml +49 -0
  2. package/.github/workflows/quality.yml +11 -11
  3. package/.github/workflows/shell-ci.yml.example +82 -0
  4. package/.github/workflows/shell-quality.yml.example +148 -0
  5. package/README.md +165 -12
  6. package/config/shell-ci.yml +82 -0
  7. package/config/shell-quality.yml +148 -0
  8. package/docs/ADOPTION-SUMMARY.md +41 -0
  9. package/docs/ARCHITECTURE-REVIEW.md +67 -0
  10. package/docs/ARCHITECTURE.md +29 -45
  11. package/docs/CI-COST-ANALYSIS.md +323 -0
  12. package/docs/CODE-REVIEW.md +100 -0
  13. package/docs/REQUIREMENTS.md +148 -0
  14. package/docs/SECURITY-AUDIT.md +68 -0
  15. package/docs/test-trace-matrix.md +28 -0
  16. package/eslint.config.cjs +2 -0
  17. package/lib/commands/analyze-ci.js +616 -0
  18. package/lib/commands/deps.js +293 -0
  19. package/lib/commands/index.js +29 -0
  20. package/lib/commands/validate.js +85 -0
  21. package/lib/config-validator.js +28 -45
  22. package/lib/error-reporter.js +14 -2
  23. package/lib/github-api.js +138 -13
  24. package/lib/license-signing.js +125 -0
  25. package/lib/license-validator.js +359 -71
  26. package/lib/licensing.js +434 -106
  27. package/lib/package-utils.js +9 -9
  28. package/lib/prelaunch-validator.js +828 -0
  29. package/lib/project-maturity.js +58 -6
  30. package/lib/quality-tools-generator.js +495 -0
  31. package/lib/result-types.js +112 -0
  32. package/lib/security-enhancements.js +1 -1
  33. package/lib/smart-strategy-generator.js +46 -10
  34. package/lib/telemetry.js +1 -1
  35. package/lib/template-loader.js +52 -19
  36. package/lib/ui-helpers.js +1 -1
  37. package/lib/validation/cache-manager.js +36 -6
  38. package/lib/validation/config-security.js +100 -33
  39. package/lib/validation/index.js +68 -97
  40. package/lib/validation/workflow-validation.js +28 -7
  41. package/package.json +4 -6
  42. package/scripts/check-test-coverage.sh +46 -0
  43. package/scripts/validate-claude-md.js +80 -0
  44. package/setup.js +923 -301
  45. package/create-saas-monetization.js +0 -1513
package/lib/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
- // gh CLI not available or not authenticated
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
- * Make GitHub API request
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
- resolve({
81
- status: res.statusCode,
82
- data: body ? JSON.parse(body) : null,
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
- new Error(
89
- `GitHub API error: ${res.statusCode} - ${body || res.statusMessage}`
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
- req.on('error', reject)
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
+ }