import { ed25519 } from '@noble/curves/ed25519'; import { hexToBytes } from '@noble/hashes/utils'; /** * Offline-verifiable license: base64url(payload JSON) + '.' + base64url(ed25519 signature). * The verifying (public) key ships with the app; the signing key never leaves the vendor. * Absence of a license never blocks the software — it only shows an evaluation banner * in the admin (never on customer checkout). */ export interface LicensePayload { licensee: string; issued_at: string; /** Updates entitlement cutoff — the license itself never expires. */ expires_updates_at: string; } export type LicenseCheck = | { valid: true; payload: LicensePayload } | { valid: false; reason: string }; export function verifyLicense(license: string, publicKeyHex: string): LicenseCheck { if (!publicKeyHex) return { valid: false, reason: 'no verifying key embedded in this build' }; const parts = license.trim().split('.'); if (parts.length !== 2) return { valid: false, reason: 'malformed license key' }; try { const payloadBytes = base64UrlDecode(parts[0]!); const signature = base64UrlDecode(parts[1]!); const ok = ed25519.verify(signature, payloadBytes, hexToBytes(publicKeyHex)); if (!ok) return { valid: false, reason: 'signature does not match' }; const payload = JSON.parse(new TextDecoder().decode(payloadBytes)) as LicensePayload; if (!payload.licensee || !payload.issued_at || !payload.expires_updates_at) { return { valid: false, reason: 'license payload incomplete' }; } return { valid: true, payload }; } catch { return { valid: false, reason: 'malformed license key' }; } } export function base64UrlDecode(text: string): Uint8Array { const b64 = text.replace(/-/g, '+').replace(/_/g, '/'); return Uint8Array.from(Buffer.from(b64, 'base64')); } export function base64UrlEncode(bytes: Uint8Array): string { return Buffer.from(bytes).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); }