@buildproven/license-core 1.0.0
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/LICENSE +21 -0
- package/README.md +71 -0
- package/dist/index.cjs +211 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +157 -0
- package/dist/index.d.ts +157 -0
- package/dist/index.js +171 -0
- package/dist/index.js.map +1 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vibe Build Lab LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# @buildproven/license-core
|
|
2
|
+
|
|
3
|
+
Shared license signing & verification primitives for BuildProven products.
|
|
4
|
+
|
|
5
|
+
## What this is
|
|
6
|
+
|
|
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
|
+
Used by:
|
|
10
|
+
|
|
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
|
+
|
|
15
|
+
## Why it exists
|
|
16
|
+
|
|
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.
|
|
18
|
+
|
|
19
|
+
## API
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import {
|
|
23
|
+
// Crypto primitives
|
|
24
|
+
stableStringify,
|
|
25
|
+
signPayload,
|
|
26
|
+
verifyPayload,
|
|
27
|
+
computeHash,
|
|
28
|
+
timingSafeStringEqual,
|
|
29
|
+
|
|
30
|
+
// Payload construction
|
|
31
|
+
normalizeEmail,
|
|
32
|
+
hashEmail,
|
|
33
|
+
buildLicensePayload,
|
|
34
|
+
|
|
35
|
+
// Registry
|
|
36
|
+
buildSignedRegistry,
|
|
37
|
+
|
|
38
|
+
// Validation helpers (pure — no I/O)
|
|
39
|
+
validateRegistryEntry,
|
|
40
|
+
verifyRegistryMetadata,
|
|
41
|
+
|
|
42
|
+
// Key format
|
|
43
|
+
licenseKeyPattern,
|
|
44
|
+
isValidLicenseKey,
|
|
45
|
+
normalizeLicenseKey,
|
|
46
|
+
} from '@buildproven/license-core';
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Frozen contract
|
|
50
|
+
|
|
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.
|
|
52
|
+
|
|
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.
|
|
54
|
+
|
|
55
|
+
## Install
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npm install @buildproven/license-core
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Develop
|
|
62
|
+
|
|
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
|
+
```
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
buildLicensePayload: () => buildLicensePayload,
|
|
24
|
+
buildSignedRegistry: () => buildSignedRegistry,
|
|
25
|
+
computeHash: () => computeHash,
|
|
26
|
+
hashEmail: () => hashEmail,
|
|
27
|
+
isValidLicenseKey: () => isValidLicenseKey,
|
|
28
|
+
licenseKeyPattern: () => licenseKeyPattern,
|
|
29
|
+
normalizeEmail: () => normalizeEmail,
|
|
30
|
+
normalizeLicenseKey: () => normalizeLicenseKey,
|
|
31
|
+
signPayload: () => signPayload,
|
|
32
|
+
stableStringify: () => stableStringify,
|
|
33
|
+
timingSafeStringEqual: () => timingSafeStringEqual,
|
|
34
|
+
validateRegistryEntry: () => validateRegistryEntry,
|
|
35
|
+
verifyPayload: () => verifyPayload,
|
|
36
|
+
verifyRegistryMetadata: () => verifyRegistryMetadata
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(index_exports);
|
|
39
|
+
|
|
40
|
+
// src/signing.ts
|
|
41
|
+
var import_crypto = require("crypto");
|
|
42
|
+
function stableStringify(value, seen = /* @__PURE__ */ new WeakSet()) {
|
|
43
|
+
if (value === null || typeof value !== "object") {
|
|
44
|
+
return JSON.stringify(value);
|
|
45
|
+
}
|
|
46
|
+
if (seen.has(value)) {
|
|
47
|
+
throw new Error("Circular reference detected in payload - cannot serialize");
|
|
48
|
+
}
|
|
49
|
+
seen.add(value);
|
|
50
|
+
if (Array.isArray(value)) {
|
|
51
|
+
return `[${value.map((item) => stableStringify(item, seen)).join(",")}]`;
|
|
52
|
+
}
|
|
53
|
+
const keys = Object.keys(value).sort();
|
|
54
|
+
const entries = keys.map(
|
|
55
|
+
(key) => `${JSON.stringify(key)}:${stableStringify(value[key], seen)}`
|
|
56
|
+
);
|
|
57
|
+
return `{${entries.join(",")}}`;
|
|
58
|
+
}
|
|
59
|
+
function signPayload(payload, privateKeyPem) {
|
|
60
|
+
const data = Buffer.from(stableStringify(payload));
|
|
61
|
+
return (0, import_crypto.sign)(null, data, privateKeyPem).toString("base64");
|
|
62
|
+
}
|
|
63
|
+
function verifyPayload(payload, signature, publicKeyPem) {
|
|
64
|
+
try {
|
|
65
|
+
const data = Buffer.from(stableStringify(payload));
|
|
66
|
+
return (0, import_crypto.verify)(null, data, publicKeyPem, Buffer.from(signature, "base64"));
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function computeHash(data) {
|
|
72
|
+
return (0, import_crypto.createHash)("sha256").update(data).digest("hex");
|
|
73
|
+
}
|
|
74
|
+
function timingSafeStringEqual(a, b) {
|
|
75
|
+
if (a.length !== b.length) return false;
|
|
76
|
+
let diff = 0;
|
|
77
|
+
for (let i = 0; i < a.length; i++) {
|
|
78
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
79
|
+
}
|
|
80
|
+
return diff === 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/payload.ts
|
|
84
|
+
var import_crypto2 = require("crypto");
|
|
85
|
+
function normalizeEmail(email) {
|
|
86
|
+
if (!email || typeof email !== "string") return null;
|
|
87
|
+
const normalized = email.trim().toLowerCase();
|
|
88
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalized)) return null;
|
|
89
|
+
return normalized.length > 0 ? normalized : null;
|
|
90
|
+
}
|
|
91
|
+
function hashEmail(email) {
|
|
92
|
+
const normalized = normalizeEmail(email);
|
|
93
|
+
if (!normalized) return null;
|
|
94
|
+
return (0, import_crypto2.createHash)("sha256").update(normalized).digest("hex");
|
|
95
|
+
}
|
|
96
|
+
function buildLicensePayload(opts) {
|
|
97
|
+
if (!opts.licenseKey || typeof opts.licenseKey !== "string") {
|
|
98
|
+
throw new Error("licenseKey is required and must be a string");
|
|
99
|
+
}
|
|
100
|
+
if (!opts.tier || typeof opts.tier !== "string") {
|
|
101
|
+
throw new Error("tier is required and must be a string");
|
|
102
|
+
}
|
|
103
|
+
if (!opts.issued || typeof opts.issued !== "string") {
|
|
104
|
+
throw new Error("issued is required and must be a string");
|
|
105
|
+
}
|
|
106
|
+
const payload = {
|
|
107
|
+
licenseKey: opts.licenseKey,
|
|
108
|
+
tier: opts.tier,
|
|
109
|
+
isFounder: Boolean(opts.isFounder),
|
|
110
|
+
issued: opts.issued
|
|
111
|
+
};
|
|
112
|
+
if (opts.emailHash) {
|
|
113
|
+
payload.emailHash = opts.emailHash;
|
|
114
|
+
}
|
|
115
|
+
return payload;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/registry.ts
|
|
119
|
+
function buildSignedRegistry(entries, privateKeyPem, keyId = "default") {
|
|
120
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
121
|
+
const entriesStr = stableStringify(entries);
|
|
122
|
+
const registrySignature = signPayload(entries, privateKeyPem);
|
|
123
|
+
const hash = computeHash(entriesStr);
|
|
124
|
+
return {
|
|
125
|
+
_metadata: {
|
|
126
|
+
version: "1.0",
|
|
127
|
+
created: now,
|
|
128
|
+
lastUpdate: now,
|
|
129
|
+
description: "BuildProven license registry \u2014 populated by fulfillment webhook",
|
|
130
|
+
algorithm: "rsa-sha256",
|
|
131
|
+
keyId,
|
|
132
|
+
registrySignature,
|
|
133
|
+
hash,
|
|
134
|
+
totalLicenses: Object.keys(entries).length
|
|
135
|
+
},
|
|
136
|
+
...entries
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/validator.ts
|
|
141
|
+
function validateRegistryEntry(opts) {
|
|
142
|
+
const { licenseKey, entry, publicKeyPem, userEmailHash } = opts;
|
|
143
|
+
if (entry.emailHash && userEmailHash && !timingSafeStringEqual(userEmailHash, entry.emailHash)) {
|
|
144
|
+
return { valid: false, error: "Email address does not match license registration" };
|
|
145
|
+
}
|
|
146
|
+
const payload = buildLicensePayload({
|
|
147
|
+
licenseKey,
|
|
148
|
+
tier: entry.tier,
|
|
149
|
+
isFounder: entry.isFounder,
|
|
150
|
+
issued: entry.issued,
|
|
151
|
+
emailHash: entry.emailHash
|
|
152
|
+
});
|
|
153
|
+
if (!verifyPayload(payload, entry.signature, publicKeyPem)) {
|
|
154
|
+
return { valid: false, error: "License entry signature verification failed" };
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
valid: true,
|
|
158
|
+
tier: entry.tier,
|
|
159
|
+
isFounder: entry.isFounder,
|
|
160
|
+
customerId: entry.customerId,
|
|
161
|
+
keyId: entry.keyId
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function verifyRegistryMetadata(signedRegistry, publicKeyPem) {
|
|
165
|
+
const { _metadata, ...entries } = signedRegistry;
|
|
166
|
+
if (!_metadata?.registrySignature) {
|
|
167
|
+
throw new Error("Registry missing _metadata.registrySignature");
|
|
168
|
+
}
|
|
169
|
+
if (!verifyPayload(entries, _metadata.registrySignature, publicKeyPem)) {
|
|
170
|
+
throw new Error("Registry signature verification failed");
|
|
171
|
+
}
|
|
172
|
+
if (_metadata.hash) {
|
|
173
|
+
const computed = computeHash(stableStringify(entries));
|
|
174
|
+
if (!timingSafeStringEqual(computed, _metadata.hash)) {
|
|
175
|
+
throw new Error("Registry hash mismatch");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return entries;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/key-format.ts
|
|
182
|
+
function licenseKeyPattern(prefix) {
|
|
183
|
+
if (!/^[A-Z0-9]+$/.test(prefix)) {
|
|
184
|
+
throw new Error(`Prefix must be uppercase alphanumeric: ${prefix}`);
|
|
185
|
+
}
|
|
186
|
+
return new RegExp(`^${prefix}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$`);
|
|
187
|
+
}
|
|
188
|
+
function isValidLicenseKey(key, prefix) {
|
|
189
|
+
return licenseKeyPattern(prefix).test(key.trim().toUpperCase());
|
|
190
|
+
}
|
|
191
|
+
function normalizeLicenseKey(key) {
|
|
192
|
+
return key.trim().toUpperCase();
|
|
193
|
+
}
|
|
194
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
195
|
+
0 && (module.exports = {
|
|
196
|
+
buildLicensePayload,
|
|
197
|
+
buildSignedRegistry,
|
|
198
|
+
computeHash,
|
|
199
|
+
hashEmail,
|
|
200
|
+
isValidLicenseKey,
|
|
201
|
+
licenseKeyPattern,
|
|
202
|
+
normalizeEmail,
|
|
203
|
+
normalizeLicenseKey,
|
|
204
|
+
signPayload,
|
|
205
|
+
stableStringify,
|
|
206
|
+
timingSafeStringEqual,
|
|
207
|
+
validateRegistryEntry,
|
|
208
|
+
verifyPayload,
|
|
209
|
+
verifyRegistryMetadata
|
|
210
|
+
});
|
|
211
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +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"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic stringify + RSA-SHA256 sign/verify primitives.
|
|
3
|
+
*
|
|
4
|
+
* stableStringify must produce byte-identical output to QA Architect's
|
|
5
|
+
* shipped lib/license-signing.js — the deployed CLI in customers' hands
|
|
6
|
+
* uses that exact algorithm. Any divergence here breaks every QAA license
|
|
7
|
+
* issued to date.
|
|
8
|
+
*/
|
|
9
|
+
declare function stableStringify(value: unknown, seen?: WeakSet<object>): string;
|
|
10
|
+
declare function signPayload(payload: unknown, privateKeyPem: string): string;
|
|
11
|
+
declare function verifyPayload(payload: unknown, signature: string, publicKeyPem: string): boolean;
|
|
12
|
+
declare function computeHash(data: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Constant-time string comparison. Same length precondition is checked
|
|
15
|
+
* outside the comparison loop to avoid leaking length info.
|
|
16
|
+
*/
|
|
17
|
+
declare function timingSafeStringEqual(a: string, b: string): boolean;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Core types for BuildProven license payloads.
|
|
21
|
+
*
|
|
22
|
+
* IMPORTANT: changing the shape of LicensePayload or RegistryEntry
|
|
23
|
+
* breaks signature verification across every product in the field.
|
|
24
|
+
* Treat these as a frozen contract — bump schemaVersion + ship a new
|
|
25
|
+
* major instead of mutating.
|
|
26
|
+
*/
|
|
27
|
+
type Tier = 'FREE' | 'PRO';
|
|
28
|
+
/**
|
|
29
|
+
* The payload that is RSA-signed for each license entry.
|
|
30
|
+
* Field set is FROZEN — see comment above.
|
|
31
|
+
*/
|
|
32
|
+
interface LicensePayload {
|
|
33
|
+
licenseKey: string;
|
|
34
|
+
tier: Tier;
|
|
35
|
+
isFounder: boolean;
|
|
36
|
+
issued: string;
|
|
37
|
+
/** Optional — only included when emailHash is non-null on the entry. */
|
|
38
|
+
emailHash?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* One row in the signed registry.
|
|
42
|
+
* Stored in Vercel KV by the fulfillment service, fetched + verified by clients.
|
|
43
|
+
*/
|
|
44
|
+
interface RegistryEntry {
|
|
45
|
+
tier: Tier;
|
|
46
|
+
isFounder: boolean;
|
|
47
|
+
issued: string;
|
|
48
|
+
emailHash: string | null;
|
|
49
|
+
/** RSA-SHA256 base64 signature of the LicensePayload above. */
|
|
50
|
+
signature: string;
|
|
51
|
+
customerId: string;
|
|
52
|
+
/** Identifies which keypair signed this entry — for rotation. */
|
|
53
|
+
keyId: string;
|
|
54
|
+
}
|
|
55
|
+
type Registry = Record<string, RegistryEntry>;
|
|
56
|
+
interface RegistryMetadata {
|
|
57
|
+
version: string;
|
|
58
|
+
created: string;
|
|
59
|
+
lastUpdate: string;
|
|
60
|
+
description: string;
|
|
61
|
+
algorithm: string;
|
|
62
|
+
keyId: string;
|
|
63
|
+
/** RSA-SHA256 base64 signature over the entries (metadata excluded). */
|
|
64
|
+
registrySignature: string;
|
|
65
|
+
/** SHA-256 hex of stableStringify(entries). */
|
|
66
|
+
hash: string;
|
|
67
|
+
totalLicenses: number;
|
|
68
|
+
}
|
|
69
|
+
interface SignedRegistry {
|
|
70
|
+
_metadata: RegistryMetadata;
|
|
71
|
+
[licenseKey: string]: RegistryEntry | RegistryMetadata;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Email normalization, hashing, and license payload construction.
|
|
76
|
+
*
|
|
77
|
+
* buildLicensePayload is the contract the fulfillment service signs against
|
|
78
|
+
* and that every client must rebuild bit-for-bit before verification. Adding
|
|
79
|
+
* fields here = breaking change.
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
declare function normalizeEmail(email: string): string | null;
|
|
83
|
+
declare function hashEmail(email: string): string | null;
|
|
84
|
+
declare function buildLicensePayload(opts: {
|
|
85
|
+
licenseKey: string;
|
|
86
|
+
tier: Tier;
|
|
87
|
+
isFounder: boolean;
|
|
88
|
+
issued: string;
|
|
89
|
+
emailHash?: string | null;
|
|
90
|
+
}): LicensePayload;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Build a complete signed registry from a flat entries map.
|
|
94
|
+
*
|
|
95
|
+
* The registry signature covers ONLY the entries — _metadata is excluded.
|
|
96
|
+
* QAA's deployed validator destructures `_metadata` out before verifying,
|
|
97
|
+
* so any change to what's signed will break compatibility.
|
|
98
|
+
*/
|
|
99
|
+
|
|
100
|
+
declare function buildSignedRegistry(entries: Registry, privateKeyPem: string, keyId?: string): SignedRegistry;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Pure validation helpers — no I/O, no caching, no env.
|
|
104
|
+
*
|
|
105
|
+
* Both QA Architect's CLI and claude-kit-pro's MCP server use these
|
|
106
|
+
* to verify a registry response. Anything that touches disk, network,
|
|
107
|
+
* or process.env stays in the consuming product. This is the seam
|
|
108
|
+
* that prevents the two validators from drifting apart.
|
|
109
|
+
*/
|
|
110
|
+
|
|
111
|
+
interface ValidatedEntry {
|
|
112
|
+
valid: true;
|
|
113
|
+
tier: RegistryEntry['tier'];
|
|
114
|
+
isFounder: boolean;
|
|
115
|
+
customerId: string;
|
|
116
|
+
keyId: string;
|
|
117
|
+
}
|
|
118
|
+
interface ValidationFailure {
|
|
119
|
+
valid: false;
|
|
120
|
+
error: string;
|
|
121
|
+
}
|
|
122
|
+
type ValidationResult = ValidatedEntry | ValidationFailure;
|
|
123
|
+
/**
|
|
124
|
+
* Verify a single registry entry against its embedded signature.
|
|
125
|
+
* Optionally check the user's email hash against the entry's emailHash.
|
|
126
|
+
*
|
|
127
|
+
* Mirrors QAA's validateLicense() field-set exactly:
|
|
128
|
+
* payload = { licenseKey, tier, isFounder, issued, emailHash? }
|
|
129
|
+
*/
|
|
130
|
+
declare function validateRegistryEntry(opts: {
|
|
131
|
+
licenseKey: string;
|
|
132
|
+
entry: RegistryEntry;
|
|
133
|
+
publicKeyPem: string;
|
|
134
|
+
/** If supplied, must match entry.emailHash (timing-safe). */
|
|
135
|
+
userEmailHash?: string;
|
|
136
|
+
}): ValidationResult;
|
|
137
|
+
/**
|
|
138
|
+
* Verify a complete signed registry: registry-level signature + hash check.
|
|
139
|
+
* Returns the entries map (with _metadata stripped) on success, throws on failure.
|
|
140
|
+
*
|
|
141
|
+
* Throws (rather than returning a result) because a registry signature failure
|
|
142
|
+
* should halt validation entirely — clients should not fall back to entries
|
|
143
|
+
* from an unverified registry.
|
|
144
|
+
*/
|
|
145
|
+
declare function verifyRegistryMetadata(signedRegistry: SignedRegistry, publicKeyPem: string): Record<string, RegistryEntry>;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Per-product license key format.
|
|
149
|
+
*
|
|
150
|
+
* QAA-XXXX-XXXX-XXXX-XXXX, CKIT-XXXX-XXXX-XXXX-XXXX, etc.
|
|
151
|
+
* One factory so every product validates the same way.
|
|
152
|
+
*/
|
|
153
|
+
declare function licenseKeyPattern(prefix: string): RegExp;
|
|
154
|
+
declare function isValidLicenseKey(key: string, prefix: string): boolean;
|
|
155
|
+
declare function normalizeLicenseKey(key: string): string;
|
|
156
|
+
|
|
157
|
+
export { type LicensePayload, type Registry, type RegistryEntry, type RegistryMetadata, type SignedRegistry, type Tier, type ValidatedEntry, type ValidationFailure, type ValidationResult, buildLicensePayload, buildSignedRegistry, computeHash, hashEmail, isValidLicenseKey, licenseKeyPattern, normalizeEmail, normalizeLicenseKey, signPayload, stableStringify, timingSafeStringEqual, validateRegistryEntry, verifyPayload, verifyRegistryMetadata };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic stringify + RSA-SHA256 sign/verify primitives.
|
|
3
|
+
*
|
|
4
|
+
* stableStringify must produce byte-identical output to QA Architect's
|
|
5
|
+
* shipped lib/license-signing.js — the deployed CLI in customers' hands
|
|
6
|
+
* uses that exact algorithm. Any divergence here breaks every QAA license
|
|
7
|
+
* issued to date.
|
|
8
|
+
*/
|
|
9
|
+
declare function stableStringify(value: unknown, seen?: WeakSet<object>): string;
|
|
10
|
+
declare function signPayload(payload: unknown, privateKeyPem: string): string;
|
|
11
|
+
declare function verifyPayload(payload: unknown, signature: string, publicKeyPem: string): boolean;
|
|
12
|
+
declare function computeHash(data: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Constant-time string comparison. Same length precondition is checked
|
|
15
|
+
* outside the comparison loop to avoid leaking length info.
|
|
16
|
+
*/
|
|
17
|
+
declare function timingSafeStringEqual(a: string, b: string): boolean;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Core types for BuildProven license payloads.
|
|
21
|
+
*
|
|
22
|
+
* IMPORTANT: changing the shape of LicensePayload or RegistryEntry
|
|
23
|
+
* breaks signature verification across every product in the field.
|
|
24
|
+
* Treat these as a frozen contract — bump schemaVersion + ship a new
|
|
25
|
+
* major instead of mutating.
|
|
26
|
+
*/
|
|
27
|
+
type Tier = 'FREE' | 'PRO';
|
|
28
|
+
/**
|
|
29
|
+
* The payload that is RSA-signed for each license entry.
|
|
30
|
+
* Field set is FROZEN — see comment above.
|
|
31
|
+
*/
|
|
32
|
+
interface LicensePayload {
|
|
33
|
+
licenseKey: string;
|
|
34
|
+
tier: Tier;
|
|
35
|
+
isFounder: boolean;
|
|
36
|
+
issued: string;
|
|
37
|
+
/** Optional — only included when emailHash is non-null on the entry. */
|
|
38
|
+
emailHash?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* One row in the signed registry.
|
|
42
|
+
* Stored in Vercel KV by the fulfillment service, fetched + verified by clients.
|
|
43
|
+
*/
|
|
44
|
+
interface RegistryEntry {
|
|
45
|
+
tier: Tier;
|
|
46
|
+
isFounder: boolean;
|
|
47
|
+
issued: string;
|
|
48
|
+
emailHash: string | null;
|
|
49
|
+
/** RSA-SHA256 base64 signature of the LicensePayload above. */
|
|
50
|
+
signature: string;
|
|
51
|
+
customerId: string;
|
|
52
|
+
/** Identifies which keypair signed this entry — for rotation. */
|
|
53
|
+
keyId: string;
|
|
54
|
+
}
|
|
55
|
+
type Registry = Record<string, RegistryEntry>;
|
|
56
|
+
interface RegistryMetadata {
|
|
57
|
+
version: string;
|
|
58
|
+
created: string;
|
|
59
|
+
lastUpdate: string;
|
|
60
|
+
description: string;
|
|
61
|
+
algorithm: string;
|
|
62
|
+
keyId: string;
|
|
63
|
+
/** RSA-SHA256 base64 signature over the entries (metadata excluded). */
|
|
64
|
+
registrySignature: string;
|
|
65
|
+
/** SHA-256 hex of stableStringify(entries). */
|
|
66
|
+
hash: string;
|
|
67
|
+
totalLicenses: number;
|
|
68
|
+
}
|
|
69
|
+
interface SignedRegistry {
|
|
70
|
+
_metadata: RegistryMetadata;
|
|
71
|
+
[licenseKey: string]: RegistryEntry | RegistryMetadata;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Email normalization, hashing, and license payload construction.
|
|
76
|
+
*
|
|
77
|
+
* buildLicensePayload is the contract the fulfillment service signs against
|
|
78
|
+
* and that every client must rebuild bit-for-bit before verification. Adding
|
|
79
|
+
* fields here = breaking change.
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
declare function normalizeEmail(email: string): string | null;
|
|
83
|
+
declare function hashEmail(email: string): string | null;
|
|
84
|
+
declare function buildLicensePayload(opts: {
|
|
85
|
+
licenseKey: string;
|
|
86
|
+
tier: Tier;
|
|
87
|
+
isFounder: boolean;
|
|
88
|
+
issued: string;
|
|
89
|
+
emailHash?: string | null;
|
|
90
|
+
}): LicensePayload;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Build a complete signed registry from a flat entries map.
|
|
94
|
+
*
|
|
95
|
+
* The registry signature covers ONLY the entries — _metadata is excluded.
|
|
96
|
+
* QAA's deployed validator destructures `_metadata` out before verifying,
|
|
97
|
+
* so any change to what's signed will break compatibility.
|
|
98
|
+
*/
|
|
99
|
+
|
|
100
|
+
declare function buildSignedRegistry(entries: Registry, privateKeyPem: string, keyId?: string): SignedRegistry;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Pure validation helpers — no I/O, no caching, no env.
|
|
104
|
+
*
|
|
105
|
+
* Both QA Architect's CLI and claude-kit-pro's MCP server use these
|
|
106
|
+
* to verify a registry response. Anything that touches disk, network,
|
|
107
|
+
* or process.env stays in the consuming product. This is the seam
|
|
108
|
+
* that prevents the two validators from drifting apart.
|
|
109
|
+
*/
|
|
110
|
+
|
|
111
|
+
interface ValidatedEntry {
|
|
112
|
+
valid: true;
|
|
113
|
+
tier: RegistryEntry['tier'];
|
|
114
|
+
isFounder: boolean;
|
|
115
|
+
customerId: string;
|
|
116
|
+
keyId: string;
|
|
117
|
+
}
|
|
118
|
+
interface ValidationFailure {
|
|
119
|
+
valid: false;
|
|
120
|
+
error: string;
|
|
121
|
+
}
|
|
122
|
+
type ValidationResult = ValidatedEntry | ValidationFailure;
|
|
123
|
+
/**
|
|
124
|
+
* Verify a single registry entry against its embedded signature.
|
|
125
|
+
* Optionally check the user's email hash against the entry's emailHash.
|
|
126
|
+
*
|
|
127
|
+
* Mirrors QAA's validateLicense() field-set exactly:
|
|
128
|
+
* payload = { licenseKey, tier, isFounder, issued, emailHash? }
|
|
129
|
+
*/
|
|
130
|
+
declare function validateRegistryEntry(opts: {
|
|
131
|
+
licenseKey: string;
|
|
132
|
+
entry: RegistryEntry;
|
|
133
|
+
publicKeyPem: string;
|
|
134
|
+
/** If supplied, must match entry.emailHash (timing-safe). */
|
|
135
|
+
userEmailHash?: string;
|
|
136
|
+
}): ValidationResult;
|
|
137
|
+
/**
|
|
138
|
+
* Verify a complete signed registry: registry-level signature + hash check.
|
|
139
|
+
* Returns the entries map (with _metadata stripped) on success, throws on failure.
|
|
140
|
+
*
|
|
141
|
+
* Throws (rather than returning a result) because a registry signature failure
|
|
142
|
+
* should halt validation entirely — clients should not fall back to entries
|
|
143
|
+
* from an unverified registry.
|
|
144
|
+
*/
|
|
145
|
+
declare function verifyRegistryMetadata(signedRegistry: SignedRegistry, publicKeyPem: string): Record<string, RegistryEntry>;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Per-product license key format.
|
|
149
|
+
*
|
|
150
|
+
* QAA-XXXX-XXXX-XXXX-XXXX, CKIT-XXXX-XXXX-XXXX-XXXX, etc.
|
|
151
|
+
* One factory so every product validates the same way.
|
|
152
|
+
*/
|
|
153
|
+
declare function licenseKeyPattern(prefix: string): RegExp;
|
|
154
|
+
declare function isValidLicenseKey(key: string, prefix: string): boolean;
|
|
155
|
+
declare function normalizeLicenseKey(key: string): string;
|
|
156
|
+
|
|
157
|
+
export { type LicensePayload, type Registry, type RegistryEntry, type RegistryMetadata, type SignedRegistry, type Tier, type ValidatedEntry, type ValidationFailure, type ValidationResult, buildLicensePayload, buildSignedRegistry, computeHash, hashEmail, isValidLicenseKey, licenseKeyPattern, normalizeEmail, normalizeLicenseKey, signPayload, stableStringify, timingSafeStringEqual, validateRegistryEntry, verifyPayload, verifyRegistryMetadata };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// src/signing.ts
|
|
2
|
+
import { sign as cryptoSign, verify as cryptoVerify, createHash } from "crypto";
|
|
3
|
+
function stableStringify(value, seen = /* @__PURE__ */ new WeakSet()) {
|
|
4
|
+
if (value === null || typeof value !== "object") {
|
|
5
|
+
return JSON.stringify(value);
|
|
6
|
+
}
|
|
7
|
+
if (seen.has(value)) {
|
|
8
|
+
throw new Error("Circular reference detected in payload - cannot serialize");
|
|
9
|
+
}
|
|
10
|
+
seen.add(value);
|
|
11
|
+
if (Array.isArray(value)) {
|
|
12
|
+
return `[${value.map((item) => stableStringify(item, seen)).join(",")}]`;
|
|
13
|
+
}
|
|
14
|
+
const keys = Object.keys(value).sort();
|
|
15
|
+
const entries = keys.map(
|
|
16
|
+
(key) => `${JSON.stringify(key)}:${stableStringify(value[key], seen)}`
|
|
17
|
+
);
|
|
18
|
+
return `{${entries.join(",")}}`;
|
|
19
|
+
}
|
|
20
|
+
function signPayload(payload, privateKeyPem) {
|
|
21
|
+
const data = Buffer.from(stableStringify(payload));
|
|
22
|
+
return cryptoSign(null, data, privateKeyPem).toString("base64");
|
|
23
|
+
}
|
|
24
|
+
function verifyPayload(payload, signature, publicKeyPem) {
|
|
25
|
+
try {
|
|
26
|
+
const data = Buffer.from(stableStringify(payload));
|
|
27
|
+
return cryptoVerify(null, data, publicKeyPem, Buffer.from(signature, "base64"));
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function computeHash(data) {
|
|
33
|
+
return createHash("sha256").update(data).digest("hex");
|
|
34
|
+
}
|
|
35
|
+
function timingSafeStringEqual(a, b) {
|
|
36
|
+
if (a.length !== b.length) return false;
|
|
37
|
+
let diff = 0;
|
|
38
|
+
for (let i = 0; i < a.length; i++) {
|
|
39
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
40
|
+
}
|
|
41
|
+
return diff === 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// src/payload.ts
|
|
45
|
+
import { createHash as createHash2 } from "crypto";
|
|
46
|
+
function normalizeEmail(email) {
|
|
47
|
+
if (!email || typeof email !== "string") return null;
|
|
48
|
+
const normalized = email.trim().toLowerCase();
|
|
49
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalized)) return null;
|
|
50
|
+
return normalized.length > 0 ? normalized : null;
|
|
51
|
+
}
|
|
52
|
+
function hashEmail(email) {
|
|
53
|
+
const normalized = normalizeEmail(email);
|
|
54
|
+
if (!normalized) return null;
|
|
55
|
+
return createHash2("sha256").update(normalized).digest("hex");
|
|
56
|
+
}
|
|
57
|
+
function buildLicensePayload(opts) {
|
|
58
|
+
if (!opts.licenseKey || typeof opts.licenseKey !== "string") {
|
|
59
|
+
throw new Error("licenseKey is required and must be a string");
|
|
60
|
+
}
|
|
61
|
+
if (!opts.tier || typeof opts.tier !== "string") {
|
|
62
|
+
throw new Error("tier is required and must be a string");
|
|
63
|
+
}
|
|
64
|
+
if (!opts.issued || typeof opts.issued !== "string") {
|
|
65
|
+
throw new Error("issued is required and must be a string");
|
|
66
|
+
}
|
|
67
|
+
const payload = {
|
|
68
|
+
licenseKey: opts.licenseKey,
|
|
69
|
+
tier: opts.tier,
|
|
70
|
+
isFounder: Boolean(opts.isFounder),
|
|
71
|
+
issued: opts.issued
|
|
72
|
+
};
|
|
73
|
+
if (opts.emailHash) {
|
|
74
|
+
payload.emailHash = opts.emailHash;
|
|
75
|
+
}
|
|
76
|
+
return payload;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/registry.ts
|
|
80
|
+
function buildSignedRegistry(entries, privateKeyPem, keyId = "default") {
|
|
81
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
82
|
+
const entriesStr = stableStringify(entries);
|
|
83
|
+
const registrySignature = signPayload(entries, privateKeyPem);
|
|
84
|
+
const hash = computeHash(entriesStr);
|
|
85
|
+
return {
|
|
86
|
+
_metadata: {
|
|
87
|
+
version: "1.0",
|
|
88
|
+
created: now,
|
|
89
|
+
lastUpdate: now,
|
|
90
|
+
description: "BuildProven license registry \u2014 populated by fulfillment webhook",
|
|
91
|
+
algorithm: "rsa-sha256",
|
|
92
|
+
keyId,
|
|
93
|
+
registrySignature,
|
|
94
|
+
hash,
|
|
95
|
+
totalLicenses: Object.keys(entries).length
|
|
96
|
+
},
|
|
97
|
+
...entries
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/validator.ts
|
|
102
|
+
function validateRegistryEntry(opts) {
|
|
103
|
+
const { licenseKey, entry, publicKeyPem, userEmailHash } = opts;
|
|
104
|
+
if (entry.emailHash && userEmailHash && !timingSafeStringEqual(userEmailHash, entry.emailHash)) {
|
|
105
|
+
return { valid: false, error: "Email address does not match license registration" };
|
|
106
|
+
}
|
|
107
|
+
const payload = buildLicensePayload({
|
|
108
|
+
licenseKey,
|
|
109
|
+
tier: entry.tier,
|
|
110
|
+
isFounder: entry.isFounder,
|
|
111
|
+
issued: entry.issued,
|
|
112
|
+
emailHash: entry.emailHash
|
|
113
|
+
});
|
|
114
|
+
if (!verifyPayload(payload, entry.signature, publicKeyPem)) {
|
|
115
|
+
return { valid: false, error: "License entry signature verification failed" };
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
valid: true,
|
|
119
|
+
tier: entry.tier,
|
|
120
|
+
isFounder: entry.isFounder,
|
|
121
|
+
customerId: entry.customerId,
|
|
122
|
+
keyId: entry.keyId
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function verifyRegistryMetadata(signedRegistry, publicKeyPem) {
|
|
126
|
+
const { _metadata, ...entries } = signedRegistry;
|
|
127
|
+
if (!_metadata?.registrySignature) {
|
|
128
|
+
throw new Error("Registry missing _metadata.registrySignature");
|
|
129
|
+
}
|
|
130
|
+
if (!verifyPayload(entries, _metadata.registrySignature, publicKeyPem)) {
|
|
131
|
+
throw new Error("Registry signature verification failed");
|
|
132
|
+
}
|
|
133
|
+
if (_metadata.hash) {
|
|
134
|
+
const computed = computeHash(stableStringify(entries));
|
|
135
|
+
if (!timingSafeStringEqual(computed, _metadata.hash)) {
|
|
136
|
+
throw new Error("Registry hash mismatch");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return entries;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// src/key-format.ts
|
|
143
|
+
function licenseKeyPattern(prefix) {
|
|
144
|
+
if (!/^[A-Z0-9]+$/.test(prefix)) {
|
|
145
|
+
throw new Error(`Prefix must be uppercase alphanumeric: ${prefix}`);
|
|
146
|
+
}
|
|
147
|
+
return new RegExp(`^${prefix}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$`);
|
|
148
|
+
}
|
|
149
|
+
function isValidLicenseKey(key, prefix) {
|
|
150
|
+
return licenseKeyPattern(prefix).test(key.trim().toUpperCase());
|
|
151
|
+
}
|
|
152
|
+
function normalizeLicenseKey(key) {
|
|
153
|
+
return key.trim().toUpperCase();
|
|
154
|
+
}
|
|
155
|
+
export {
|
|
156
|
+
buildLicensePayload,
|
|
157
|
+
buildSignedRegistry,
|
|
158
|
+
computeHash,
|
|
159
|
+
hashEmail,
|
|
160
|
+
isValidLicenseKey,
|
|
161
|
+
licenseKeyPattern,
|
|
162
|
+
normalizeEmail,
|
|
163
|
+
normalizeLicenseKey,
|
|
164
|
+
signPayload,
|
|
165
|
+
stableStringify,
|
|
166
|
+
timingSafeStringEqual,
|
|
167
|
+
validateRegistryEntry,
|
|
168
|
+
verifyPayload,
|
|
169
|
+
verifyRegistryMetadata
|
|
170
|
+
};
|
|
171
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@buildproven/license-core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Shared license signing & verification primitives for BuildProven products (RSA-SHA256, signed registry).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"test:watch": "vitest",
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"format": "prettier --write .",
|
|
27
|
+
"format:check": "prettier --check .",
|
|
28
|
+
"prepublishOnly": "npm run build && npm test"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^22.0.0",
|
|
32
|
+
"prettier": "^3.0.0",
|
|
33
|
+
"tsup": "^8.0.0",
|
|
34
|
+
"typescript": "^5.4.0",
|
|
35
|
+
"vitest": "^4.0.0"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "https://github.com/buildproven/buildproven-license-core.git"
|
|
46
|
+
},
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"keywords": [
|
|
49
|
+
"license",
|
|
50
|
+
"licensing",
|
|
51
|
+
"rsa",
|
|
52
|
+
"rsa-sha256",
|
|
53
|
+
"signature",
|
|
54
|
+
"registry",
|
|
55
|
+
"stripe",
|
|
56
|
+
"saas"
|
|
57
|
+
],
|
|
58
|
+
"private": false
|
|
59
|
+
}
|