@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 +133 -38
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,71 +1,166 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@buildproven/license-core`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@buildproven/license-core)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
5
|
|
|
5
|
-
|
|
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
|
-
|
|
8
|
+
```
|
|
9
|
+
npm install @buildproven/license-core
|
|
10
|
+
```
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
## Why this exists
|
|
10
13
|
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
21
|
+
## Quick example
|
|
22
|
+
|
|
23
|
+
### Server side (when a customer purchases)
|
|
20
24
|
|
|
21
25
|
```ts
|
|
22
26
|
import {
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
buildLicensePayload,
|
|
28
|
+
buildSignedRegistry,
|
|
29
|
+
hashEmail,
|
|
25
30
|
signPayload,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
timingSafeStringEqual,
|
|
31
|
+
type Registry,
|
|
32
|
+
} from '@buildproven/license-core';
|
|
29
33
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
36
|
-
buildSignedRegistry,
|
|
61
|
+
### Client side (when the user starts your app)
|
|
37
62
|
|
|
38
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
##
|
|
142
|
+
## How keys work
|
|
143
|
+
|
|
144
|
+
You generate one RSA-2048 keypair per product:
|
|
56
145
|
|
|
57
146
|
```bash
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: "
|
|
129
|
+
description: "License registry \u2014 populated by fulfillment webhook",
|
|
130
130
|
algorithm: "rsa-sha256",
|
|
131
131
|
keyId,
|
|
132
132
|
registrySignature,
|
package/dist/index.cjs.map
CHANGED
|
@@ -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
|
|
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
|
|
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: "
|
|
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.
|
|
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/
|
|
45
|
+
"url": "https://github.com/buildproven/license-core.git"
|
|
46
46
|
},
|
|
47
47
|
"license": "MIT",
|
|
48
48
|
"keywords": [
|