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
package/lib/license-validator.js
CHANGED
|
@@ -10,13 +10,72 @@ const fs = require('fs')
|
|
|
10
10
|
const path = require('path')
|
|
11
11
|
const os = require('os')
|
|
12
12
|
const crypto = require('crypto')
|
|
13
|
+
const {
|
|
14
|
+
LICENSE_KEY_PATTERN,
|
|
15
|
+
buildLicensePayload,
|
|
16
|
+
hashEmail,
|
|
17
|
+
verifyPayload,
|
|
18
|
+
stableStringify,
|
|
19
|
+
loadKeyFromEnv,
|
|
20
|
+
} = require('./license-signing')
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* TD10 fix: Timing-safe string comparison to prevent timing attacks
|
|
24
|
+
* on hash/signature verification.
|
|
25
|
+
*
|
|
26
|
+
* Security enhancement: Uses constant-time operations for length check
|
|
27
|
+
* to avoid leaking length information through timing side-channels.
|
|
28
|
+
*/
|
|
29
|
+
function timingSafeEqual(a, b) {
|
|
30
|
+
if (typeof a !== 'string' || typeof b !== 'string') return false
|
|
31
|
+
|
|
32
|
+
// Use constant-time length comparison to avoid timing leaks
|
|
33
|
+
const maxLen = Math.max(a.length, b.length, 1)
|
|
34
|
+
const bufA = Buffer.alloc(maxLen, 0)
|
|
35
|
+
const bufB = Buffer.alloc(maxLen, 0)
|
|
36
|
+
|
|
37
|
+
// Write actual values - this is constant time regardless of length
|
|
38
|
+
Buffer.from(a, 'utf8').copy(bufA)
|
|
39
|
+
Buffer.from(b, 'utf8').copy(bufB)
|
|
40
|
+
|
|
41
|
+
// Both comparisons happen regardless of results (no early return)
|
|
42
|
+
const lengthsMatch = a.length === b.length
|
|
43
|
+
const contentsMatch = crypto.timingSafeEqual(bufA, bufB)
|
|
44
|
+
|
|
45
|
+
return lengthsMatch && contentsMatch
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Validate that a license directory path is safe (no path traversal)
|
|
50
|
+
* Security: Prevents attackers from using QAA_LICENSE_DIR to access arbitrary paths
|
|
51
|
+
*/
|
|
52
|
+
function validateLicenseDir(dirPath) {
|
|
53
|
+
const resolved = path.resolve(dirPath)
|
|
54
|
+
const home = os.homedir()
|
|
55
|
+
const tmp = os.tmpdir()
|
|
56
|
+
|
|
57
|
+
// Must be within home directory or temp directory (for tests)
|
|
58
|
+
const isInHome = resolved.startsWith(home + path.sep) || resolved === home
|
|
59
|
+
const isInTmp = resolved.startsWith(tmp + path.sep) || resolved === tmp
|
|
60
|
+
|
|
61
|
+
if (!isInHome && !isInTmp) {
|
|
62
|
+
console.warn(
|
|
63
|
+
`⚠️ QAA_LICENSE_DIR must be within home or temp directory, ignoring: ${dirPath}`
|
|
64
|
+
)
|
|
65
|
+
return path.join(home, '.create-qa-architect')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return resolved
|
|
69
|
+
}
|
|
13
70
|
|
|
14
71
|
class LicenseValidator {
|
|
15
72
|
constructor() {
|
|
16
73
|
// Support environment variable override for testing (like telemetry/error-reporter)
|
|
17
|
-
|
|
74
|
+
// Security: Validate path to prevent traversal attacks
|
|
75
|
+
const requestedDir =
|
|
18
76
|
process.env.QAA_LICENSE_DIR ||
|
|
19
77
|
path.join(os.homedir(), '.create-qa-architect')
|
|
78
|
+
this.licenseDir = validateLicenseDir(requestedDir)
|
|
20
79
|
this.licenseFile = path.join(this.licenseDir, 'license.json')
|
|
21
80
|
this.legitimateDBFile = path.join(
|
|
22
81
|
this.licenseDir,
|
|
@@ -27,6 +86,21 @@ class LicenseValidator {
|
|
|
27
86
|
this.licenseDbUrl =
|
|
28
87
|
process.env.QAA_LICENSE_DB_URL ||
|
|
29
88
|
'https://vibebuildlab.com/api/licenses/qa-architect.json'
|
|
89
|
+
|
|
90
|
+
this.licensePublicKey = loadKeyFromEnv(
|
|
91
|
+
process.env.QAA_LICENSE_PUBLIC_KEY,
|
|
92
|
+
process.env.QAA_LICENSE_PUBLIC_KEY_PATH
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
// DR14 fix: Add in-memory cache with 5-minute TTL
|
|
96
|
+
this.dbCache = null
|
|
97
|
+
this.dbCacheTime = 0
|
|
98
|
+
this.cacheTTL = 5 * 60 * 1000 // 5 minutes
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
normalizeLicenseKey(key) {
|
|
102
|
+
if (typeof key !== 'string') return ''
|
|
103
|
+
return key.trim().toUpperCase()
|
|
30
104
|
}
|
|
31
105
|
|
|
32
106
|
ensureLicenseDir() {
|
|
@@ -69,19 +143,43 @@ class LicenseValidator {
|
|
|
69
143
|
* Load legitimate licenses from the cached database
|
|
70
144
|
*/
|
|
71
145
|
loadLegitimateDatabase() {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
146
|
+
// File doesn't exist - expected for first run
|
|
147
|
+
if (!fs.existsSync(this.legitimateDBFile)) {
|
|
148
|
+
return {}
|
|
149
|
+
}
|
|
76
150
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
151
|
+
try {
|
|
152
|
+
const data = fs.readFileSync(this.legitimateDBFile, 'utf8')
|
|
153
|
+
const parsed = JSON.parse(data)
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
this.verifyRegistrySignature(parsed)
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.warn(`⚠️ Cached license database invalid: ${error.message}`)
|
|
159
|
+
return {}
|
|
80
160
|
}
|
|
161
|
+
|
|
162
|
+
// Remove metadata for license lookup
|
|
163
|
+
const { _metadata, ...licenses } = parsed
|
|
164
|
+
return licenses
|
|
81
165
|
} catch (error) {
|
|
82
|
-
|
|
166
|
+
// Silent failure fix: Differentiate error types for actionable messages
|
|
167
|
+
if (error.code === 'EACCES') {
|
|
168
|
+
console.error(
|
|
169
|
+
`❌ Permission denied reading license database: ${this.legitimateDBFile}`
|
|
170
|
+
)
|
|
171
|
+
} else if (error instanceof SyntaxError) {
|
|
172
|
+
console.error(
|
|
173
|
+
`❌ License database is corrupted (invalid JSON): ${error.message}`
|
|
174
|
+
)
|
|
175
|
+
} else {
|
|
176
|
+
console.error(
|
|
177
|
+
'Error loading legitimate license database:',
|
|
178
|
+
error.message
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
return {}
|
|
83
182
|
}
|
|
84
|
-
return {}
|
|
85
183
|
}
|
|
86
184
|
|
|
87
185
|
/**
|
|
@@ -95,12 +193,31 @@ class LicenseValidator {
|
|
|
95
193
|
* Fetch latest legitimate licenses from server (if available)
|
|
96
194
|
*/
|
|
97
195
|
async fetchLegitimateDatabase() {
|
|
196
|
+
// DR14 fix: Check in-memory cache first (5-min TTL)
|
|
197
|
+
const now = Date.now()
|
|
198
|
+
if (this.dbCache && now - this.dbCacheTime < this.cacheTTL) {
|
|
199
|
+
console.log('✅ Using cached license database (fresh)')
|
|
200
|
+
return this.dbCache
|
|
201
|
+
}
|
|
202
|
+
|
|
98
203
|
try {
|
|
99
204
|
this.ensureLicenseDir()
|
|
100
205
|
console.log(
|
|
101
206
|
`🔄 Fetching latest license database from ${this.licenseDbUrl} ...`
|
|
102
207
|
)
|
|
103
208
|
|
|
209
|
+
const parsedUrl = new URL(this.licenseDbUrl)
|
|
210
|
+
const isTest = process.argv.join(' ').includes('test')
|
|
211
|
+
if (
|
|
212
|
+
parsedUrl.protocol !== 'https:' &&
|
|
213
|
+
!process.env.QAA_ALLOW_INSECURE_LICENSE_DB &&
|
|
214
|
+
!isTest
|
|
215
|
+
) {
|
|
216
|
+
throw new Error(
|
|
217
|
+
'license database URL must use HTTPS (set QAA_ALLOW_INSECURE_LICENSE_DB=1 to override)'
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
|
|
104
221
|
const controller = new AbortController()
|
|
105
222
|
const timeout = setTimeout(() => controller.abort(), 10000)
|
|
106
223
|
const response = await fetch(this.licenseDbUrl, {
|
|
@@ -119,27 +236,48 @@ class LicenseValidator {
|
|
|
119
236
|
throw new Error('invalid database format')
|
|
120
237
|
}
|
|
121
238
|
|
|
122
|
-
|
|
123
|
-
if (!database._metadata.sha256) {
|
|
124
|
-
throw new Error('license database missing sha256 checksum')
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const { _metadata, ...licenses } = database
|
|
128
|
-
const computed = this.computeSha256(JSON.stringify(licenses))
|
|
129
|
-
if (computed !== database._metadata.sha256) {
|
|
130
|
-
throw new Error('license database integrity check failed')
|
|
131
|
-
}
|
|
239
|
+
this.verifyRegistrySignature(database)
|
|
132
240
|
|
|
133
241
|
// Cache locally for offline use
|
|
134
242
|
fs.writeFileSync(this.legitimateDBFile, JSON.stringify(database, null, 2))
|
|
135
243
|
console.log('✅ License database updated and cached')
|
|
136
244
|
|
|
245
|
+
// Remove metadata for license lookup
|
|
246
|
+
const { _metadata, ...licenses } = database
|
|
247
|
+
|
|
248
|
+
// DR14 fix: Update in-memory cache
|
|
249
|
+
this.dbCache = licenses
|
|
250
|
+
this.dbCacheTime = Date.now()
|
|
251
|
+
|
|
137
252
|
return licenses
|
|
138
253
|
} catch (error) {
|
|
139
254
|
const isTest = process.argv.join(' ').includes('test')
|
|
140
255
|
const prefix = isTest ? '📋 TEST SCENARIO:' : '⚠️'
|
|
141
256
|
console.log(`${prefix} Database fetch failed: ${error.message}`)
|
|
142
|
-
|
|
257
|
+
|
|
258
|
+
// DR2 fix: Check if cached database has data before falling back
|
|
259
|
+
const cachedDb = this.loadLegitimateDatabase()
|
|
260
|
+
const hasCachedData = Object.keys(cachedDb || {}).length > 0
|
|
261
|
+
|
|
262
|
+
if (!hasCachedData) {
|
|
263
|
+
console.error('❌ No cached license database available')
|
|
264
|
+
console.error(
|
|
265
|
+
' You need an internet connection to activate new licenses'
|
|
266
|
+
)
|
|
267
|
+
console.error(' If you recently purchased, please retry when online')
|
|
268
|
+
throw new Error(
|
|
269
|
+
'Cannot validate license: network unavailable and no cached database. Please connect to the internet and retry.'
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Only fall back to cache if it has data
|
|
274
|
+
console.log(
|
|
275
|
+
`✅ Using cached database (${Object.keys(cachedDb).length} licenses)`
|
|
276
|
+
)
|
|
277
|
+
console.log(
|
|
278
|
+
' Note: Cache may be outdated. Connect to internet for latest licenses.'
|
|
279
|
+
)
|
|
280
|
+
return cachedDb
|
|
143
281
|
}
|
|
144
282
|
}
|
|
145
283
|
|
|
@@ -148,11 +286,12 @@ class LicenseValidator {
|
|
|
148
286
|
*/
|
|
149
287
|
async validateLicense(licenseKey, userEmail) {
|
|
150
288
|
try {
|
|
289
|
+
const normalizedKey = this.normalizeLicenseKey(licenseKey)
|
|
151
290
|
// Check if already activated locally
|
|
152
291
|
const localLicense = this.getLocalLicense()
|
|
153
292
|
if (
|
|
154
293
|
localLicense &&
|
|
155
|
-
localLicense.licenseKey ===
|
|
294
|
+
localLicense.licenseKey === normalizedKey &&
|
|
156
295
|
localLicense.valid
|
|
157
296
|
) {
|
|
158
297
|
return {
|
|
@@ -168,7 +307,7 @@ class LicenseValidator {
|
|
|
168
307
|
const legitimateDB = await this.fetchLegitimateDatabase()
|
|
169
308
|
|
|
170
309
|
// If database is empty (no licenses), fail with actionable message
|
|
171
|
-
const licenseInfo = legitimateDB[
|
|
310
|
+
const licenseInfo = legitimateDB[normalizedKey]
|
|
172
311
|
const hasLicenses = Object.keys(legitimateDB || {}).length > 0
|
|
173
312
|
|
|
174
313
|
if (!hasLicenses) {
|
|
@@ -187,10 +326,21 @@ class LicenseValidator {
|
|
|
187
326
|
}
|
|
188
327
|
}
|
|
189
328
|
|
|
329
|
+
// DR21 fix: Validate email format before hashing to prevent timing attacks on invalid emails
|
|
330
|
+
if (userEmail && !userEmail.includes('@')) {
|
|
331
|
+
return {
|
|
332
|
+
valid: false,
|
|
333
|
+
error: 'Invalid email format. Please provide a valid email address.',
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
190
337
|
// Verify email matches (if specified in database)
|
|
338
|
+
// TD10 fix: Use timing-safe comparison to prevent timing attacks
|
|
339
|
+
const emailHash = hashEmail(userEmail)
|
|
191
340
|
if (
|
|
192
|
-
|
|
193
|
-
licenseInfo.
|
|
341
|
+
emailHash &&
|
|
342
|
+
licenseInfo.emailHash &&
|
|
343
|
+
!timingSafeEqual(licenseInfo.emailHash, emailHash)
|
|
194
344
|
) {
|
|
195
345
|
return {
|
|
196
346
|
valid: false,
|
|
@@ -199,6 +349,29 @@ class LicenseValidator {
|
|
|
199
349
|
}
|
|
200
350
|
}
|
|
201
351
|
|
|
352
|
+
const payload = buildLicensePayload({
|
|
353
|
+
licenseKey: normalizedKey,
|
|
354
|
+
tier: licenseInfo.tier,
|
|
355
|
+
isFounder: licenseInfo.isFounder,
|
|
356
|
+
emailHash: licenseInfo.emailHash,
|
|
357
|
+
issued: licenseInfo.issued,
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
if (!licenseInfo.signature) {
|
|
361
|
+
return {
|
|
362
|
+
valid: false,
|
|
363
|
+
error: 'License entry missing signature. Please contact support.',
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (!this.verifySignature(payload, licenseInfo.signature)) {
|
|
368
|
+
return {
|
|
369
|
+
valid: false,
|
|
370
|
+
error:
|
|
371
|
+
'License entry signature verification failed. Please contact support.',
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
202
375
|
// License is valid
|
|
203
376
|
console.log(
|
|
204
377
|
`✅ License validated: ${licenseInfo.tier} ${licenseInfo.isFounder ? '(Founder)' : ''}`
|
|
@@ -210,6 +383,8 @@ class LicenseValidator {
|
|
|
210
383
|
isFounder: licenseInfo.isFounder || false,
|
|
211
384
|
customerId: licenseInfo.customerId,
|
|
212
385
|
email: userEmail,
|
|
386
|
+
signature: licenseInfo.signature,
|
|
387
|
+
payload,
|
|
213
388
|
source: 'legitimate_database',
|
|
214
389
|
}
|
|
215
390
|
} catch (error) {
|
|
@@ -226,26 +401,57 @@ class LicenseValidator {
|
|
|
226
401
|
* Get local license file if it exists
|
|
227
402
|
*/
|
|
228
403
|
getLocalLicense() {
|
|
229
|
-
if (fs.existsSync(this.licenseFile)) {
|
|
230
|
-
|
|
231
|
-
|
|
404
|
+
if (!fs.existsSync(this.licenseFile)) {
|
|
405
|
+
return null
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
try {
|
|
409
|
+
const content = fs.readFileSync(this.licenseFile, 'utf8')
|
|
410
|
+
const license = JSON.parse(content)
|
|
411
|
+
const normalizedKey = this.normalizeLicenseKey(
|
|
412
|
+
license.licenseKey || license.key
|
|
413
|
+
)
|
|
232
414
|
|
|
233
|
-
// Check signature if present
|
|
234
415
|
if (license.payload && license.signature) {
|
|
235
416
|
const isValid = this.verifySignature(license.payload, license.signature)
|
|
236
|
-
return {
|
|
417
|
+
return {
|
|
418
|
+
...license,
|
|
419
|
+
licenseKey: normalizedKey,
|
|
420
|
+
valid: isValid,
|
|
421
|
+
}
|
|
237
422
|
}
|
|
238
423
|
|
|
239
|
-
// Legacy format
|
|
424
|
+
// Legacy format (unsigned) is no longer trusted
|
|
240
425
|
return {
|
|
241
426
|
...license,
|
|
242
|
-
|
|
427
|
+
licenseKey: normalizedKey,
|
|
428
|
+
valid: false,
|
|
243
429
|
tier: license.tier,
|
|
244
|
-
licenseKey: license.licenseKey || license.key,
|
|
245
430
|
email: license.email,
|
|
246
431
|
}
|
|
432
|
+
} catch (error) {
|
|
433
|
+
// DR3 fix: Handle corrupted license files gracefully
|
|
434
|
+
if (error instanceof SyntaxError) {
|
|
435
|
+
console.error('❌ License file is corrupted (invalid JSON)')
|
|
436
|
+
console.error(` File: ${this.licenseFile}`)
|
|
437
|
+
console.error(' Please re-activate your license:')
|
|
438
|
+
console.error(' npx create-qa-architect@latest --activate-license')
|
|
439
|
+
|
|
440
|
+
// Backup corrupted file
|
|
441
|
+
const backupPath = `${this.licenseFile}.corrupted.${Date.now()}`
|
|
442
|
+
try {
|
|
443
|
+
fs.copyFileSync(this.licenseFile, backupPath)
|
|
444
|
+
console.error(` Corrupted file backed up to: ${backupPath}`)
|
|
445
|
+
} catch (_backupError) {
|
|
446
|
+
// Ignore backup failures
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return null
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
console.error(`❌ Error reading license file: ${error.message}`)
|
|
453
|
+
return null
|
|
247
454
|
}
|
|
248
|
-
return null
|
|
249
455
|
}
|
|
250
456
|
|
|
251
457
|
/**
|
|
@@ -255,25 +461,24 @@ class LicenseValidator {
|
|
|
255
461
|
try {
|
|
256
462
|
this.initialize()
|
|
257
463
|
|
|
258
|
-
const payload = {
|
|
259
|
-
customerId: licenseData.customerId,
|
|
260
|
-
tier: licenseData.tier,
|
|
261
|
-
isFounder: licenseData.isFounder,
|
|
262
|
-
email: licenseData.email,
|
|
263
|
-
issued: Date.now(),
|
|
264
|
-
version: '1.0',
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const signature = this.signPayload(payload)
|
|
268
|
-
|
|
269
464
|
const licenseRecord = {
|
|
270
465
|
licenseKey: licenseData.licenseKey,
|
|
271
466
|
tier: licenseData.tier,
|
|
272
467
|
isFounder: licenseData.isFounder,
|
|
273
468
|
email: licenseData.email,
|
|
274
469
|
activated: new Date().toISOString(),
|
|
275
|
-
payload,
|
|
276
|
-
signature,
|
|
470
|
+
payload: licenseData.payload,
|
|
471
|
+
signature: licenseData.signature,
|
|
472
|
+
source: licenseData.source || 'legitimate_database',
|
|
473
|
+
verifiedAt: new Date().toISOString(),
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (!licenseRecord.payload || !licenseRecord.signature) {
|
|
477
|
+
return {
|
|
478
|
+
success: false,
|
|
479
|
+
error:
|
|
480
|
+
'Missing license signature payload. Please re-activate online.',
|
|
481
|
+
}
|
|
277
482
|
}
|
|
278
483
|
|
|
279
484
|
fs.writeFileSync(this.licenseFile, JSON.stringify(licenseRecord, null, 2))
|
|
@@ -291,43 +496,123 @@ class LicenseValidator {
|
|
|
291
496
|
}
|
|
292
497
|
|
|
293
498
|
/**
|
|
294
|
-
*
|
|
499
|
+
* Check if developer mode bypass is allowed.
|
|
500
|
+
* Security: In production mode, never allow developer bypass to prevent
|
|
501
|
+
* accidental security misconfiguration.
|
|
295
502
|
*/
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
.digest('hex')
|
|
503
|
+
isDevBypassAllowed() {
|
|
504
|
+
// Production mode always enforces signature verification
|
|
505
|
+
if (process.env.NODE_ENV === 'production') {
|
|
506
|
+
return false
|
|
507
|
+
}
|
|
508
|
+
return process.env.QAA_DEVELOPER === 'true'
|
|
303
509
|
}
|
|
304
510
|
|
|
305
|
-
/**
|
|
306
|
-
* Verify signature
|
|
307
|
-
*/
|
|
308
511
|
verifySignature(payload, signature) {
|
|
309
|
-
|
|
512
|
+
if (!this.licensePublicKey) {
|
|
513
|
+
// TD12 fix: Log warning when public key is missing in non-dev mode
|
|
514
|
+
if (!this.isDevBypassAllowed()) {
|
|
515
|
+
console.warn(
|
|
516
|
+
'⚠️ License public key not configured - signature verification skipped'
|
|
517
|
+
)
|
|
518
|
+
}
|
|
519
|
+
return this.isDevBypassAllowed()
|
|
520
|
+
}
|
|
310
521
|
try {
|
|
311
|
-
return
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
)
|
|
315
|
-
|
|
522
|
+
return verifyPayload(payload, signature, this.licensePublicKey)
|
|
523
|
+
} catch (error) {
|
|
524
|
+
// DR16 fix: Provide specific error types for better debugging
|
|
525
|
+
const msg = error.message?.toLowerCase() || ''
|
|
526
|
+
|
|
527
|
+
if (msg.includes('circular reference')) {
|
|
528
|
+
console.error(
|
|
529
|
+
'❌ Signature verification failed: License payload has circular reference'
|
|
530
|
+
)
|
|
531
|
+
console.error(
|
|
532
|
+
' This indicates data corruption. Please contact support.'
|
|
533
|
+
)
|
|
534
|
+
} else if (msg.includes('key') || msg.includes('pem')) {
|
|
535
|
+
console.error(
|
|
536
|
+
'❌ Signature verification failed: Invalid public key format'
|
|
537
|
+
)
|
|
538
|
+
console.error(
|
|
539
|
+
' Check QAA_LICENSE_PUBLIC_KEY or QAA_LICENSE_PUBLIC_KEY_PATH'
|
|
540
|
+
)
|
|
541
|
+
} else if (msg.includes('signature') && msg.includes('invalid')) {
|
|
542
|
+
console.error(
|
|
543
|
+
'❌ Signature verification failed: Corrupted signature data'
|
|
544
|
+
)
|
|
545
|
+
console.error(
|
|
546
|
+
' License may have been tampered with. Please re-activate.'
|
|
547
|
+
)
|
|
548
|
+
} else if (msg.includes('algorithm')) {
|
|
549
|
+
console.error('❌ Signature verification failed: Algorithm mismatch')
|
|
550
|
+
console.error(
|
|
551
|
+
' License was signed with different algorithm than expected.'
|
|
552
|
+
)
|
|
553
|
+
} else {
|
|
554
|
+
console.warn(`⚠️ Signature verification failed: ${error.message}`)
|
|
555
|
+
console.warn(
|
|
556
|
+
' If this persists, please contact support with this error message.'
|
|
557
|
+
)
|
|
558
|
+
}
|
|
316
559
|
return false
|
|
317
560
|
}
|
|
318
561
|
}
|
|
319
562
|
|
|
563
|
+
verifyRegistrySignature(database) {
|
|
564
|
+
// DR17 fix: Dev mode should still verify signatures if present, only bypass when missing
|
|
565
|
+
const isDevMode = this.isDevBypassAllowed()
|
|
566
|
+
const signature = database?._metadata?.registrySignature
|
|
567
|
+
|
|
568
|
+
// Missing signature handling
|
|
569
|
+
if (!signature) {
|
|
570
|
+
if (isDevMode) {
|
|
571
|
+
console.warn(
|
|
572
|
+
'⚠️ DEV MODE: License database signature missing (bypassed)'
|
|
573
|
+
)
|
|
574
|
+
return true
|
|
575
|
+
}
|
|
576
|
+
throw new Error('license database missing registry signature')
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Missing public key handling
|
|
580
|
+
if (!this.licensePublicKey) {
|
|
581
|
+
if (isDevMode) {
|
|
582
|
+
console.warn(
|
|
583
|
+
'⚠️ DEV MODE: License public key not configured (bypassed)'
|
|
584
|
+
)
|
|
585
|
+
return true
|
|
586
|
+
}
|
|
587
|
+
throw new Error('license public key not configured')
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Always verify signature if both signature and key are present
|
|
591
|
+
const { _metadata, ...licenses } = database
|
|
592
|
+
const isValid = verifyPayload(licenses, signature, this.licensePublicKey)
|
|
593
|
+
if (!isValid) {
|
|
594
|
+
throw new Error('license database signature verification failed')
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// TD10 fix: Use timing-safe comparison for hash verification
|
|
598
|
+
const expectedHash = database?._metadata?.hash
|
|
599
|
+
if (expectedHash) {
|
|
600
|
+
const computed = this.computeSha256(stableStringify(licenses))
|
|
601
|
+
if (!timingSafeEqual(computed, expectedHash)) {
|
|
602
|
+
throw new Error('license database hash mismatch')
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return true
|
|
606
|
+
}
|
|
607
|
+
|
|
320
608
|
/**
|
|
321
609
|
* Activate license (main user entry point)
|
|
322
610
|
*/
|
|
323
611
|
async activateLicense(licenseKey, userEmail) {
|
|
324
612
|
try {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
/^QAA-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/
|
|
329
|
-
)
|
|
330
|
-
) {
|
|
613
|
+
const normalizedKey = this.normalizeLicenseKey(licenseKey)
|
|
614
|
+
// Validate license key format (TD15 fix: use shared constant)
|
|
615
|
+
if (!LICENSE_KEY_PATTERN.test(normalizedKey)) {
|
|
331
616
|
return {
|
|
332
617
|
success: false,
|
|
333
618
|
error:
|
|
@@ -357,11 +642,14 @@ class LicenseValidator {
|
|
|
357
642
|
|
|
358
643
|
// Save locally
|
|
359
644
|
const saveResult = this.saveLicense({
|
|
360
|
-
licenseKey,
|
|
645
|
+
licenseKey: normalizedKey,
|
|
361
646
|
tier: validation.tier,
|
|
362
647
|
isFounder: validation.isFounder,
|
|
363
648
|
email: userEmail,
|
|
364
649
|
customerId: validation.customerId,
|
|
650
|
+
source: validation.source,
|
|
651
|
+
payload: validation.payload,
|
|
652
|
+
signature: validation.signature,
|
|
365
653
|
})
|
|
366
654
|
|
|
367
655
|
if (saveResult.success) {
|