@buildproven/license-core 1.0.0 → 1.0.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/README.md CHANGED
@@ -1,71 +1,166 @@
1
- # @buildproven/license-core
1
+ # `@buildproven/license-core`
2
2
 
3
- Shared license signing & verification primitives for BuildProven products.
3
+ [![npm version](https://img.shields.io/npm/v/@buildproven/license-core.svg)](https://www.npmjs.com/package/@buildproven/license-core)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
5
 
5
- ## What this is
6
+ Tiny, frozen-contract license signing & verification primitives. Deterministic JSON, RSA-SHA256, signed registry. Use it to issue and verify software licenses without running a license server on the hot path — clients fetch a signed JSON registry, verify it locally with a bundled public key, and decide.
6
7
 
7
- A tiny, frozen-contract package that does one thing: deterministic stringify + RSA-SHA256 sign/verify so that every BuildProven product (QA Architect, claude-kit-pro, future products) can validate licenses against the same byte-for-byte format.
8
+ ```
9
+ npm install @buildproven/license-core
10
+ ```
8
11
 
9
- Used by:
12
+ ## Why this exists
10
13
 
11
- - `buildproven-fulfillment` (Vercel) signs entries, builds signed registries
12
- - `qa-architect` (npm CLI) — verifies fetched registry against bundled public key
13
- - `claude-kit-pro` (Claude Code MCP plugin) — same
14
+ If you're shipping a desktop app, CLI tool, or developer plugin and want to sell licenses, you need:
14
15
 
15
- ## Why it exists
16
+ 1. A way to **sign** a license entry on your server (when a customer pays)
17
+ 2. A way for the **client** to verify that signature locally — without making a network call on every launch
16
18
 
17
- Before this package, the same crypto code lived in 3 places. Any drift broke signature verification across the product/server boundary. Now there's one source of truth.
19
+ That's what this is. ~14 functions, no runtime dependencies. Sign on the server, distribute a signed registry as a static JSON file, verify on the client. Works offline, no license server to keep alive, no per-call latency.
18
20
 
19
- ## API
21
+ ## Quick example
22
+
23
+ ### Server side (when a customer purchases)
20
24
 
21
25
  ```ts
22
26
  import {
23
- // Crypto primitives
24
- stableStringify,
27
+ buildLicensePayload,
28
+ buildSignedRegistry,
29
+ hashEmail,
25
30
  signPayload,
26
- verifyPayload,
27
- computeHash,
28
- timingSafeStringEqual,
31
+ type Registry,
32
+ } from '@buildproven/license-core';
29
33
 
30
- // Payload construction
31
- normalizeEmail,
32
- hashEmail,
33
- buildLicensePayload,
34
+ const PRIVATE_KEY = process.env.LICENSE_PRIVATE_KEY!; // PEM, RSA 2048+
35
+
36
+ const issued = new Date().toISOString();
37
+ const payload = buildLicensePayload({
38
+ licenseKey: 'MYAPP-A1B2-C3D4-E5F6-7890',
39
+ tier: 'PRO',
40
+ isFounder: false,
41
+ issued,
42
+ emailHash: hashEmail('customer@example.com') ?? undefined,
43
+ });
44
+
45
+ const entry = {
46
+ tier: 'PRO' as const,
47
+ isFounder: false,
48
+ issued,
49
+ emailHash: hashEmail('customer@example.com'),
50
+ signature: signPayload(payload, PRIVATE_KEY),
51
+ customerId: 'cus_abc123',
52
+ keyId: 'default',
53
+ };
54
+
55
+ const registry: Registry = { 'MYAPP-A1B2-C3D4-E5F6-7890': entry };
56
+ const signedRegistry = buildSignedRegistry(registry, PRIVATE_KEY);
57
+
58
+ // Serve `signedRegistry` as JSON at e.g. https://yoursite.com/api/licenses.json
59
+ ```
34
60
 
35
- // Registry
36
- buildSignedRegistry,
61
+ ### Client side (when the user starts your app)
37
62
 
38
- // Validation helpers (pure — no I/O)
63
+ ```ts
64
+ import {
39
65
  validateRegistryEntry,
40
66
  verifyRegistryMetadata,
41
-
42
- // Key format
43
- licenseKeyPattern,
67
+ hashEmail,
44
68
  isValidLicenseKey,
45
- normalizeLicenseKey,
46
69
  } from '@buildproven/license-core';
70
+
71
+ const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
72
+ ...
73
+ -----END PUBLIC KEY-----`; // bundled with your app
74
+
75
+ const userKey = process.env.MYAPP_LICENSE_KEY ?? '';
76
+ const userEmail = '...'; // from your activation flow
77
+
78
+ if (!isValidLicenseKey(userKey, 'MYAPP')) {
79
+ throw new Error('Bad key format');
80
+ }
81
+
82
+ const res = await fetch('https://yoursite.com/api/licenses.json');
83
+ const signedRegistry = await res.json();
84
+
85
+ const entries = verifyRegistryMetadata(signedRegistry, PUBLIC_KEY); // throws on tampering
86
+ const entry = entries[userKey.toUpperCase()];
87
+ if (!entry) throw new Error('License not found');
88
+
89
+ const result = validateRegistryEntry({
90
+ licenseKey: userKey.toUpperCase(),
91
+ entry,
92
+ publicKeyPem: PUBLIC_KEY,
93
+ userEmailHash: hashEmail(userEmail) ?? undefined,
94
+ });
95
+
96
+ if (!result.valid) throw new Error(result.error);
97
+
98
+ console.log(`Licensed: ${result.tier}, Founder: ${result.isFounder}`);
47
99
  ```
48
100
 
101
+ ## API reference
102
+
103
+ ### Crypto primitives
104
+
105
+ - `stableStringify(value)` — deterministic JSON stringify (sorted keys, circular-ref detection)
106
+ - `signPayload(payload, privateKeyPem)` — RSA-SHA256 sign, returns base64
107
+ - `verifyPayload(payload, signature, publicKeyPem)` — RSA-SHA256 verify, returns boolean
108
+ - `computeHash(data)` — SHA-256 hex
109
+ - `timingSafeStringEqual(a, b)` — constant-time string comparison
110
+
111
+ ### Payload construction
112
+
113
+ - `normalizeEmail(email)` — trim + lowercase + format-validate, or `null`
114
+ - `hashEmail(email)` — SHA-256 hex of normalized email, or `null`
115
+ - `buildLicensePayload({ licenseKey, tier, isFounder, issued, emailHash? })` — the **frozen** payload shape
116
+
117
+ ### Registry
118
+
119
+ - `buildSignedRegistry(entries, privateKeyPem, keyId?)` — wraps entries with `_metadata` containing the registry signature and hash
120
+
121
+ ### Validation helpers (pure — no I/O)
122
+
123
+ - `validateRegistryEntry({ licenseKey, entry, publicKeyPem, userEmailHash? })` — verifies one entry's signature and optional email match
124
+ - `verifyRegistryMetadata(signedRegistry, publicKeyPem)` — verifies the registry-level signature and hash, returns extracted entries (throws on failure)
125
+
126
+ ### Key format
127
+
128
+ - `licenseKeyPattern(prefix)` — RegExp for `PREFIX-XXXX-XXXX-XXXX-XXXX`
129
+ - `isValidLicenseKey(key, prefix)` — boolean
130
+ - `normalizeLicenseKey(key)` — trim + uppercase
131
+
132
+ ### Types
133
+
134
+ `Tier`, `LicensePayload`, `RegistryEntry`, `Registry`, `RegistryMetadata`, `SignedRegistry`, `ValidatedEntry`, `ValidationFailure`, `ValidationResult`
135
+
49
136
  ## Frozen contract
50
137
 
51
- The shape of `LicensePayload` and `RegistryEntry` is **frozen** for the v1.x line. Adding fields is a breaking change because shipped customer CLIs reconstruct payloads from registry entries to verify signatures — any field set drift causes silent verification failure.
138
+ The shape of `LicensePayload` and `RegistryEntry` is **frozen for the entire `1.x` line**. Shipped clients in the field rebuild these payloads from registry entries to verify signatures — adding a field would silently break verification for every existing customer.
52
139
 
53
- If you need a new field, bump the major and ship as a new package name (`@buildproven/license-core-v2`). The 1.x line continues to be the contract for QA Architect's deployed customers.
140
+ To evolve the schema: bump major and ship as a new package name. The 1.x line is the contract for already-deployed clients.
54
141
 
55
- ## Install
142
+ ## How keys work
143
+
144
+ You generate one RSA-2048 keypair per product:
56
145
 
57
146
  ```bash
58
- npm install @buildproven/license-core
147
+ openssl genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:2048
148
+ openssl rsa -in private.pem -pubout -out public.pem
59
149
  ```
60
150
 
61
- ## Develop
151
+ - **Private key** lives in your fulfillment server's env vars. It signs license entries.
152
+ - **Public key** is bundled with your client. It verifies signatures.
62
153
 
63
- ```bash
64
- npm install
65
- npm test # vitest, includes golden-vector test against QAA's deployed code
66
- npm run build # tsup → dist/
67
- ```
154
+ The package never handles key generation, storage, or rotation — that's your call. Use whatever secret manager you already have.
68
155
 
69
156
  ## License
70
157
 
71
- MIT
158
+ [MIT](./LICENSE) © Vibe Build Lab LLC
159
+
160
+ ## Contributing
161
+
162
+ See [CONTRIBUTING.md](./CONTRIBUTING.md). The `1.x` line has a frozen contract; most contributions should be bug fixes, doc improvements, or test coverage.
163
+
164
+ ## Security
165
+
166
+ See [SECURITY.md](./SECURITY.md) for the threat model and how to report vulnerabilities.
package/dist/index.cjs CHANGED
@@ -126,7 +126,7 @@ function buildSignedRegistry(entries, privateKeyPem, keyId = "default") {
126
126
  version: "1.0",
127
127
  created: now,
128
128
  lastUpdate: now,
129
- description: "BuildProven license registry \u2014 populated by fulfillment webhook",
129
+ description: "License registry \u2014 populated by fulfillment webhook",
130
130
  algorithm: "rsa-sha256",
131
131
  keyId,
132
132
  registrySignature,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/signing.ts","../src/payload.ts","../src/registry.ts","../src/validator.ts","../src/key-format.ts"],"sourcesContent":["// Crypto primitives\nexport {\n stableStringify,\n signPayload,\n verifyPayload,\n computeHash,\n timingSafeStringEqual,\n} from './signing.js';\n\n// Payload construction\nexport { normalizeEmail, hashEmail, buildLicensePayload } from './payload.js';\n\n// Registry construction\nexport { buildSignedRegistry } from './registry.js';\n\n// Validation helpers (pure — no I/O)\nexport { validateRegistryEntry, verifyRegistryMetadata } from './validator.js';\nexport type { ValidatedEntry, ValidationFailure, ValidationResult } from './validator.js';\n\n// License key format\nexport { licenseKeyPattern, isValidLicenseKey, normalizeLicenseKey } from './key-format.js';\n\n// Types\nexport type {\n Tier,\n LicensePayload,\n RegistryEntry,\n Registry,\n RegistryMetadata,\n SignedRegistry,\n} from './types.js';\n","/**\n * Deterministic stringify + RSA-SHA256 sign/verify primitives.\n *\n * stableStringify must produce byte-identical output to QA Architect's\n * shipped lib/license-signing.js — the deployed CLI in customers' hands\n * uses that exact algorithm. Any divergence here breaks every QAA license\n * issued to date.\n */\n\nimport { sign as cryptoSign, verify as cryptoVerify, createHash } from 'crypto';\n\nexport function stableStringify(value: unknown, seen: WeakSet<object> = new WeakSet()): string {\n if (value === null || typeof value !== 'object') {\n return JSON.stringify(value);\n }\n if (seen.has(value as object)) {\n throw new Error('Circular reference detected in payload - cannot serialize');\n }\n seen.add(value as object);\n\n if (Array.isArray(value)) {\n return `[${value.map((item) => stableStringify(item, seen)).join(',')}]`;\n }\n const keys = Object.keys(value as Record<string, unknown>).sort();\n const entries = keys.map(\n (key) =>\n `${JSON.stringify(key)}:${stableStringify((value as Record<string, unknown>)[key], seen)}`,\n );\n return `{${entries.join(',')}}`;\n}\n\nexport function signPayload(payload: unknown, privateKeyPem: string): string {\n const data = Buffer.from(stableStringify(payload));\n return cryptoSign(null, data, privateKeyPem).toString('base64');\n}\n\nexport function verifyPayload(payload: unknown, signature: string, publicKeyPem: string): boolean {\n try {\n const data = Buffer.from(stableStringify(payload));\n return cryptoVerify(null, data, publicKeyPem, Buffer.from(signature, 'base64'));\n } catch {\n return false;\n }\n}\n\nexport function computeHash(data: string): string {\n return createHash('sha256').update(data).digest('hex');\n}\n\n/**\n * Constant-time string comparison. Same length precondition is checked\n * outside the comparison loop to avoid leaking length info.\n */\nexport function timingSafeStringEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n let diff = 0;\n for (let i = 0; i < a.length; i++) {\n diff |= a.charCodeAt(i) ^ b.charCodeAt(i);\n }\n return diff === 0;\n}\n","/**\n * Email normalization, hashing, and license payload construction.\n *\n * buildLicensePayload is the contract the fulfillment service signs against\n * and that every client must rebuild bit-for-bit before verification. Adding\n * fields here = breaking change.\n */\n\nimport { createHash } from 'crypto';\nimport type { LicensePayload, Tier } from './types.js';\n\nexport function normalizeEmail(email: string): string | null {\n if (!email || typeof email !== 'string') return null;\n const normalized = email.trim().toLowerCase();\n if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(normalized)) return null;\n return normalized.length > 0 ? normalized : null;\n}\n\nexport function hashEmail(email: string): string | null {\n const normalized = normalizeEmail(email);\n if (!normalized) return null;\n return createHash('sha256').update(normalized).digest('hex');\n}\n\nexport function buildLicensePayload(opts: {\n licenseKey: string;\n tier: Tier;\n isFounder: boolean;\n issued: string;\n emailHash?: string | null;\n}): LicensePayload {\n if (!opts.licenseKey || typeof opts.licenseKey !== 'string') {\n throw new Error('licenseKey is required and must be a string');\n }\n if (!opts.tier || typeof opts.tier !== 'string') {\n throw new Error('tier is required and must be a string');\n }\n if (!opts.issued || typeof opts.issued !== 'string') {\n throw new Error('issued is required and must be a string');\n }\n\n const payload: LicensePayload = {\n licenseKey: opts.licenseKey,\n tier: opts.tier,\n isFounder: Boolean(opts.isFounder),\n issued: opts.issued,\n };\n if (opts.emailHash) {\n payload.emailHash = opts.emailHash;\n }\n return payload;\n}\n","/**\n * Build a complete signed registry from a flat entries map.\n *\n * The registry signature covers ONLY the entries — _metadata is excluded.\n * QAA's deployed validator destructures `_metadata` out before verifying,\n * so any change to what's signed will break compatibility.\n */\n\nimport { computeHash, signPayload, stableStringify } from './signing.js';\nimport type { Registry, SignedRegistry } from './types.js';\n\nexport function buildSignedRegistry(\n entries: Registry,\n privateKeyPem: string,\n keyId = 'default',\n): SignedRegistry {\n const now = new Date().toISOString();\n const entriesStr = stableStringify(entries);\n const registrySignature = signPayload(entries, privateKeyPem);\n const hash = computeHash(entriesStr);\n\n return {\n _metadata: {\n version: '1.0',\n created: now,\n lastUpdate: now,\n description: 'BuildProven license registry — populated by fulfillment webhook',\n algorithm: 'rsa-sha256',\n keyId,\n registrySignature,\n hash,\n totalLicenses: Object.keys(entries).length,\n },\n ...entries,\n };\n}\n","/**\n * Pure validation helpers — no I/O, no caching, no env.\n *\n * Both QA Architect's CLI and claude-kit-pro's MCP server use these\n * to verify a registry response. Anything that touches disk, network,\n * or process.env stays in the consuming product. This is the seam\n * that prevents the two validators from drifting apart.\n */\n\nimport { buildLicensePayload } from './payload.js';\nimport { computeHash, stableStringify, timingSafeStringEqual, verifyPayload } from './signing.js';\nimport type { RegistryEntry, SignedRegistry } from './types.js';\n\nexport interface ValidatedEntry {\n valid: true;\n tier: RegistryEntry['tier'];\n isFounder: boolean;\n customerId: string;\n keyId: string;\n}\n\nexport interface ValidationFailure {\n valid: false;\n error: string;\n}\n\nexport type ValidationResult = ValidatedEntry | ValidationFailure;\n\n/**\n * Verify a single registry entry against its embedded signature.\n * Optionally check the user's email hash against the entry's emailHash.\n *\n * Mirrors QAA's validateLicense() field-set exactly:\n * payload = { licenseKey, tier, isFounder, issued, emailHash? }\n */\nexport function validateRegistryEntry(opts: {\n licenseKey: string;\n entry: RegistryEntry;\n publicKeyPem: string;\n /** If supplied, must match entry.emailHash (timing-safe). */\n userEmailHash?: string;\n}): ValidationResult {\n const { licenseKey, entry, publicKeyPem, userEmailHash } = opts;\n\n if (entry.emailHash && userEmailHash && !timingSafeStringEqual(userEmailHash, entry.emailHash)) {\n return { valid: false, error: 'Email address does not match license registration' };\n }\n\n const payload = buildLicensePayload({\n licenseKey,\n tier: entry.tier,\n isFounder: entry.isFounder,\n issued: entry.issued,\n emailHash: entry.emailHash,\n });\n\n if (!verifyPayload(payload, entry.signature, publicKeyPem)) {\n return { valid: false, error: 'License entry signature verification failed' };\n }\n\n return {\n valid: true,\n tier: entry.tier,\n isFounder: entry.isFounder,\n customerId: entry.customerId,\n keyId: entry.keyId,\n };\n}\n\n/**\n * Verify a complete signed registry: registry-level signature + hash check.\n * Returns the entries map (with _metadata stripped) on success, throws on failure.\n *\n * Throws (rather than returning a result) because a registry signature failure\n * should halt validation entirely — clients should not fall back to entries\n * from an unverified registry.\n */\nexport function verifyRegistryMetadata(\n signedRegistry: SignedRegistry,\n publicKeyPem: string,\n): Record<string, RegistryEntry> {\n const { _metadata, ...entries } = signedRegistry;\n\n if (!_metadata?.registrySignature) {\n throw new Error('Registry missing _metadata.registrySignature');\n }\n\n if (!verifyPayload(entries, _metadata.registrySignature, publicKeyPem)) {\n throw new Error('Registry signature verification failed');\n }\n\n if (_metadata.hash) {\n const computed = computeHash(stableStringify(entries));\n if (!timingSafeStringEqual(computed, _metadata.hash)) {\n throw new Error('Registry hash mismatch');\n }\n }\n\n return entries as Record<string, RegistryEntry>;\n}\n","/**\n * Per-product license key format.\n *\n * QAA-XXXX-XXXX-XXXX-XXXX, CKIT-XXXX-XXXX-XXXX-XXXX, etc.\n * One factory so every product validates the same way.\n */\n\nexport function licenseKeyPattern(prefix: string): RegExp {\n if (!/^[A-Z0-9]+$/.test(prefix)) {\n throw new Error(`Prefix must be uppercase alphanumeric: ${prefix}`);\n }\n return new RegExp(`^${prefix}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$`);\n}\n\nexport function isValidLicenseKey(key: string, prefix: string): boolean {\n return licenseKeyPattern(prefix).test(key.trim().toUpperCase());\n}\n\nexport function normalizeLicenseKey(key: string): string {\n return key.trim().toUpperCase();\n}\n"],"mappings":"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","names":["cryptoSign","cryptoVerify","import_crypto"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/signing.ts","../src/payload.ts","../src/registry.ts","../src/validator.ts","../src/key-format.ts"],"sourcesContent":["// Crypto primitives\nexport {\n stableStringify,\n signPayload,\n verifyPayload,\n computeHash,\n timingSafeStringEqual,\n} from './signing.js';\n\n// Payload construction\nexport { normalizeEmail, hashEmail, buildLicensePayload } from './payload.js';\n\n// Registry construction\nexport { buildSignedRegistry } from './registry.js';\n\n// Validation helpers (pure — no I/O)\nexport { validateRegistryEntry, verifyRegistryMetadata } from './validator.js';\nexport type { ValidatedEntry, ValidationFailure, ValidationResult } from './validator.js';\n\n// License key format\nexport { licenseKeyPattern, isValidLicenseKey, normalizeLicenseKey } from './key-format.js';\n\n// Types\nexport type {\n Tier,\n LicensePayload,\n RegistryEntry,\n Registry,\n RegistryMetadata,\n SignedRegistry,\n} from './types.js';\n","/**\n * Deterministic stringify + RSA-SHA256 sign/verify primitives.\n *\n * stableStringify must produce byte-identical output to QA Architect's\n * shipped lib/license-signing.js — the deployed CLI in customers' hands\n * uses that exact algorithm. Any divergence here breaks every QAA license\n * issued to date.\n */\n\nimport { sign as cryptoSign, verify as cryptoVerify, createHash } from 'crypto';\n\nexport function stableStringify(value: unknown, seen: WeakSet<object> = new WeakSet()): string {\n if (value === null || typeof value !== 'object') {\n return JSON.stringify(value);\n }\n if (seen.has(value as object)) {\n throw new Error('Circular reference detected in payload - cannot serialize');\n }\n seen.add(value as object);\n\n if (Array.isArray(value)) {\n return `[${value.map((item) => stableStringify(item, seen)).join(',')}]`;\n }\n const keys = Object.keys(value as Record<string, unknown>).sort();\n const entries = keys.map(\n (key) =>\n `${JSON.stringify(key)}:${stableStringify((value as Record<string, unknown>)[key], seen)}`,\n );\n return `{${entries.join(',')}}`;\n}\n\nexport function signPayload(payload: unknown, privateKeyPem: string): string {\n const data = Buffer.from(stableStringify(payload));\n return cryptoSign(null, data, privateKeyPem).toString('base64');\n}\n\nexport function verifyPayload(payload: unknown, signature: string, publicKeyPem: string): boolean {\n try {\n const data = Buffer.from(stableStringify(payload));\n return cryptoVerify(null, data, publicKeyPem, Buffer.from(signature, 'base64'));\n } catch {\n return false;\n }\n}\n\nexport function computeHash(data: string): string {\n return createHash('sha256').update(data).digest('hex');\n}\n\n/**\n * Constant-time string comparison. Same length precondition is checked\n * outside the comparison loop to avoid leaking length info.\n */\nexport function timingSafeStringEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n let diff = 0;\n for (let i = 0; i < a.length; i++) {\n diff |= a.charCodeAt(i) ^ b.charCodeAt(i);\n }\n return diff === 0;\n}\n","/**\n * Email normalization, hashing, and license payload construction.\n *\n * buildLicensePayload is the contract the fulfillment service signs against\n * and that every client must rebuild bit-for-bit before verification. Adding\n * fields here = breaking change.\n */\n\nimport { createHash } from 'crypto';\nimport type { LicensePayload, Tier } from './types.js';\n\nexport function normalizeEmail(email: string): string | null {\n if (!email || typeof email !== 'string') return null;\n const normalized = email.trim().toLowerCase();\n if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(normalized)) return null;\n return normalized.length > 0 ? normalized : null;\n}\n\nexport function hashEmail(email: string): string | null {\n const normalized = normalizeEmail(email);\n if (!normalized) return null;\n return createHash('sha256').update(normalized).digest('hex');\n}\n\nexport function buildLicensePayload(opts: {\n licenseKey: string;\n tier: Tier;\n isFounder: boolean;\n issued: string;\n emailHash?: string | null;\n}): LicensePayload {\n if (!opts.licenseKey || typeof opts.licenseKey !== 'string') {\n throw new Error('licenseKey is required and must be a string');\n }\n if (!opts.tier || typeof opts.tier !== 'string') {\n throw new Error('tier is required and must be a string');\n }\n if (!opts.issued || typeof opts.issued !== 'string') {\n throw new Error('issued is required and must be a string');\n }\n\n const payload: LicensePayload = {\n licenseKey: opts.licenseKey,\n tier: opts.tier,\n isFounder: Boolean(opts.isFounder),\n issued: opts.issued,\n };\n if (opts.emailHash) {\n payload.emailHash = opts.emailHash;\n }\n return payload;\n}\n","/**\n * Build a complete signed registry from a flat entries map.\n *\n * The registry signature covers ONLY the entries — _metadata is excluded.\n * QAA's deployed validator destructures `_metadata` out before verifying,\n * so any change to what's signed will break compatibility.\n */\n\nimport { computeHash, signPayload, stableStringify } from './signing.js';\nimport type { Registry, SignedRegistry } from './types.js';\n\nexport function buildSignedRegistry(\n entries: Registry,\n privateKeyPem: string,\n keyId = 'default',\n): SignedRegistry {\n const now = new Date().toISOString();\n const entriesStr = stableStringify(entries);\n const registrySignature = signPayload(entries, privateKeyPem);\n const hash = computeHash(entriesStr);\n\n return {\n _metadata: {\n version: '1.0',\n created: now,\n lastUpdate: now,\n description: 'License registry — populated by fulfillment webhook',\n algorithm: 'rsa-sha256',\n keyId,\n registrySignature,\n hash,\n totalLicenses: Object.keys(entries).length,\n },\n ...entries,\n };\n}\n","/**\n * Pure validation helpers — no I/O, no caching, no env.\n *\n * Both QA Architect's CLI and claude-kit-pro's MCP server use these\n * to verify a registry response. Anything that touches disk, network,\n * or process.env stays in the consuming product. This is the seam\n * that prevents the two validators from drifting apart.\n */\n\nimport { buildLicensePayload } from './payload.js';\nimport { computeHash, stableStringify, timingSafeStringEqual, verifyPayload } from './signing.js';\nimport type { RegistryEntry, SignedRegistry } from './types.js';\n\nexport interface ValidatedEntry {\n valid: true;\n tier: RegistryEntry['tier'];\n isFounder: boolean;\n customerId: string;\n keyId: string;\n}\n\nexport interface ValidationFailure {\n valid: false;\n error: string;\n}\n\nexport type ValidationResult = ValidatedEntry | ValidationFailure;\n\n/**\n * Verify a single registry entry against its embedded signature.\n * Optionally check the user's email hash against the entry's emailHash.\n *\n * Mirrors QAA's validateLicense() field-set exactly:\n * payload = { licenseKey, tier, isFounder, issued, emailHash? }\n */\nexport function validateRegistryEntry(opts: {\n licenseKey: string;\n entry: RegistryEntry;\n publicKeyPem: string;\n /** If supplied, must match entry.emailHash (timing-safe). */\n userEmailHash?: string;\n}): ValidationResult {\n const { licenseKey, entry, publicKeyPem, userEmailHash } = opts;\n\n if (entry.emailHash && userEmailHash && !timingSafeStringEqual(userEmailHash, entry.emailHash)) {\n return { valid: false, error: 'Email address does not match license registration' };\n }\n\n const payload = buildLicensePayload({\n licenseKey,\n tier: entry.tier,\n isFounder: entry.isFounder,\n issued: entry.issued,\n emailHash: entry.emailHash,\n });\n\n if (!verifyPayload(payload, entry.signature, publicKeyPem)) {\n return { valid: false, error: 'License entry signature verification failed' };\n }\n\n return {\n valid: true,\n tier: entry.tier,\n isFounder: entry.isFounder,\n customerId: entry.customerId,\n keyId: entry.keyId,\n };\n}\n\n/**\n * Verify a complete signed registry: registry-level signature + hash check.\n * Returns the entries map (with _metadata stripped) on success, throws on failure.\n *\n * Throws (rather than returning a result) because a registry signature failure\n * should halt validation entirely — clients should not fall back to entries\n * from an unverified registry.\n */\nexport function verifyRegistryMetadata(\n signedRegistry: SignedRegistry,\n publicKeyPem: string,\n): Record<string, RegistryEntry> {\n const { _metadata, ...entries } = signedRegistry;\n\n if (!_metadata?.registrySignature) {\n throw new Error('Registry missing _metadata.registrySignature');\n }\n\n if (!verifyPayload(entries, _metadata.registrySignature, publicKeyPem)) {\n throw new Error('Registry signature verification failed');\n }\n\n if (_metadata.hash) {\n const computed = computeHash(stableStringify(entries));\n if (!timingSafeStringEqual(computed, _metadata.hash)) {\n throw new Error('Registry hash mismatch');\n }\n }\n\n return entries as Record<string, RegistryEntry>;\n}\n","/**\n * Per-product license key format.\n *\n * QAA-XXXX-XXXX-XXXX-XXXX, CKIT-XXXX-XXXX-XXXX-XXXX, etc.\n * One factory so every product validates the same way.\n */\n\nexport function licenseKeyPattern(prefix: string): RegExp {\n if (!/^[A-Z0-9]+$/.test(prefix)) {\n throw new Error(`Prefix must be uppercase alphanumeric: ${prefix}`);\n }\n return new RegExp(`^${prefix}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$`);\n}\n\nexport function isValidLicenseKey(key: string, prefix: string): boolean {\n return licenseKeyPattern(prefix).test(key.trim().toUpperCase());\n}\n\nexport function normalizeLicenseKey(key: string): string {\n return key.trim().toUpperCase();\n}\n"],"mappings":"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","names":["cryptoSign","cryptoVerify","import_crypto"]}
package/dist/index.d.cts CHANGED
@@ -17,7 +17,7 @@ declare function computeHash(data: string): string;
17
17
  declare function timingSafeStringEqual(a: string, b: string): boolean;
18
18
 
19
19
  /**
20
- * Core types for BuildProven license payloads.
20
+ * Core types for license payloads.
21
21
  *
22
22
  * IMPORTANT: changing the shape of LicensePayload or RegistryEntry
23
23
  * breaks signature verification across every product in the field.
package/dist/index.d.ts CHANGED
@@ -17,7 +17,7 @@ declare function computeHash(data: string): string;
17
17
  declare function timingSafeStringEqual(a: string, b: string): boolean;
18
18
 
19
19
  /**
20
- * Core types for BuildProven license payloads.
20
+ * Core types for license payloads.
21
21
  *
22
22
  * IMPORTANT: changing the shape of LicensePayload or RegistryEntry
23
23
  * breaks signature verification across every product in the field.
package/dist/index.js CHANGED
@@ -87,7 +87,7 @@ function buildSignedRegistry(entries, privateKeyPem, keyId = "default") {
87
87
  version: "1.0",
88
88
  created: now,
89
89
  lastUpdate: now,
90
- description: "BuildProven license registry \u2014 populated by fulfillment webhook",
90
+ description: "License registry \u2014 populated by fulfillment webhook",
91
91
  algorithm: "rsa-sha256",
92
92
  keyId,
93
93
  registrySignature,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/signing.ts","../src/payload.ts","../src/registry.ts","../src/validator.ts","../src/key-format.ts"],"sourcesContent":["/**\n * Deterministic stringify + RSA-SHA256 sign/verify primitives.\n *\n * stableStringify must produce byte-identical output to QA Architect's\n * shipped lib/license-signing.js — the deployed CLI in customers' hands\n * uses that exact algorithm. Any divergence here breaks every QAA license\n * issued to date.\n */\n\nimport { sign as cryptoSign, verify as cryptoVerify, createHash } from 'crypto';\n\nexport function stableStringify(value: unknown, seen: WeakSet<object> = new WeakSet()): string {\n if (value === null || typeof value !== 'object') {\n return JSON.stringify(value);\n }\n if (seen.has(value as object)) {\n throw new Error('Circular reference detected in payload - cannot serialize');\n }\n seen.add(value as object);\n\n if (Array.isArray(value)) {\n return `[${value.map((item) => stableStringify(item, seen)).join(',')}]`;\n }\n const keys = Object.keys(value as Record<string, unknown>).sort();\n const entries = keys.map(\n (key) =>\n `${JSON.stringify(key)}:${stableStringify((value as Record<string, unknown>)[key], seen)}`,\n );\n return `{${entries.join(',')}}`;\n}\n\nexport function signPayload(payload: unknown, privateKeyPem: string): string {\n const data = Buffer.from(stableStringify(payload));\n return cryptoSign(null, data, privateKeyPem).toString('base64');\n}\n\nexport function verifyPayload(payload: unknown, signature: string, publicKeyPem: string): boolean {\n try {\n const data = Buffer.from(stableStringify(payload));\n return cryptoVerify(null, data, publicKeyPem, Buffer.from(signature, 'base64'));\n } catch {\n return false;\n }\n}\n\nexport function computeHash(data: string): string {\n return createHash('sha256').update(data).digest('hex');\n}\n\n/**\n * Constant-time string comparison. Same length precondition is checked\n * outside the comparison loop to avoid leaking length info.\n */\nexport function timingSafeStringEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n let diff = 0;\n for (let i = 0; i < a.length; i++) {\n diff |= a.charCodeAt(i) ^ b.charCodeAt(i);\n }\n return diff === 0;\n}\n","/**\n * Email normalization, hashing, and license payload construction.\n *\n * buildLicensePayload is the contract the fulfillment service signs against\n * and that every client must rebuild bit-for-bit before verification. Adding\n * fields here = breaking change.\n */\n\nimport { createHash } from 'crypto';\nimport type { LicensePayload, Tier } from './types.js';\n\nexport function normalizeEmail(email: string): string | null {\n if (!email || typeof email !== 'string') return null;\n const normalized = email.trim().toLowerCase();\n if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(normalized)) return null;\n return normalized.length > 0 ? normalized : null;\n}\n\nexport function hashEmail(email: string): string | null {\n const normalized = normalizeEmail(email);\n if (!normalized) return null;\n return createHash('sha256').update(normalized).digest('hex');\n}\n\nexport function buildLicensePayload(opts: {\n licenseKey: string;\n tier: Tier;\n isFounder: boolean;\n issued: string;\n emailHash?: string | null;\n}): LicensePayload {\n if (!opts.licenseKey || typeof opts.licenseKey !== 'string') {\n throw new Error('licenseKey is required and must be a string');\n }\n if (!opts.tier || typeof opts.tier !== 'string') {\n throw new Error('tier is required and must be a string');\n }\n if (!opts.issued || typeof opts.issued !== 'string') {\n throw new Error('issued is required and must be a string');\n }\n\n const payload: LicensePayload = {\n licenseKey: opts.licenseKey,\n tier: opts.tier,\n isFounder: Boolean(opts.isFounder),\n issued: opts.issued,\n };\n if (opts.emailHash) {\n payload.emailHash = opts.emailHash;\n }\n return payload;\n}\n","/**\n * Build a complete signed registry from a flat entries map.\n *\n * The registry signature covers ONLY the entries — _metadata is excluded.\n * QAA's deployed validator destructures `_metadata` out before verifying,\n * so any change to what's signed will break compatibility.\n */\n\nimport { computeHash, signPayload, stableStringify } from './signing.js';\nimport type { Registry, SignedRegistry } from './types.js';\n\nexport function buildSignedRegistry(\n entries: Registry,\n privateKeyPem: string,\n keyId = 'default',\n): SignedRegistry {\n const now = new Date().toISOString();\n const entriesStr = stableStringify(entries);\n const registrySignature = signPayload(entries, privateKeyPem);\n const hash = computeHash(entriesStr);\n\n return {\n _metadata: {\n version: '1.0',\n created: now,\n lastUpdate: now,\n description: 'BuildProven license registry — populated by fulfillment webhook',\n algorithm: 'rsa-sha256',\n keyId,\n registrySignature,\n hash,\n totalLicenses: Object.keys(entries).length,\n },\n ...entries,\n };\n}\n","/**\n * Pure validation helpers — no I/O, no caching, no env.\n *\n * Both QA Architect's CLI and claude-kit-pro's MCP server use these\n * to verify a registry response. Anything that touches disk, network,\n * or process.env stays in the consuming product. This is the seam\n * that prevents the two validators from drifting apart.\n */\n\nimport { buildLicensePayload } from './payload.js';\nimport { computeHash, stableStringify, timingSafeStringEqual, verifyPayload } from './signing.js';\nimport type { RegistryEntry, SignedRegistry } from './types.js';\n\nexport interface ValidatedEntry {\n valid: true;\n tier: RegistryEntry['tier'];\n isFounder: boolean;\n customerId: string;\n keyId: string;\n}\n\nexport interface ValidationFailure {\n valid: false;\n error: string;\n}\n\nexport type ValidationResult = ValidatedEntry | ValidationFailure;\n\n/**\n * Verify a single registry entry against its embedded signature.\n * Optionally check the user's email hash against the entry's emailHash.\n *\n * Mirrors QAA's validateLicense() field-set exactly:\n * payload = { licenseKey, tier, isFounder, issued, emailHash? }\n */\nexport function validateRegistryEntry(opts: {\n licenseKey: string;\n entry: RegistryEntry;\n publicKeyPem: string;\n /** If supplied, must match entry.emailHash (timing-safe). */\n userEmailHash?: string;\n}): ValidationResult {\n const { licenseKey, entry, publicKeyPem, userEmailHash } = opts;\n\n if (entry.emailHash && userEmailHash && !timingSafeStringEqual(userEmailHash, entry.emailHash)) {\n return { valid: false, error: 'Email address does not match license registration' };\n }\n\n const payload = buildLicensePayload({\n licenseKey,\n tier: entry.tier,\n isFounder: entry.isFounder,\n issued: entry.issued,\n emailHash: entry.emailHash,\n });\n\n if (!verifyPayload(payload, entry.signature, publicKeyPem)) {\n return { valid: false, error: 'License entry signature verification failed' };\n }\n\n return {\n valid: true,\n tier: entry.tier,\n isFounder: entry.isFounder,\n customerId: entry.customerId,\n keyId: entry.keyId,\n };\n}\n\n/**\n * Verify a complete signed registry: registry-level signature + hash check.\n * Returns the entries map (with _metadata stripped) on success, throws on failure.\n *\n * Throws (rather than returning a result) because a registry signature failure\n * should halt validation entirely — clients should not fall back to entries\n * from an unverified registry.\n */\nexport function verifyRegistryMetadata(\n signedRegistry: SignedRegistry,\n publicKeyPem: string,\n): Record<string, RegistryEntry> {\n const { _metadata, ...entries } = signedRegistry;\n\n if (!_metadata?.registrySignature) {\n throw new Error('Registry missing _metadata.registrySignature');\n }\n\n if (!verifyPayload(entries, _metadata.registrySignature, publicKeyPem)) {\n throw new Error('Registry signature verification failed');\n }\n\n if (_metadata.hash) {\n const computed = computeHash(stableStringify(entries));\n if (!timingSafeStringEqual(computed, _metadata.hash)) {\n throw new Error('Registry hash mismatch');\n }\n }\n\n return entries as Record<string, RegistryEntry>;\n}\n","/**\n * Per-product license key format.\n *\n * QAA-XXXX-XXXX-XXXX-XXXX, CKIT-XXXX-XXXX-XXXX-XXXX, etc.\n * One factory so every product validates the same way.\n */\n\nexport function licenseKeyPattern(prefix: string): RegExp {\n if (!/^[A-Z0-9]+$/.test(prefix)) {\n throw new Error(`Prefix must be uppercase alphanumeric: ${prefix}`);\n }\n return new RegExp(`^${prefix}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$`);\n}\n\nexport function isValidLicenseKey(key: string, prefix: string): boolean {\n return licenseKeyPattern(prefix).test(key.trim().toUpperCase());\n}\n\nexport function normalizeLicenseKey(key: string): string {\n return key.trim().toUpperCase();\n}\n"],"mappings":"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","names":["createHash"]}
1
+ {"version":3,"sources":["../src/signing.ts","../src/payload.ts","../src/registry.ts","../src/validator.ts","../src/key-format.ts"],"sourcesContent":["/**\n * Deterministic stringify + RSA-SHA256 sign/verify primitives.\n *\n * stableStringify must produce byte-identical output to QA Architect's\n * shipped lib/license-signing.js — the deployed CLI in customers' hands\n * uses that exact algorithm. Any divergence here breaks every QAA license\n * issued to date.\n */\n\nimport { sign as cryptoSign, verify as cryptoVerify, createHash } from 'crypto';\n\nexport function stableStringify(value: unknown, seen: WeakSet<object> = new WeakSet()): string {\n if (value === null || typeof value !== 'object') {\n return JSON.stringify(value);\n }\n if (seen.has(value as object)) {\n throw new Error('Circular reference detected in payload - cannot serialize');\n }\n seen.add(value as object);\n\n if (Array.isArray(value)) {\n return `[${value.map((item) => stableStringify(item, seen)).join(',')}]`;\n }\n const keys = Object.keys(value as Record<string, unknown>).sort();\n const entries = keys.map(\n (key) =>\n `${JSON.stringify(key)}:${stableStringify((value as Record<string, unknown>)[key], seen)}`,\n );\n return `{${entries.join(',')}}`;\n}\n\nexport function signPayload(payload: unknown, privateKeyPem: string): string {\n const data = Buffer.from(stableStringify(payload));\n return cryptoSign(null, data, privateKeyPem).toString('base64');\n}\n\nexport function verifyPayload(payload: unknown, signature: string, publicKeyPem: string): boolean {\n try {\n const data = Buffer.from(stableStringify(payload));\n return cryptoVerify(null, data, publicKeyPem, Buffer.from(signature, 'base64'));\n } catch {\n return false;\n }\n}\n\nexport function computeHash(data: string): string {\n return createHash('sha256').update(data).digest('hex');\n}\n\n/**\n * Constant-time string comparison. Same length precondition is checked\n * outside the comparison loop to avoid leaking length info.\n */\nexport function timingSafeStringEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n let diff = 0;\n for (let i = 0; i < a.length; i++) {\n diff |= a.charCodeAt(i) ^ b.charCodeAt(i);\n }\n return diff === 0;\n}\n","/**\n * Email normalization, hashing, and license payload construction.\n *\n * buildLicensePayload is the contract the fulfillment service signs against\n * and that every client must rebuild bit-for-bit before verification. Adding\n * fields here = breaking change.\n */\n\nimport { createHash } from 'crypto';\nimport type { LicensePayload, Tier } from './types.js';\n\nexport function normalizeEmail(email: string): string | null {\n if (!email || typeof email !== 'string') return null;\n const normalized = email.trim().toLowerCase();\n if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(normalized)) return null;\n return normalized.length > 0 ? normalized : null;\n}\n\nexport function hashEmail(email: string): string | null {\n const normalized = normalizeEmail(email);\n if (!normalized) return null;\n return createHash('sha256').update(normalized).digest('hex');\n}\n\nexport function buildLicensePayload(opts: {\n licenseKey: string;\n tier: Tier;\n isFounder: boolean;\n issued: string;\n emailHash?: string | null;\n}): LicensePayload {\n if (!opts.licenseKey || typeof opts.licenseKey !== 'string') {\n throw new Error('licenseKey is required and must be a string');\n }\n if (!opts.tier || typeof opts.tier !== 'string') {\n throw new Error('tier is required and must be a string');\n }\n if (!opts.issued || typeof opts.issued !== 'string') {\n throw new Error('issued is required and must be a string');\n }\n\n const payload: LicensePayload = {\n licenseKey: opts.licenseKey,\n tier: opts.tier,\n isFounder: Boolean(opts.isFounder),\n issued: opts.issued,\n };\n if (opts.emailHash) {\n payload.emailHash = opts.emailHash;\n }\n return payload;\n}\n","/**\n * Build a complete signed registry from a flat entries map.\n *\n * The registry signature covers ONLY the entries — _metadata is excluded.\n * QAA's deployed validator destructures `_metadata` out before verifying,\n * so any change to what's signed will break compatibility.\n */\n\nimport { computeHash, signPayload, stableStringify } from './signing.js';\nimport type { Registry, SignedRegistry } from './types.js';\n\nexport function buildSignedRegistry(\n entries: Registry,\n privateKeyPem: string,\n keyId = 'default',\n): SignedRegistry {\n const now = new Date().toISOString();\n const entriesStr = stableStringify(entries);\n const registrySignature = signPayload(entries, privateKeyPem);\n const hash = computeHash(entriesStr);\n\n return {\n _metadata: {\n version: '1.0',\n created: now,\n lastUpdate: now,\n description: 'License registry — populated by fulfillment webhook',\n algorithm: 'rsa-sha256',\n keyId,\n registrySignature,\n hash,\n totalLicenses: Object.keys(entries).length,\n },\n ...entries,\n };\n}\n","/**\n * Pure validation helpers — no I/O, no caching, no env.\n *\n * Both QA Architect's CLI and claude-kit-pro's MCP server use these\n * to verify a registry response. Anything that touches disk, network,\n * or process.env stays in the consuming product. This is the seam\n * that prevents the two validators from drifting apart.\n */\n\nimport { buildLicensePayload } from './payload.js';\nimport { computeHash, stableStringify, timingSafeStringEqual, verifyPayload } from './signing.js';\nimport type { RegistryEntry, SignedRegistry } from './types.js';\n\nexport interface ValidatedEntry {\n valid: true;\n tier: RegistryEntry['tier'];\n isFounder: boolean;\n customerId: string;\n keyId: string;\n}\n\nexport interface ValidationFailure {\n valid: false;\n error: string;\n}\n\nexport type ValidationResult = ValidatedEntry | ValidationFailure;\n\n/**\n * Verify a single registry entry against its embedded signature.\n * Optionally check the user's email hash against the entry's emailHash.\n *\n * Mirrors QAA's validateLicense() field-set exactly:\n * payload = { licenseKey, tier, isFounder, issued, emailHash? }\n */\nexport function validateRegistryEntry(opts: {\n licenseKey: string;\n entry: RegistryEntry;\n publicKeyPem: string;\n /** If supplied, must match entry.emailHash (timing-safe). */\n userEmailHash?: string;\n}): ValidationResult {\n const { licenseKey, entry, publicKeyPem, userEmailHash } = opts;\n\n if (entry.emailHash && userEmailHash && !timingSafeStringEqual(userEmailHash, entry.emailHash)) {\n return { valid: false, error: 'Email address does not match license registration' };\n }\n\n const payload = buildLicensePayload({\n licenseKey,\n tier: entry.tier,\n isFounder: entry.isFounder,\n issued: entry.issued,\n emailHash: entry.emailHash,\n });\n\n if (!verifyPayload(payload, entry.signature, publicKeyPem)) {\n return { valid: false, error: 'License entry signature verification failed' };\n }\n\n return {\n valid: true,\n tier: entry.tier,\n isFounder: entry.isFounder,\n customerId: entry.customerId,\n keyId: entry.keyId,\n };\n}\n\n/**\n * Verify a complete signed registry: registry-level signature + hash check.\n * Returns the entries map (with _metadata stripped) on success, throws on failure.\n *\n * Throws (rather than returning a result) because a registry signature failure\n * should halt validation entirely — clients should not fall back to entries\n * from an unverified registry.\n */\nexport function verifyRegistryMetadata(\n signedRegistry: SignedRegistry,\n publicKeyPem: string,\n): Record<string, RegistryEntry> {\n const { _metadata, ...entries } = signedRegistry;\n\n if (!_metadata?.registrySignature) {\n throw new Error('Registry missing _metadata.registrySignature');\n }\n\n if (!verifyPayload(entries, _metadata.registrySignature, publicKeyPem)) {\n throw new Error('Registry signature verification failed');\n }\n\n if (_metadata.hash) {\n const computed = computeHash(stableStringify(entries));\n if (!timingSafeStringEqual(computed, _metadata.hash)) {\n throw new Error('Registry hash mismatch');\n }\n }\n\n return entries as Record<string, RegistryEntry>;\n}\n","/**\n * Per-product license key format.\n *\n * QAA-XXXX-XXXX-XXXX-XXXX, CKIT-XXXX-XXXX-XXXX-XXXX, etc.\n * One factory so every product validates the same way.\n */\n\nexport function licenseKeyPattern(prefix: string): RegExp {\n if (!/^[A-Z0-9]+$/.test(prefix)) {\n throw new Error(`Prefix must be uppercase alphanumeric: ${prefix}`);\n }\n return new RegExp(`^${prefix}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$`);\n}\n\nexport function isValidLicenseKey(key: string, prefix: string): boolean {\n return licenseKeyPattern(prefix).test(key.trim().toUpperCase());\n}\n\nexport function normalizeLicenseKey(key: string): string {\n return key.trim().toUpperCase();\n}\n"],"mappings":"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","names":["createHash"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buildproven/license-core",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Shared license signing & verification primitives for BuildProven products (RSA-SHA256, signed registry).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "repository": {
44
44
  "type": "git",
45
- "url": "https://github.com/buildproven/buildproven-license-core.git"
45
+ "url": "https://github.com/buildproven/license-core.git"
46
46
  },
47
47
  "license": "MIT",
48
48
  "keywords": [