@carchalabs/licensekit-core 0.0.0-stage → 0.1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bugra Omer Aydemir
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 CHANGED
@@ -1,3 +1,20 @@
1
- # Temporary Holding Version
1
+ # @carchalabs/licensekit-core
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The lease format used by [LicenseKit](https://licensekit.carchalabs.com). A lease is a compact JSON Web Signature (RFC 7515) signed with Ed25519 (RFC 9864), with the header `typ: licensekit-lease+jwt` and a `kid` that is the RFC 7638 thumbprint of the signing key.
4
+
5
+ Apps normally use [`@carchalabs/licensekit-electron`](https://www.npmjs.com/package/@carchalabs/licensekit-electron), which uses this package. Use it directly to sign leases on a server or to verify them in another runtime.
6
+
7
+ ```js
8
+ const { generateSigningKeys, signLease, verifyLease, keySet } = require('@carchalabs/licensekit-core');
9
+
10
+ const { privateKeyPem, publicKeyPem } = generateSigningKeys();
11
+ const token = signLease(claims, privateKeyPem);
12
+ const result = verifyLease(token, { keys: keySet([publicKeyPem]), product: 'my-app', fingerprintHash, now: Math.floor(Date.now() / 1000) });
13
+ if (!result.ok) console.log(result.reason);
14
+ ```
15
+
16
+ `verifyLease` accepts only Ed25519, selects the key by `kid`, and checks issuer, product, device, `nbf`, `exp` and the check-in deadline. Failure reasons are listed in the [documentation](https://licensekit.carchalabs.com/docs/#lease).
17
+
18
+ Other exports: `keyId`, `publicJwk`, `publicKeyFromPrivate`, `decodeLease`, `hashFingerprint`, `generateLicenseKey`, `normalizeLicenseKey`, `hashLicenseKey`.
19
+
20
+ Requires Node 22.13 or later. MIT License.
@@ -0,0 +1,72 @@
1
+ import { KeyObject } from 'node:crypto';
2
+ export declare const LEASE_ALGORITHM = "Ed25519";
3
+ export declare const LEASE_TYPE = "licensekit-lease+jwt";
4
+ export declare const LEASE_ISSUER = "licensekit";
5
+ export declare const DEFAULT_LEEWAY_SECONDS = 300;
6
+ export interface LeaseHeader {
7
+ alg: string;
8
+ typ: string;
9
+ kid: string;
10
+ }
11
+ export interface LeaseClaims {
12
+ iss: string;
13
+ sub: string;
14
+ aud: string;
15
+ jti: string;
16
+ iat: number;
17
+ nbf: number;
18
+ exp?: number;
19
+ rev: number;
20
+ fp: string;
21
+ feat: string[];
22
+ cus?: string;
23
+ grc?: number;
24
+ mnt?: number;
25
+ }
26
+ export interface PublicJwk {
27
+ kty: 'OKP';
28
+ crv: 'Ed25519';
29
+ x: string;
30
+ kid: string;
31
+ alg: typeof LEASE_ALGORITHM;
32
+ use: 'sig';
33
+ }
34
+ export type LeaseFailure = 'malformed' | 'unsupported_algorithm' | 'wrong_type' | 'unknown_key' | 'bad_signature' | 'wrong_issuer' | 'wrong_product' | 'wrong_device' | 'not_yet_valid' | 'expired' | 'revalidation_required';
35
+ export type LeaseVerifyResult = {
36
+ ok: true;
37
+ header: LeaseHeader;
38
+ claims: LeaseClaims;
39
+ } | {
40
+ ok: false;
41
+ reason: LeaseFailure;
42
+ claims?: LeaseClaims;
43
+ };
44
+ export type KeyInput = string | KeyObject;
45
+ export interface LeaseVerifyOptions {
46
+ keys: KeyInput[] | Map<string, KeyObject>;
47
+ product: string;
48
+ fingerprintHash: string;
49
+ issuer?: string;
50
+ now?: number;
51
+ graceSeconds?: number;
52
+ leewaySeconds?: number;
53
+ }
54
+ export declare function generateSigningKeys(): {
55
+ privateKeyPem: string;
56
+ publicKeyPem: string;
57
+ };
58
+ export declare function publicKeyFromPrivate(privateKeyPem: string): string;
59
+ export declare function keyId(key: KeyInput): string;
60
+ export declare function publicJwk(key: KeyInput): PublicJwk;
61
+ export declare function keySet(keys: KeyInput[]): Map<string, KeyObject>;
62
+ export declare function signLease(claims: LeaseClaims, privateKey: KeyInput): string;
63
+ export declare function decodeLease(token: string): {
64
+ header: LeaseHeader;
65
+ claims: LeaseClaims;
66
+ } | null;
67
+ export declare function verifyLease(token: string, options: LeaseVerifyOptions): LeaseVerifyResult;
68
+ export declare function hashFingerprint(rawFingerprint: string, product: string): string;
69
+ export declare function generateLicenseKey(): string;
70
+ export declare function normalizeLicenseKey(input: string): string;
71
+ export declare function hashLicenseKey(input: string): string;
72
+ export declare function legacyLicenseKeyHash(input: string): string;
package/dist/index.js ADDED
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_LEEWAY_SECONDS = exports.LEASE_ISSUER = exports.LEASE_TYPE = exports.LEASE_ALGORITHM = void 0;
4
+ exports.generateSigningKeys = generateSigningKeys;
5
+ exports.publicKeyFromPrivate = publicKeyFromPrivate;
6
+ exports.keyId = keyId;
7
+ exports.publicJwk = publicJwk;
8
+ exports.keySet = keySet;
9
+ exports.signLease = signLease;
10
+ exports.decodeLease = decodeLease;
11
+ exports.verifyLease = verifyLease;
12
+ exports.hashFingerprint = hashFingerprint;
13
+ exports.generateLicenseKey = generateLicenseKey;
14
+ exports.normalizeLicenseKey = normalizeLicenseKey;
15
+ exports.hashLicenseKey = hashLicenseKey;
16
+ exports.legacyLicenseKeyHash = legacyLicenseKeyHash;
17
+ const node_crypto_1 = require("node:crypto");
18
+ exports.LEASE_ALGORITHM = 'Ed25519';
19
+ exports.LEASE_TYPE = 'licensekit-lease+jwt';
20
+ exports.LEASE_ISSUER = 'licensekit';
21
+ exports.DEFAULT_LEEWAY_SECONDS = 300;
22
+ const toBase64Url = (buf) => buf.toString('base64url');
23
+ const fromBase64Url = (text) => Buffer.from(text, 'base64url');
24
+ const BASE64URL = /^[A-Za-z0-9_-]+$/;
25
+ function asPrivateKey(key) {
26
+ return typeof key === 'string' ? (0, node_crypto_1.createPrivateKey)(key) : key;
27
+ }
28
+ function asPublicKey(key) {
29
+ if (typeof key === 'string')
30
+ return (0, node_crypto_1.createPublicKey)(key);
31
+ return key.type === 'private' ? (0, node_crypto_1.createPublicKey)(key) : key;
32
+ }
33
+ function generateSigningKeys() {
34
+ const { privateKey, publicKey } = (0, node_crypto_1.generateKeyPairSync)('ed25519');
35
+ return {
36
+ privateKeyPem: privateKey.export({ type: 'pkcs8', format: 'pem' }).toString(),
37
+ publicKeyPem: publicKey.export({ type: 'spki', format: 'pem' }).toString(),
38
+ };
39
+ }
40
+ function publicKeyFromPrivate(privateKeyPem) {
41
+ return (0, node_crypto_1.createPublicKey)((0, node_crypto_1.createPrivateKey)(privateKeyPem)).export({ type: 'spki', format: 'pem' }).toString();
42
+ }
43
+ function ed25519PublicX(key) {
44
+ const jwk = asPublicKey(key).export({ format: 'jwk' });
45
+ if (jwk.kty !== 'OKP' || jwk.crv !== 'Ed25519' || typeof jwk.x !== 'string') {
46
+ throw new Error('Only Ed25519 keys are supported');
47
+ }
48
+ return jwk.x;
49
+ }
50
+ function keyId(key) {
51
+ const canonical = JSON.stringify({ crv: 'Ed25519', kty: 'OKP', x: ed25519PublicX(key) });
52
+ return toBase64Url((0, node_crypto_1.createHash)('sha256').update(canonical, 'utf8').digest());
53
+ }
54
+ function publicJwk(key) {
55
+ return { kty: 'OKP', crv: 'Ed25519', x: ed25519PublicX(key), kid: keyId(key), alg: exports.LEASE_ALGORITHM, use: 'sig' };
56
+ }
57
+ function keySet(keys) {
58
+ const set = new Map();
59
+ for (const key of keys) {
60
+ const publicKey = asPublicKey(key);
61
+ set.set(keyId(publicKey), publicKey);
62
+ }
63
+ return set;
64
+ }
65
+ function signLease(claims, privateKey) {
66
+ const key = asPrivateKey(privateKey);
67
+ const header = { alg: exports.LEASE_ALGORITHM, typ: exports.LEASE_TYPE, kid: keyId(key) };
68
+ const signingInput = `${toBase64Url(Buffer.from(JSON.stringify(header), 'utf8'))}.${toBase64Url(Buffer.from(JSON.stringify(claims), 'utf8'))}`;
69
+ const signature = (0, node_crypto_1.sign)(null, Buffer.from(signingInput, 'ascii'), key);
70
+ return `${signingInput}.${toBase64Url(signature)}`;
71
+ }
72
+ function parseSegment(segment) {
73
+ if (!BASE64URL.test(segment))
74
+ return null;
75
+ try {
76
+ return JSON.parse(fromBase64Url(segment).toString('utf8'));
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ }
82
+ function isObject(value) {
83
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
84
+ }
85
+ function isLeaseClaims(value) {
86
+ if (!isObject(value))
87
+ return false;
88
+ const strings = ['iss', 'sub', 'aud', 'jti', 'fp'].every((name) => typeof value[name] === 'string');
89
+ const numbers = ['iat', 'nbf', 'rev'].every((name) => Number.isFinite(value[name]));
90
+ const expiry = value.exp === undefined || Number.isFinite(value.exp);
91
+ const features = Array.isArray(value.feat) && value.feat.every((feature) => typeof feature === 'string');
92
+ const customer = value.cus === undefined || typeof value.cus === 'string';
93
+ const optionalTimes = ['grc', 'mnt'].every((name) => value[name] === undefined || (Number.isFinite(value[name]) && value[name] >= 0));
94
+ return strings && numbers && expiry && features && customer && optionalTimes;
95
+ }
96
+ function decodeLease(token) {
97
+ const parts = token.split('.');
98
+ if (parts.length !== 3)
99
+ return null;
100
+ const header = parseSegment(parts[0]);
101
+ const claims = parseSegment(parts[1]);
102
+ if (!isObject(header) || !isLeaseClaims(claims))
103
+ return null;
104
+ return { header: header, claims };
105
+ }
106
+ function verifyLease(token, options) {
107
+ const parts = token.split('.');
108
+ if (parts.length !== 3 || parts.some((part) => part.length === 0))
109
+ return { ok: false, reason: 'malformed' };
110
+ const [encodedHeader, encodedClaims, encodedSignature] = parts;
111
+ const header = parseSegment(encodedHeader);
112
+ if (!isObject(header))
113
+ return { ok: false, reason: 'malformed' };
114
+ if (header.alg !== exports.LEASE_ALGORITHM)
115
+ return { ok: false, reason: 'unsupported_algorithm' };
116
+ if (header.typ !== exports.LEASE_TYPE)
117
+ return { ok: false, reason: 'wrong_type' };
118
+ const keys = options.keys instanceof Map ? options.keys : keySet(options.keys);
119
+ const key = typeof header.kid === 'string' ? keys.get(header.kid) : undefined;
120
+ if (!key)
121
+ return { ok: false, reason: 'unknown_key' };
122
+ if (!BASE64URL.test(encodedSignature))
123
+ return { ok: false, reason: 'malformed' };
124
+ let signatureValid = false;
125
+ try {
126
+ signatureValid = (0, node_crypto_1.verify)(null, Buffer.from(`${encodedHeader}.${encodedClaims}`, 'ascii'), key, fromBase64Url(encodedSignature));
127
+ }
128
+ catch {
129
+ signatureValid = false;
130
+ }
131
+ if (!signatureValid)
132
+ return { ok: false, reason: 'bad_signature' };
133
+ const claims = parseSegment(encodedClaims);
134
+ if (!isLeaseClaims(claims))
135
+ return { ok: false, reason: 'malformed' };
136
+ if (claims.iss !== (options.issuer ?? exports.LEASE_ISSUER))
137
+ return { ok: false, reason: 'wrong_issuer', claims };
138
+ if (claims.aud !== options.product)
139
+ return { ok: false, reason: 'wrong_product', claims };
140
+ if (claims.fp !== options.fingerprintHash)
141
+ return { ok: false, reason: 'wrong_device', claims };
142
+ const now = options.now ?? Math.floor(Date.now() / 1000);
143
+ if (now + (options.leewaySeconds ?? exports.DEFAULT_LEEWAY_SECONDS) < claims.nbf)
144
+ return { ok: false, reason: 'not_yet_valid', claims };
145
+ if (claims.exp !== undefined && now >= claims.exp)
146
+ return { ok: false, reason: 'expired', claims };
147
+ if (now >= claims.rev + (options.graceSeconds ?? 0))
148
+ return { ok: false, reason: 'revalidation_required', claims };
149
+ return { ok: true, header: header, claims };
150
+ }
151
+ function hashFingerprint(rawFingerprint, product) {
152
+ return (0, node_crypto_1.createHash)('sha256').update(`licensekit:${product}:${rawFingerprint}`).digest('hex');
153
+ }
154
+ const CROCKFORD_ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
155
+ const LICENSE_KEY_PREFIX = 'LK';
156
+ function generateLicenseKey() {
157
+ const bytes = (0, node_crypto_1.randomBytes)(16);
158
+ const body = Array.from(bytes, (byte) => CROCKFORD_ALPHABET[byte & 31]).join('');
159
+ return `${LICENSE_KEY_PREFIX}-${body.match(/.{4}/g).join('-')}`;
160
+ }
161
+ function normalizeLicenseKey(input) {
162
+ let body = input.toUpperCase().replace(/[\s-]/g, '');
163
+ if (body.startsWith(LICENSE_KEY_PREFIX))
164
+ body = body.slice(LICENSE_KEY_PREFIX.length);
165
+ body = body.replace(/[IL]/g, '1').replace(/O/g, '0');
166
+ const groups = body.match(/.{1,4}/g) ?? [];
167
+ return [LICENSE_KEY_PREFIX, ...groups].join('-');
168
+ }
169
+ function hashLicenseKey(input) {
170
+ return (0, node_crypto_1.createHash)('sha256').update(normalizeLicenseKey(input)).digest('hex');
171
+ }
172
+ function legacyLicenseKeyHash(input) {
173
+ return (0, node_crypto_1.createHash)('sha256').update(input.trim().toUpperCase()).digest('hex');
174
+ }
175
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAyFA,kDAMC;AAED,oDAEC;AAUD,sBAGC;AAED,8BAEC;AAED,wBAOC;AAED,8BAMC;AA0BD,kCAOC;AAED,kCAoCC;AAED,0CAEC;AAKD,gDAIC;AAED,kDAMC;AAED,wCAEC;AAED,oDAEC;AAzOD,6CASqB;AAER,QAAA,eAAe,GAAG,SAAS,CAAC;AAC5B,QAAA,UAAU,GAAG,sBAAsB,CAAC;AACpC,QAAA,YAAY,GAAG,YAAY,CAAC;AAC5B,QAAA,sBAAsB,GAAG,GAAG,CAAC;AA8D1C,MAAM,WAAW,GAAG,CAAC,GAAW,EAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAC/D,MAAM,aAAa,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;AACvE,MAAM,SAAS,GAAG,kBAAkB,CAAC;AAErC,SAAS,YAAY,CAAC,GAAa;IACjC,OAAO,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAA,8BAAgB,EAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;AAC/D,CAAC;AAED,SAAS,WAAW,CAAC,GAAa;IAChC,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,IAAA,6BAAe,EAAC,GAAG,CAAC,CAAC;IACzD,OAAO,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,IAAA,6BAAe,EAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;AAC7D,CAAC;AAED,SAAgB,mBAAmB;IACjC,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,GAAG,IAAA,iCAAmB,EAAC,SAAS,CAAC,CAAC;IACjE,OAAO;QACL,aAAa,EAAE,UAAU,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,QAAQ,EAAE;QAC7E,YAAY,EAAE,SAAS,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,QAAQ,EAAE;KAC3E,CAAC;AACJ,CAAC;AAED,SAAgB,oBAAoB,CAAC,aAAqB;IACxD,OAAO,IAAA,6BAAe,EAAC,IAAA,8BAAgB,EAAC,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC;AAC7G,CAAC;AAED,SAAS,cAAc,CAAC,GAAa;IACnC,MAAM,GAAG,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;IACvD,IAAI,GAAG,CAAC,GAAG,KAAK,KAAK,IAAI,GAAG,CAAC,GAAG,KAAK,SAAS,IAAI,OAAO,GAAG,CAAC,CAAC,KAAK,QAAQ,EAAE,CAAC;QAC5E,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAC;IACrD,CAAC;IACD,OAAO,GAAG,CAAC,CAAC,CAAC;AACf,CAAC;AAED,SAAgB,KAAK,CAAC,GAAa;IACjC,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,cAAc,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACzF,OAAO,WAAW,CAAC,IAAA,wBAAU,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;AAC9E,CAAC;AAED,SAAgB,SAAS,CAAC,GAAa;IACrC,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC,EAAE,cAAc,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,uBAAe,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC;AACnH,CAAC;AAED,SAAgB,MAAM,CAAC,IAAgB;IACrC,MAAM,GAAG,GAAG,IAAI,GAAG,EAAqB,CAAC;IACzC,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,SAAS,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;QACnC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,CAAC;IACvC,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAgB,SAAS,CAAC,MAAmB,EAAE,UAAoB;IACjE,MAAM,GAAG,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC;IACrC,MAAM,MAAM,GAAgB,EAAE,GAAG,EAAE,uBAAe,EAAE,GAAG,EAAE,kBAAU,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;IACvF,MAAM,YAAY,GAAG,GAAG,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,IAAI,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC;IAC/I,MAAM,SAAS,GAAG,IAAA,kBAAI,EAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,YAAY,EAAE,OAAO,CAAC,EAAE,GAAG,CAAC,CAAC;IACtE,OAAO,GAAG,YAAY,IAAI,WAAW,CAAC,SAAS,CAAC,EAAE,CAAC;AACrD,CAAC;AAED,SAAS,YAAY,CAAC,OAAe;IACnC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1C,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;IAC7D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED,SAAS,aAAa,CAAC,KAAc;IACnC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACnC,MAAM,OAAO,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,QAAQ,CAAC,CAAC;IACpG,MAAM,OAAO,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpF,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,KAAK,SAAS,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACrE,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC;IACzG,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,KAAK,SAAS,IAAI,OAAO,KAAK,CAAC,GAAG,KAAK,QAAQ,CAAC;IAC1E,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAK,KAAK,CAAC,IAAI,CAAY,IAAI,CAAC,CAAC,CAAC,CAAC;IAClJ,OAAO,OAAO,IAAI,OAAO,IAAI,MAAM,IAAI,QAAQ,IAAI,QAAQ,IAAI,aAAa,CAAC;AAC/E,CAAC;AAED,SAAgB,WAAW,CAAC,KAAa;IACvC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC/B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACpC,MAAM,MAAM,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACtC,MAAM,MAAM,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACtC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IAC7D,OAAO,EAAE,MAAM,EAAE,MAAgC,EAAE,MAAM,EAAE,CAAC;AAC9D,CAAC;AAED,SAAgB,WAAW,CAAC,KAAa,EAAE,OAA2B;IACpE,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC/B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;IAC7G,MAAM,CAAC,aAAa,EAAE,aAAa,EAAE,gBAAgB,CAAC,GAAG,KAAK,CAAC;IAE/D,MAAM,MAAM,GAAG,YAAY,CAAC,aAAa,CAAC,CAAC;IAC3C,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;IACjE,IAAI,MAAM,CAAC,GAAG,KAAK,uBAAe;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,uBAAuB,EAAE,CAAC;IAC1F,IAAI,MAAM,CAAC,GAAG,KAAK,kBAAU;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC;IAE1E,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,YAAY,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/E,MAAM,GAAG,GAAG,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAC9E,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC;IAEtD,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,gBAAgB,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;IACjF,IAAI,cAAc,GAAG,KAAK,CAAC;IAC3B,IAAI,CAAC;QACH,cAAc,GAAG,IAAA,oBAAM,EAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,aAAa,IAAI,aAAa,EAAE,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,aAAa,CAAC,gBAAgB,CAAC,CAAC,CAAC;IACjI,CAAC;IAAC,MAAM,CAAC;QACP,cAAc,GAAG,KAAK,CAAC;IACzB,CAAC;IACD,IAAI,CAAC,cAAc;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC;IAEnE,MAAM,MAAM,GAAG,YAAY,CAAC,aAAa,CAAC,CAAC;IAC3C,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;IAEtE,IAAI,MAAM,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,IAAI,oBAAY,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,CAAC;IAC1G,IAAI,MAAM,CAAC,GAAG,KAAK,OAAO,CAAC,OAAO;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1F,IAAI,MAAM,CAAC,EAAE,KAAK,OAAO,CAAC,eAAe;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,CAAC;IAEhG,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;IACzD,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,IAAI,8BAAsB,CAAC,GAAG,MAAM,CAAC,GAAG;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,EAAE,CAAC;IAChI,IAAI,MAAM,CAAC,GAAG,KAAK,SAAS,IAAI,GAAG,IAAI,MAAM,CAAC,GAAG;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC;IACnG,IAAI,GAAG,IAAI,MAAM,CAAC,GAAG,GAAG,CAAC,OAAO,CAAC,YAAY,IAAI,CAAC,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,uBAAuB,EAAE,MAAM,EAAE,CAAC;IAEnH,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,MAAgC,EAAE,MAAM,EAAE,CAAC;AACxE,CAAC;AAED,SAAgB,eAAe,CAAC,cAAsB,EAAE,OAAe;IACrE,OAAO,IAAA,wBAAU,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,cAAc,OAAO,IAAI,cAAc,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC9F,CAAC;AAED,MAAM,kBAAkB,GAAG,kCAAkC,CAAC;AAC9D,MAAM,kBAAkB,GAAG,IAAI,CAAC;AAEhC,SAAgB,kBAAkB;IAChC,MAAM,KAAK,GAAG,IAAA,yBAAW,EAAC,EAAE,CAAC,CAAC;IAC9B,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,kBAAkB,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACjF,OAAO,GAAG,kBAAkB,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAE,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;AACnE,CAAC;AAED,SAAgB,mBAAmB,CAAC,KAAa;IAC/C,IAAI,IAAI,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IACrD,IAAI,IAAI,CAAC,UAAU,CAAC,kBAAkB,CAAC;QAAE,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC;IACtF,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IACrD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;IAC3C,OAAO,CAAC,kBAAkB,EAAE,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACnD,CAAC;AAED,SAAgB,cAAc,CAAC,KAAa;IAC1C,OAAO,IAAA,wBAAU,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,mBAAmB,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC/E,CAAC;AAED,SAAgB,oBAAoB,CAAC,KAAa;IAChD,OAAO,IAAA,wBAAU,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC/E,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,19 @@
1
1
  {
2
2
  "name": "@carchalabs/licensekit-core",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "LicenseKit lease format: Ed25519-signed compact JWS, key sets and license keys",
5
+ "homepage": "https://licensekit.carchalabs.com/docs/",
6
+ "repository": { "type": "git", "url": "git+https://github.com/carcha-labs/licensekit-sdk.git", "directory": "packages/core" },
7
+ "bugs": { "url": "https://github.com/carcha-labs/licensekit-sdk/issues" },
8
+ "main": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "files": ["dist", "src", "!dist/**/*.test.*", "!src/**/*.test.ts"],
11
+ "scripts": {
12
+ "build": "tsc -p tsconfig.json",
13
+ "prepublishOnly": "npm run build",
14
+ "test": "node --test dist/*.test.js"
15
+ },
16
+ "engines": { "node": ">=22.13" },
17
+ "publishConfig": { "access": "public" },
18
+ "license": "MIT"
19
+ }
package/src/index.ts ADDED
@@ -0,0 +1,234 @@
1
+ import {
2
+ createHash,
3
+ createPrivateKey,
4
+ createPublicKey,
5
+ generateKeyPairSync,
6
+ randomBytes,
7
+ sign,
8
+ verify,
9
+ KeyObject,
10
+ } from 'node:crypto';
11
+
12
+ export const LEASE_ALGORITHM = 'Ed25519';
13
+ export const LEASE_TYPE = 'licensekit-lease+jwt';
14
+ export const LEASE_ISSUER = 'licensekit';
15
+ export const DEFAULT_LEEWAY_SECONDS = 300;
16
+
17
+ export interface LeaseHeader {
18
+ alg: string;
19
+ typ: string;
20
+ kid: string;
21
+ }
22
+
23
+ export interface LeaseClaims {
24
+ iss: string;
25
+ sub: string;
26
+ aud: string;
27
+ jti: string;
28
+ iat: number;
29
+ nbf: number;
30
+ exp?: number;
31
+ rev: number;
32
+ fp: string;
33
+ feat: string[];
34
+ cus?: string;
35
+ grc?: number;
36
+ mnt?: number;
37
+ }
38
+
39
+ export interface PublicJwk {
40
+ kty: 'OKP';
41
+ crv: 'Ed25519';
42
+ x: string;
43
+ kid: string;
44
+ alg: typeof LEASE_ALGORITHM;
45
+ use: 'sig';
46
+ }
47
+
48
+ export type LeaseFailure =
49
+ | 'malformed'
50
+ | 'unsupported_algorithm'
51
+ | 'wrong_type'
52
+ | 'unknown_key'
53
+ | 'bad_signature'
54
+ | 'wrong_issuer'
55
+ | 'wrong_product'
56
+ | 'wrong_device'
57
+ | 'not_yet_valid'
58
+ | 'expired'
59
+ | 'revalidation_required';
60
+
61
+ export type LeaseVerifyResult =
62
+ | { ok: true; header: LeaseHeader; claims: LeaseClaims }
63
+ | { ok: false; reason: LeaseFailure; claims?: LeaseClaims };
64
+
65
+ export type KeyInput = string | KeyObject;
66
+
67
+ export interface LeaseVerifyOptions {
68
+ keys: KeyInput[] | Map<string, KeyObject>;
69
+ product: string;
70
+ fingerprintHash: string;
71
+ issuer?: string;
72
+ now?: number;
73
+ graceSeconds?: number;
74
+ leewaySeconds?: number;
75
+ }
76
+
77
+ const toBase64Url = (buf: Buffer) => buf.toString('base64url');
78
+ const fromBase64Url = (text: string) => Buffer.from(text, 'base64url');
79
+ const BASE64URL = /^[A-Za-z0-9_-]+$/;
80
+
81
+ function asPrivateKey(key: KeyInput): KeyObject {
82
+ return typeof key === 'string' ? createPrivateKey(key) : key;
83
+ }
84
+
85
+ function asPublicKey(key: KeyInput): KeyObject {
86
+ if (typeof key === 'string') return createPublicKey(key);
87
+ return key.type === 'private' ? createPublicKey(key) : key;
88
+ }
89
+
90
+ export function generateSigningKeys(): { privateKeyPem: string; publicKeyPem: string } {
91
+ const { privateKey, publicKey } = generateKeyPairSync('ed25519');
92
+ return {
93
+ privateKeyPem: privateKey.export({ type: 'pkcs8', format: 'pem' }).toString(),
94
+ publicKeyPem: publicKey.export({ type: 'spki', format: 'pem' }).toString(),
95
+ };
96
+ }
97
+
98
+ export function publicKeyFromPrivate(privateKeyPem: string): string {
99
+ return createPublicKey(createPrivateKey(privateKeyPem)).export({ type: 'spki', format: 'pem' }).toString();
100
+ }
101
+
102
+ function ed25519PublicX(key: KeyInput): string {
103
+ const jwk = asPublicKey(key).export({ format: 'jwk' });
104
+ if (jwk.kty !== 'OKP' || jwk.crv !== 'Ed25519' || typeof jwk.x !== 'string') {
105
+ throw new Error('Only Ed25519 keys are supported');
106
+ }
107
+ return jwk.x;
108
+ }
109
+
110
+ export function keyId(key: KeyInput): string {
111
+ const canonical = JSON.stringify({ crv: 'Ed25519', kty: 'OKP', x: ed25519PublicX(key) });
112
+ return toBase64Url(createHash('sha256').update(canonical, 'utf8').digest());
113
+ }
114
+
115
+ export function publicJwk(key: KeyInput): PublicJwk {
116
+ return { kty: 'OKP', crv: 'Ed25519', x: ed25519PublicX(key), kid: keyId(key), alg: LEASE_ALGORITHM, use: 'sig' };
117
+ }
118
+
119
+ export function keySet(keys: KeyInput[]): Map<string, KeyObject> {
120
+ const set = new Map<string, KeyObject>();
121
+ for (const key of keys) {
122
+ const publicKey = asPublicKey(key);
123
+ set.set(keyId(publicKey), publicKey);
124
+ }
125
+ return set;
126
+ }
127
+
128
+ export function signLease(claims: LeaseClaims, privateKey: KeyInput): string {
129
+ const key = asPrivateKey(privateKey);
130
+ const header: LeaseHeader = { alg: LEASE_ALGORITHM, typ: LEASE_TYPE, kid: keyId(key) };
131
+ const signingInput = `${toBase64Url(Buffer.from(JSON.stringify(header), 'utf8'))}.${toBase64Url(Buffer.from(JSON.stringify(claims), 'utf8'))}`;
132
+ const signature = sign(null, Buffer.from(signingInput, 'ascii'), key);
133
+ return `${signingInput}.${toBase64Url(signature)}`;
134
+ }
135
+
136
+ function parseSegment(segment: string): unknown {
137
+ if (!BASE64URL.test(segment)) return null;
138
+ try {
139
+ return JSON.parse(fromBase64Url(segment).toString('utf8'));
140
+ } catch {
141
+ return null;
142
+ }
143
+ }
144
+
145
+ function isObject(value: unknown): value is Record<string, unknown> {
146
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
147
+ }
148
+
149
+ function isLeaseClaims(value: unknown): value is LeaseClaims {
150
+ if (!isObject(value)) return false;
151
+ const strings = ['iss', 'sub', 'aud', 'jti', 'fp'].every((name) => typeof value[name] === 'string');
152
+ const numbers = ['iat', 'nbf', 'rev'].every((name) => Number.isFinite(value[name]));
153
+ const expiry = value.exp === undefined || Number.isFinite(value.exp);
154
+ const features = Array.isArray(value.feat) && value.feat.every((feature) => typeof feature === 'string');
155
+ const customer = value.cus === undefined || typeof value.cus === 'string';
156
+ const optionalTimes = ['grc', 'mnt'].every((name) => value[name] === undefined || (Number.isFinite(value[name]) && (value[name] as number) >= 0));
157
+ return strings && numbers && expiry && features && customer && optionalTimes;
158
+ }
159
+
160
+ export function decodeLease(token: string): { header: LeaseHeader; claims: LeaseClaims } | null {
161
+ const parts = token.split('.');
162
+ if (parts.length !== 3) return null;
163
+ const header = parseSegment(parts[0]);
164
+ const claims = parseSegment(parts[1]);
165
+ if (!isObject(header) || !isLeaseClaims(claims)) return null;
166
+ return { header: header as unknown as LeaseHeader, claims };
167
+ }
168
+
169
+ export function verifyLease(token: string, options: LeaseVerifyOptions): LeaseVerifyResult {
170
+ const parts = token.split('.');
171
+ if (parts.length !== 3 || parts.some((part) => part.length === 0)) return { ok: false, reason: 'malformed' };
172
+ const [encodedHeader, encodedClaims, encodedSignature] = parts;
173
+
174
+ const header = parseSegment(encodedHeader);
175
+ if (!isObject(header)) return { ok: false, reason: 'malformed' };
176
+ if (header.alg !== LEASE_ALGORITHM) return { ok: false, reason: 'unsupported_algorithm' };
177
+ if (header.typ !== LEASE_TYPE) return { ok: false, reason: 'wrong_type' };
178
+
179
+ const keys = options.keys instanceof Map ? options.keys : keySet(options.keys);
180
+ const key = typeof header.kid === 'string' ? keys.get(header.kid) : undefined;
181
+ if (!key) return { ok: false, reason: 'unknown_key' };
182
+
183
+ if (!BASE64URL.test(encodedSignature)) return { ok: false, reason: 'malformed' };
184
+ let signatureValid = false;
185
+ try {
186
+ signatureValid = verify(null, Buffer.from(`${encodedHeader}.${encodedClaims}`, 'ascii'), key, fromBase64Url(encodedSignature));
187
+ } catch {
188
+ signatureValid = false;
189
+ }
190
+ if (!signatureValid) return { ok: false, reason: 'bad_signature' };
191
+
192
+ const claims = parseSegment(encodedClaims);
193
+ if (!isLeaseClaims(claims)) return { ok: false, reason: 'malformed' };
194
+
195
+ if (claims.iss !== (options.issuer ?? LEASE_ISSUER)) return { ok: false, reason: 'wrong_issuer', claims };
196
+ if (claims.aud !== options.product) return { ok: false, reason: 'wrong_product', claims };
197
+ if (claims.fp !== options.fingerprintHash) return { ok: false, reason: 'wrong_device', claims };
198
+
199
+ const now = options.now ?? Math.floor(Date.now() / 1000);
200
+ if (now + (options.leewaySeconds ?? DEFAULT_LEEWAY_SECONDS) < claims.nbf) return { ok: false, reason: 'not_yet_valid', claims };
201
+ if (claims.exp !== undefined && now >= claims.exp) return { ok: false, reason: 'expired', claims };
202
+ if (now >= claims.rev + (options.graceSeconds ?? 0)) return { ok: false, reason: 'revalidation_required', claims };
203
+
204
+ return { ok: true, header: header as unknown as LeaseHeader, claims };
205
+ }
206
+
207
+ export function hashFingerprint(rawFingerprint: string, product: string): string {
208
+ return createHash('sha256').update(`licensekit:${product}:${rawFingerprint}`).digest('hex');
209
+ }
210
+
211
+ const CROCKFORD_ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
212
+ const LICENSE_KEY_PREFIX = 'LK';
213
+
214
+ export function generateLicenseKey(): string {
215
+ const bytes = randomBytes(16);
216
+ const body = Array.from(bytes, (byte) => CROCKFORD_ALPHABET[byte & 31]).join('');
217
+ return `${LICENSE_KEY_PREFIX}-${body.match(/.{4}/g)!.join('-')}`;
218
+ }
219
+
220
+ export function normalizeLicenseKey(input: string): string {
221
+ let body = input.toUpperCase().replace(/[\s-]/g, '');
222
+ if (body.startsWith(LICENSE_KEY_PREFIX)) body = body.slice(LICENSE_KEY_PREFIX.length);
223
+ body = body.replace(/[IL]/g, '1').replace(/O/g, '0');
224
+ const groups = body.match(/.{1,4}/g) ?? [];
225
+ return [LICENSE_KEY_PREFIX, ...groups].join('-');
226
+ }
227
+
228
+ export function hashLicenseKey(input: string): string {
229
+ return createHash('sha256').update(normalizeLicenseKey(input)).digest('hex');
230
+ }
231
+
232
+ export function legacyLicenseKeyHash(input: string): string {
233
+ return createHash('sha256').update(input.trim().toUpperCase()).digest('hex');
234
+ }