create-qa-architect 5.13.4 → 5.13.5
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/lib/license-signing.js +25 -103
- package/lib/license-validator.js +32 -32
- package/package.json +2 -1
package/lib/license-signing.js
CHANGED
|
@@ -1,107 +1,28 @@
|
|
|
1
1
|
'use strict'
|
|
2
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
3
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
4
|
+
* License signing primitives — thin adapter over @buildproven/license-core.
|
|
5
|
+
*
|
|
6
|
+
* The package is the single source of truth for the signing/verification
|
|
7
|
+
* algorithm. This file exists for backward compatibility with existing
|
|
8
|
+
* `require('./license-signing')` callers in licensing.js, license-validator.js,
|
|
9
|
+
* admin-license.js, and tests. The bit-for-bit format is locked by the
|
|
10
|
+
* package's golden-vector tests against this exact file's prior behavior.
|
|
11
|
+
*
|
|
12
|
+
* QAA-specific bits (loadKeyFromEnv, LICENSE_KEY_PATTERN) stay here because
|
|
13
|
+
* they're not generic to all BuildProven products.
|
|
12
14
|
*/
|
|
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
15
|
|
|
23
|
-
|
|
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
|
-
}
|
|
16
|
+
const core = require('@buildproven/license-core')
|
|
32
17
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
}
|
|
18
|
+
// QAA license key format — kept here, not in the shared package, because
|
|
19
|
+
// each product has its own prefix.
|
|
20
|
+
const LICENSE_KEY_PATTERN =
|
|
21
|
+
/^QAA-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/
|
|
104
22
|
|
|
23
|
+
// QAA-specific helper: load a PEM key from either an env var (raw value)
|
|
24
|
+
// or a path env var (file contents). Not in the shared package because
|
|
25
|
+
// other consumers (Vercel functions) read keys differently.
|
|
105
26
|
function loadKeyFromEnv(envValue, envPathValue) {
|
|
106
27
|
if (envValue) return envValue
|
|
107
28
|
if (envPathValue) {
|
|
@@ -115,11 +36,12 @@ function loadKeyFromEnv(envValue, envPathValue) {
|
|
|
115
36
|
|
|
116
37
|
module.exports = {
|
|
117
38
|
LICENSE_KEY_PATTERN,
|
|
118
|
-
stableStringify,
|
|
119
|
-
normalizeEmail,
|
|
120
|
-
hashEmail,
|
|
121
|
-
buildLicensePayload,
|
|
122
|
-
signPayload,
|
|
123
|
-
verifyPayload,
|
|
124
39
|
loadKeyFromEnv,
|
|
40
|
+
// Re-exports — single source of truth in @buildproven/license-core
|
|
41
|
+
stableStringify: core.stableStringify,
|
|
42
|
+
normalizeEmail: core.normalizeEmail,
|
|
43
|
+
hashEmail: core.hashEmail,
|
|
44
|
+
buildLicensePayload: core.buildLicensePayload,
|
|
45
|
+
signPayload: core.signPayload,
|
|
46
|
+
verifyPayload: core.verifyPayload,
|
|
125
47
|
}
|
package/lib/license-validator.js
CHANGED
|
@@ -15,9 +15,12 @@ const {
|
|
|
15
15
|
buildLicensePayload,
|
|
16
16
|
hashEmail,
|
|
17
17
|
verifyPayload,
|
|
18
|
-
stableStringify,
|
|
19
18
|
loadKeyFromEnv,
|
|
20
19
|
} = require('./license-signing')
|
|
20
|
+
const {
|
|
21
|
+
validateRegistryEntry,
|
|
22
|
+
verifyRegistryMetadata,
|
|
23
|
+
} = require('@buildproven/license-core')
|
|
21
24
|
|
|
22
25
|
/**
|
|
23
26
|
* TD10 fix: Timing-safe string comparison to prevent timing attacks
|
|
@@ -85,7 +88,7 @@ class LicenseValidator {
|
|
|
85
88
|
// Allow enterprises to host their own registry
|
|
86
89
|
this.licenseDbUrl =
|
|
87
90
|
process.env.QAA_LICENSE_DB_URL ||
|
|
88
|
-
'https://buildproven.ai/api/licenses/qa-architect.json'
|
|
91
|
+
'https://licenses.buildproven.ai/api/licenses/qa-architect.json'
|
|
89
92
|
|
|
90
93
|
this.licensePublicKey = loadKeyFromEnv(
|
|
91
94
|
process.env.QAA_LICENSE_PUBLIC_KEY,
|
|
@@ -351,14 +354,6 @@ class LicenseValidator {
|
|
|
351
354
|
}
|
|
352
355
|
}
|
|
353
356
|
|
|
354
|
-
const payload = buildLicensePayload({
|
|
355
|
-
licenseKey: normalizedKey,
|
|
356
|
-
tier: licenseInfo.tier,
|
|
357
|
-
isFounder: licenseInfo.isFounder,
|
|
358
|
-
emailHash: licenseInfo.emailHash,
|
|
359
|
-
issued: licenseInfo.issued,
|
|
360
|
-
})
|
|
361
|
-
|
|
362
357
|
if (!licenseInfo.signature) {
|
|
363
358
|
return {
|
|
364
359
|
valid: false,
|
|
@@ -366,7 +361,18 @@ class LicenseValidator {
|
|
|
366
361
|
}
|
|
367
362
|
}
|
|
368
363
|
|
|
369
|
-
|
|
364
|
+
// Delegate signature verification to @buildproven/license-core so
|
|
365
|
+
// every BuildProven product validates against the same code path.
|
|
366
|
+
// Email hash check is duplicated above (with QAA-specific error message);
|
|
367
|
+
// pass userEmailHash here as defense-in-depth.
|
|
368
|
+
const verification = validateRegistryEntry({
|
|
369
|
+
licenseKey: normalizedKey,
|
|
370
|
+
entry: licenseInfo,
|
|
371
|
+
publicKeyPem: this.licensePublicKey,
|
|
372
|
+
userEmailHash: emailHash || undefined,
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
if (!verification.valid) {
|
|
370
376
|
return {
|
|
371
377
|
valid: false,
|
|
372
378
|
error:
|
|
@@ -374,6 +380,15 @@ class LicenseValidator {
|
|
|
374
380
|
}
|
|
375
381
|
}
|
|
376
382
|
|
|
383
|
+
// saveLicense() reads .payload off the result — rebuild for persistence.
|
|
384
|
+
const payload = buildLicensePayload({
|
|
385
|
+
licenseKey: normalizedKey,
|
|
386
|
+
tier: licenseInfo.tier,
|
|
387
|
+
isFounder: licenseInfo.isFounder,
|
|
388
|
+
emailHash: licenseInfo.emailHash,
|
|
389
|
+
issued: licenseInfo.issued,
|
|
390
|
+
})
|
|
391
|
+
|
|
377
392
|
// License is valid
|
|
378
393
|
console.log(
|
|
379
394
|
`✅ License validated: ${licenseInfo.tier} ${licenseInfo.isFounder ? '(Founder)' : ''}`
|
|
@@ -563,12 +578,11 @@ class LicenseValidator {
|
|
|
563
578
|
}
|
|
564
579
|
|
|
565
580
|
verifyRegistrySignature(database) {
|
|
566
|
-
// DR17 fix: Dev mode
|
|
581
|
+
// DR17 fix: Dev mode bypasses ONLY when signature or key is missing.
|
|
582
|
+
// When both are present, always verify (no bypass).
|
|
567
583
|
const isDevMode = this.isDevBypassAllowed()
|
|
568
|
-
const signature = database?._metadata?.registrySignature
|
|
569
584
|
|
|
570
|
-
|
|
571
|
-
if (!signature) {
|
|
585
|
+
if (!database?._metadata?.registrySignature) {
|
|
572
586
|
if (isDevMode) {
|
|
573
587
|
console.warn(
|
|
574
588
|
'⚠️ DEV MODE: License database signature missing (bypassed)'
|
|
@@ -578,7 +592,6 @@ class LicenseValidator {
|
|
|
578
592
|
throw new Error('license database missing registry signature')
|
|
579
593
|
}
|
|
580
594
|
|
|
581
|
-
// Missing public key handling
|
|
582
595
|
if (!this.licensePublicKey) {
|
|
583
596
|
if (isDevMode) {
|
|
584
597
|
console.warn(
|
|
@@ -589,22 +602,9 @@ class LicenseValidator {
|
|
|
589
602
|
throw new Error('license public key not configured')
|
|
590
603
|
}
|
|
591
604
|
|
|
592
|
-
//
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
const isValid = verifyPayload(licenses, signature, this.licensePublicKey)
|
|
596
|
-
if (!isValid) {
|
|
597
|
-
throw new Error('license database signature verification failed')
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
// TD10 fix: Use timing-safe comparison for hash verification
|
|
601
|
-
const expectedHash = database?._metadata?.hash
|
|
602
|
-
if (expectedHash) {
|
|
603
|
-
const computed = this.computeSha256(stableStringify(licenses))
|
|
604
|
-
if (!timingSafeEqual(computed, expectedHash)) {
|
|
605
|
-
throw new Error('license database hash mismatch')
|
|
606
|
-
}
|
|
607
|
-
}
|
|
605
|
+
// Delegate to @buildproven/license-core. Throws on signature or hash
|
|
606
|
+
// mismatch. Single source of truth shared with fulfillment + claude-kit-pro.
|
|
607
|
+
verifyRegistryMetadata(database, this.licensePublicKey)
|
|
608
608
|
return true
|
|
609
609
|
}
|
|
610
610
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-qa-architect",
|
|
3
|
-
"version": "5.13.
|
|
3
|
+
"version": "5.13.5",
|
|
4
4
|
"description": "QA Architect - Bootstrap quality automation for JavaScript/TypeScript and Python projects with GitHub Actions, pre-commit hooks, linting, formatting, and smart test strategy",
|
|
5
5
|
"main": "setup.js",
|
|
6
6
|
"bin": {
|
|
@@ -219,6 +219,7 @@
|
|
|
219
219
|
]
|
|
220
220
|
},
|
|
221
221
|
"dependencies": {
|
|
222
|
+
"@buildproven/license-core": "^1.0.2",
|
|
222
223
|
"@npmcli/package-json": "^7.0.4",
|
|
223
224
|
"ajv": "^8.17.1",
|
|
224
225
|
"ajv-formats": "^3.0.1",
|