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.
@@ -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
- * Deterministic JSON stringify with sorted keys for signature verification.
11
- * TD13 fix: Added circular reference protection using WeakSet.
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
- 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
- }
16
+ const core = require('@buildproven/license-core')
32
17
 
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
- }
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
  }
@@ -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
- if (!this.verifySignature(payload, licenseInfo.signature)) {
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 should still verify signatures if present, only bypass when missing
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
- // Missing signature handling
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
- // Always verify signature if both signature and key are present (destructure to exclude metadata)
593
- const { _metadata: _unused3, ...licenses } = database
594
- void _unused3
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.4",
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",