Initial public commit
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
/**
|
||||
* Cross-check the TypeScript SDK against the canonical wire-format
|
||||
* test vectors at ./vector.json (mirrored from
|
||||
* <repo>/tests/crosscheck/vector.json).
|
||||
*
|
||||
* These are the same vectors exercised by the Python and Rust SDKs.
|
||||
* Any new SDK must pass every fixture in vector.json — that's how we
|
||||
* guarantee wire-format compatibility across languages.
|
||||
*
|
||||
* Run with: `npm test`.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll } from 'vitest'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import {
|
||||
Verifier,
|
||||
PublicKey,
|
||||
parseLicenseKey,
|
||||
isExpiredAt,
|
||||
hasEntitlement,
|
||||
hashFingerprint,
|
||||
LicensingError,
|
||||
KEY_VERSION_V1,
|
||||
KEY_VERSION_V2,
|
||||
} from '../src/index.js'
|
||||
|
||||
interface Fixture {
|
||||
licenseKey: string
|
||||
expected: {
|
||||
version: number
|
||||
productUuid: string
|
||||
licenseUuid: string
|
||||
issuedAt: number
|
||||
expiresAt: number
|
||||
flags: number
|
||||
isFingerprintBound: boolean
|
||||
isTrial: boolean
|
||||
entitlements: string[]
|
||||
fingerprintRaw: string | null
|
||||
fingerprintHashHex: string
|
||||
}
|
||||
}
|
||||
|
||||
interface Vectors {
|
||||
publicKeyPem: string
|
||||
v1: Fixture
|
||||
v2: Fixture
|
||||
v2_perpetual_unbound?: Fixture
|
||||
}
|
||||
|
||||
const vectors: Vectors = JSON.parse(
|
||||
readFileSync(join(__dirname, 'vector.json'), 'utf8'),
|
||||
)
|
||||
|
||||
const verifier = new Verifier(PublicKey.fromPem(vectors.publicKeyPem))
|
||||
|
||||
function bytesToHex(b: Uint8Array): string {
|
||||
return Array.from(b)
|
||||
.map((x) => x.toString(16).padStart(2, '0'))
|
||||
.join('')
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// v1 fixture: legacy fixed-74 layout, fingerprint-bound, no expiry,
|
||||
// no entitlements.
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
describe('v1 fixture', () => {
|
||||
it('parses', () => {
|
||||
const parsed = parseLicenseKey(vectors.v1.licenseKey)
|
||||
const exp = vectors.v1.expected
|
||||
expect(parsed.payload.version).toBe(exp.version)
|
||||
expect(parsed.payload.version).toBe(KEY_VERSION_V1)
|
||||
expect(parsed.payload.productUuid).toBe(exp.productUuid)
|
||||
expect(parsed.payload.licenseUuid).toBe(exp.licenseUuid)
|
||||
expect(parsed.payload.issuedAt).toBe(exp.issuedAt)
|
||||
expect(parsed.payload.expiresAt).toBe(exp.expiresAt)
|
||||
expect(parsed.payload.flags).toBe(exp.flags)
|
||||
expect(parsed.payload.isFingerprintBound).toBe(exp.isFingerprintBound)
|
||||
expect(parsed.payload.isTrial).toBe(exp.isTrial)
|
||||
expect(parsed.payload.entitlements).toEqual(exp.entitlements)
|
||||
expect(bytesToHex(parsed.payload.fingerprintHash)).toBe(
|
||||
exp.fingerprintHashHex,
|
||||
)
|
||||
})
|
||||
|
||||
it('verifies signature', () => {
|
||||
const ok = verifier.verify(vectors.v1.licenseKey)
|
||||
expect(ok.productId).toBe(vectors.v1.expected.productUuid)
|
||||
expect(ok.licenseId).toBe(vectors.v1.expected.licenseUuid)
|
||||
})
|
||||
|
||||
it('detects tampering', () => {
|
||||
const key = vectors.v1.licenseKey
|
||||
const dashIdx = key.indexOf('-')
|
||||
expect(dashIdx).toBeGreaterThan(0)
|
||||
const payloadStart = dashIdx + 1
|
||||
const swapChar = key[payloadStart] !== 'B' ? 'B' : 'C'
|
||||
const tampered =
|
||||
key.substring(0, payloadStart) +
|
||||
swapChar +
|
||||
key.substring(payloadStart + 1)
|
||||
expect(() => verifier.verify(tampered)).toThrow(LicensingError)
|
||||
})
|
||||
})
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// v2 fixture: trial, fingerprint-bound, explicit expiry, two entitlements.
|
||||
// Stresses the variable-length tail parser.
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
describe('v2 fixture', () => {
|
||||
it('parses', () => {
|
||||
const parsed = parseLicenseKey(vectors.v2.licenseKey)
|
||||
const exp = vectors.v2.expected
|
||||
expect(parsed.payload.version).toBe(exp.version)
|
||||
expect(parsed.payload.version).toBe(KEY_VERSION_V2)
|
||||
expect(parsed.payload.productUuid).toBe(exp.productUuid)
|
||||
expect(parsed.payload.licenseUuid).toBe(exp.licenseUuid)
|
||||
expect(parsed.payload.issuedAt).toBe(exp.issuedAt)
|
||||
expect(parsed.payload.expiresAt).toBe(exp.expiresAt)
|
||||
expect(parsed.payload.flags).toBe(exp.flags)
|
||||
expect(parsed.payload.isFingerprintBound).toBe(exp.isFingerprintBound)
|
||||
expect(parsed.payload.isTrial).toBe(exp.isTrial)
|
||||
expect(parsed.payload.entitlements).toEqual(exp.entitlements)
|
||||
})
|
||||
|
||||
it('verifies signature', () => {
|
||||
const ok = verifier.verify(vectors.v2.licenseKey)
|
||||
expect(ok.productId).toBe(vectors.v2.expected.productUuid)
|
||||
})
|
||||
|
||||
it('expiry boundary is exact', () => {
|
||||
const parsed = parseLicenseKey(vectors.v2.licenseKey)
|
||||
const expiresAt = parsed.payload.expiresAt
|
||||
expect(isExpiredAt(parsed.payload, expiresAt)).toBe(true)
|
||||
expect(isExpiredAt(parsed.payload, expiresAt - 1)).toBe(false)
|
||||
})
|
||||
|
||||
it('reports configured entitlements', () => {
|
||||
const parsed = parseLicenseKey(vectors.v2.licenseKey)
|
||||
for (const slug of vectors.v2.expected.entitlements) {
|
||||
expect(hasEntitlement(parsed.payload, slug)).toBe(true)
|
||||
}
|
||||
expect(
|
||||
hasEntitlement(parsed.payload, 'definitely-not-a-real-entitlement'),
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// v2_perpetual_unbound — common case for paid purchase: v2, no expiry,
|
||||
// no fingerprint binding, no entitlements.
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
describe('v2_perpetual_unbound fixture', () => {
|
||||
it('parses if present', () => {
|
||||
if (!vectors.v2_perpetual_unbound) return
|
||||
const parsed = parseLicenseKey(vectors.v2_perpetual_unbound.licenseKey)
|
||||
expect(parsed.payload.version).toBe(2)
|
||||
expect(parsed.payload.expiresAt).toBe(0)
|
||||
expect(parsed.payload.isFingerprintBound).toBe(false)
|
||||
})
|
||||
|
||||
it('verifies if present', () => {
|
||||
if (!vectors.v2_perpetual_unbound) return
|
||||
const ok = verifier.verify(vectors.v2_perpetual_unbound.licenseKey)
|
||||
expect(ok.productId).toBe(
|
||||
vectors.v2_perpetual_unbound.expected.productUuid,
|
||||
)
|
||||
})
|
||||
|
||||
it('never reports expired (perpetual)', () => {
|
||||
if (!vectors.v2_perpetual_unbound) return
|
||||
const parsed = parseLicenseKey(vectors.v2_perpetual_unbound.licenseKey)
|
||||
expect(isExpiredAt(parsed.payload, 9_999_999_999)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Cross-language fingerprint-hash compatibility.
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
describe('hashFingerprint', () => {
|
||||
it('matches Python stdlib SHA-256 hex output', () => {
|
||||
const hashed = bytesToHex(hashFingerprint('hello'))
|
||||
// Python: hashlib.sha256(b"hello").hexdigest()
|
||||
expect(hashed).toBe(
|
||||
'2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824',
|
||||
)
|
||||
})
|
||||
|
||||
it('matches the v1 fixture fingerprint hash', () => {
|
||||
const raw = vectors.v1.expected.fingerprintRaw
|
||||
if (raw == null) return
|
||||
const hashed = bytesToHex(hashFingerprint(raw))
|
||||
expect(hashed).toBe(vectors.v1.expected.fingerprintHashHex)
|
||||
})
|
||||
})
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Negative cases.
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
describe('negative cases', () => {
|
||||
it('rejects a too-short key', () => {
|
||||
expect(() => parseLicenseKey('notakey')).toThrow(LicensingError)
|
||||
})
|
||||
|
||||
it('rejects a wrong prefix', () => {
|
||||
expect(() => parseLicenseKey('LIC9-AAAA-BBBB')).toThrow(LicensingError)
|
||||
})
|
||||
|
||||
it('rejects an empty string', () => {
|
||||
expect(() => parseLicenseKey('')).toThrow(LicensingError)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAA6EHv/POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg=\n-----END PUBLIC KEY-----\n",
|
||||
"v1": {
|
||||
"licenseKey": "LIC1-AEAW6RVE6YGS6SRIW2VD5D57N4UPAEIRCEISEIRTGNCEIVKVKVKVKVIAAAAAAZKT6EANGRDB75RRODPGGO22UJISZYXBLLASBMODEWWNVWTHYAXFSS5DWPI-FV56FI7ZTB5GIFQHIPQ35QVVE5AO5FQGVQS45UJ5F632MLXS7VHMHYVLZWGE64FJOEXD2PVIFNE5XGRMTNOUOVEKDTW736743W25MAY",
|
||||
"expected": {
|
||||
"version": 1,
|
||||
"productUuid": "6f46a4f6-0d2f-4a28-b6aa-3e8fbf6f28f0",
|
||||
"licenseUuid": "11111111-2222-3333-4444-555555555555",
|
||||
"issuedAt": 1700000000,
|
||||
"expiresAt": 0,
|
||||
"flags": 1,
|
||||
"isFingerprintBound": true,
|
||||
"isTrial": false,
|
||||
"entitlements": [],
|
||||
"fingerprintRaw": "test-machine-fingerprint",
|
||||
"fingerprintHashHex": "d34461ff63170de633b5aa2512ce2e15ac120b1c325acdada67c02e594ba3b3d"
|
||||
}
|
||||
},
|
||||
"v2": {
|
||||
"licenseKey": "LIC1-AIBW6RVE6YGS6SRIW2VD5D57N4UPBKVKVKVLXO6MZTO533XO53XO53QAAAAAAZKT6EAAAAAAABYT7MYA2NCGD73DC4G6MM5VVISRFTROCWWBECY4GJNM3LNGPQBOLFF2HM6QEA3QOJXQY3LVNR2GSLLEMV3GSY3F-QPSJIDYL6Y5TFCKXQ2SN43EDJIZIRJZCEROM2I4MJHODT6KO4KDPW6AJ3HMYJERYPD34CF2Z46PXPYFKSRZS7BDZKVKWE57UBJSTEBI",
|
||||
"expected": {
|
||||
"version": 2,
|
||||
"productUuid": "6f46a4f6-0d2f-4a28-b6aa-3e8fbf6f28f0",
|
||||
"licenseUuid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
|
||||
"issuedAt": 1700000000,
|
||||
"expiresAt": 1900000000,
|
||||
"flags": 3,
|
||||
"isFingerprintBound": true,
|
||||
"isTrial": true,
|
||||
"entitlements": [
|
||||
"pro",
|
||||
"multi-device"
|
||||
],
|
||||
"fingerprintRaw": "test-machine-fingerprint",
|
||||
"fingerprintHashHex": "d34461ff63170de633b5aa2512ce2e15ac120b1c325acdada67c02e594ba3b3d"
|
||||
}
|
||||
},
|
||||
"v2_perpetual_unbound": {
|
||||
"licenseKey": "LIC1-AIAG6RVE6YGS6SRIW2VD5D57N4UPBKVKVKVLXO6MZTO533XO53XO53QAAAAAAZKT6EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA-E4IT25ES5NBBQAXVAMZPLBDB5P2ILZL4RGKYUWEWLME5ZVGM7HGBG5CP3XHWBQ5FCYPEC6YGKBHCTQ7M7RZP7OR7NAYAMNAJAWW4QDQ",
|
||||
"expected": {
|
||||
"version": 2,
|
||||
"productUuid": "6f46a4f6-0d2f-4a28-b6aa-3e8fbf6f28f0",
|
||||
"licenseUuid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
|
||||
"issuedAt": 1700000000,
|
||||
"expiresAt": 0,
|
||||
"flags": 0,
|
||||
"isFingerprintBound": false,
|
||||
"isTrial": false,
|
||||
"entitlements": [],
|
||||
"fingerprintRaw": null,
|
||||
"fingerprintHashHex": "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user