174 lines
7.6 KiB
JavaScript
174 lines
7.6 KiB
JavaScript
// Generated, do not edit. sha256:0a4af87a128e6b903b5ce63c00bf6f500061dd0feb2e18ca3a3d4791c738eff9
|
|
import { readFileSync } from 'node:fs';
|
|
import { createHash } from 'node:crypto';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { schnorr } from '@noble/curves/secp256k1.js';
|
|
import { CONFIG } from '../shared/config.js';
|
|
import { installDir } from './portfolios.mjs';
|
|
import { services } from './registers.mjs';
|
|
|
|
const L = CONFIG.LICENSE;
|
|
const HEX64 = /^[0-9a-f]{64}$/;
|
|
const HEX128 = /^[0-9a-f]{128}$/;
|
|
const DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
|
|
const FIELDS = ['buyer', 'level', 'issued', 'majorFrom', 'majorTo'];
|
|
|
|
export function licenseMessage(body) {
|
|
const lines = FIELDS.map((f) => `${f}=${body[f]}`);
|
|
return [L.MESSAGE_PREFIX, ...lines].join('\n');
|
|
}
|
|
|
|
function messageHash(body) {
|
|
return createHash('sha256').update(licenseMessage(body), 'utf-8').digest();
|
|
}
|
|
|
|
const ISSUER_FIELDS = ['key', 'validFrom', 'validTo'];
|
|
|
|
export function issuerMessage(issuer) {
|
|
return [L.ISSUER_PREFIX, ...ISSUER_FIELDS.map((f) => `${f}=${issuer[f]}`)].join('\n');
|
|
}
|
|
|
|
const sha256 = (text) => createHash('sha256').update(text, 'utf-8').digest();
|
|
|
|
const schnorrOk = (sig, hash, key) => {
|
|
try {
|
|
return schnorr.verify(Buffer.from(sig, 'hex'), hash, Buffer.from(key, 'hex'));
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
class LicenseError extends Error {}
|
|
|
|
function refuse(reason) {
|
|
throw new LicenseError(reason);
|
|
}
|
|
|
|
export function parseLicense(text) {
|
|
let raw;
|
|
try {
|
|
raw = JSON.parse(text);
|
|
} catch {
|
|
refuse('The licence file is not valid JSON.');
|
|
}
|
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) refuse('The licence file is not an object.');
|
|
|
|
if (!L.FORMATS.includes(raw.format)) refuse(`The licence has format ${JSON.stringify(raw.format)}, this app reads format ${L.FORMATS.join(' and ')}.`);
|
|
if (typeof raw.signature !== 'string' || !HEX128.test(raw.signature)) refuse('The signature is missing or is not 128 hex characters.');
|
|
|
|
const body = raw.license;
|
|
if (!body || typeof body !== 'object' || Array.isArray(body)) refuse('The licence file has no `license` object.');
|
|
|
|
const extra = Object.keys(body).filter((k) => !FIELDS.includes(k));
|
|
if (extra.length) refuse(`The licence carries unknown fields: ${extra.join(', ')}.`);
|
|
const missing = FIELDS.filter((f) => !(f in body));
|
|
if (missing.length) refuse(`The licence is missing fields: ${missing.join(', ')}.`);
|
|
|
|
if (typeof body.buyer !== 'string' || !HEX64.test(body.buyer)) refuse('The buyer key is not 64 hex characters.');
|
|
if (!L.LEVELS.includes(body.level)) refuse(`Unknown level ${JSON.stringify(body.level)}.`);
|
|
if (typeof body.issued !== 'string' || !DATE.test(body.issued)) refuse('The issue date is not a YYYY-MM-DD date.');
|
|
if (!Number.isInteger(body.majorFrom) || body.majorFrom < 1) refuse('`majorFrom` is not a major version of 1 or higher.');
|
|
|
|
const unlimited = body.majorTo === L.UNLIMITED;
|
|
if (!unlimited && (!Number.isInteger(body.majorTo) || body.majorTo < body.majorFrom)) {
|
|
refuse(`\`majorTo\` is neither '${L.UNLIMITED}' nor a whole number from \`majorFrom\` up.`);
|
|
}
|
|
|
|
if (raw.format === 1) return { format: raw.format, license: body, signature: raw.signature };
|
|
|
|
const issuer = raw.issuer;
|
|
if (!issuer || typeof issuer !== 'object' || Array.isArray(issuer)) refuse('A format 2 licence has no `issuer` object.');
|
|
const issuerExtra = Object.keys(issuer).filter((k) => ![...ISSUER_FIELDS, 'signature'].includes(k));
|
|
if (issuerExtra.length) refuse(`The issuer carries unknown fields: ${issuerExtra.join(', ')}.`);
|
|
if (typeof issuer.key !== 'string' || !HEX64.test(issuer.key)) refuse('The issuer key is not 64 hex characters.');
|
|
if (typeof issuer.validFrom !== 'string' || !DATE.test(issuer.validFrom)) refuse('`validFrom` of the issuer is not a YYYY-MM-DD date.');
|
|
if (typeof issuer.validTo !== 'string' || !DATE.test(issuer.validTo)) refuse('`validTo` of the issuer is not a YYYY-MM-DD date.');
|
|
if (issuer.validTo < issuer.validFrom) refuse('The issuer is valid until before it is valid from.');
|
|
if (typeof issuer.signature !== 'string' || !HEX128.test(issuer.signature)) refuse('The signature of the issuer is missing or is not 128 hex characters.');
|
|
if (body.issued < issuer.validFrom || body.issued > issuer.validTo) refuse('The licence was issued outside the period of its issuer.');
|
|
return { format: raw.format, license: body, signature: raw.signature, issuer };
|
|
}
|
|
|
|
export function verifySignature(parsed, sellerKey = L.SELLER_KEY) {
|
|
if (!HEX64.test(sellerKey)) return false;
|
|
if (parsed.format === 2) {
|
|
if (!schnorrOk(parsed.issuer.signature, sha256(issuerMessage(parsed.issuer)), sellerKey)) return false;
|
|
return schnorrOk(parsed.signature, messageHash(parsed.license), parsed.issuer.key);
|
|
}
|
|
return schnorrOk(parsed.signature, messageHash(parsed.license), sellerKey);
|
|
}
|
|
|
|
export function coversVersion(license, appVersion) {
|
|
const m = /^(\d+)\.\d+\.\d+$/.exec(String(appVersion));
|
|
if (!m) return false;
|
|
const major = Number(m[1]);
|
|
if (major < 1) return false;
|
|
if (major < license.majorFrom) return false;
|
|
return license.majorTo === L.UNLIMITED ? true : major <= license.majorTo;
|
|
}
|
|
|
|
export function appVersion() {
|
|
const pad = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
|
|
return JSON.parse(readFileSync(pad, 'utf8')).version;
|
|
}
|
|
|
|
export function licenseState(version = appVersion(), dir = undefined) {
|
|
const major = Number(/^(\d+)\./.exec(String(version))?.[1]);
|
|
if (!Number.isInteger(major)) return { level: 'free', enforced: true, why: 'unknown-version' };
|
|
|
|
const forced = process.env.ASSETAPP_LICENSE_STATE;
|
|
if (forced === 'free') return { level: 'free', enforced: true, why: 'forced' };
|
|
const develop = major < 1 && services.get('develop') !== null;
|
|
if (forced === 'full' && develop) return { level: 'full', enforced: false, why: 'forced-develop' };
|
|
|
|
if (develop) return { level: 'full', enforced: false, why: 'develop' };
|
|
|
|
const uitkomst = dir === undefined ? readLicense() : readLicense(dir);
|
|
if (uitkomst.state === 'valid' && coversVersion(uitkomst.license, version)) {
|
|
return { level: uitkomst.license.level, enforced: true, why: 'licensed', license: uitkomst.license };
|
|
}
|
|
return { level: 'free', enforced: true, why: uitkomst.state === 'valid' ? 'out-of-range' : uitkomst.state };
|
|
}
|
|
|
|
export function maxFor(soort, state = licenseState()) {
|
|
const tabel = L.FREE.MAX;
|
|
if (!(soort in tabel)) throw new Error(`Unknown capacity: ${soort}. Known: ${Object.keys(tabel).join(', ')}.`);
|
|
return state.level === 'full' ? null : tabel[soort];
|
|
}
|
|
|
|
export function capacityReached(soort, huidig, state = licenseState()) {
|
|
const max = maxFor(soort, state);
|
|
if (max === null) return null;
|
|
return huidig >= max ? max : null;
|
|
}
|
|
|
|
export function allowedAssets(state = licenseState()) {
|
|
return state.level === 'full' ? null : L.FREE.ASSETS;
|
|
}
|
|
|
|
export function hasFeature(naam, state = licenseState()) {
|
|
const tabel = L.FREE.HAS;
|
|
if (!(naam in tabel)) throw new Error(`Unknown feature: ${naam}. Known: ${Object.keys(tabel).join(', ')}.`);
|
|
return state.level === 'full' ? true : tabel[naam];
|
|
}
|
|
|
|
export function readLicense(dir = installDir()) {
|
|
let text;
|
|
try {
|
|
text = readFileSync(join(dir, L.FILE_NAME), 'utf8');
|
|
} catch (err) {
|
|
if (err.code === 'ENOENT') return { state: 'none' };
|
|
return { state: 'unreadable', reason: err.message };
|
|
}
|
|
try {
|
|
const parsed = parseLicense(text);
|
|
if (!verifySignature(parsed)) return { state: 'invalid', reason: 'The signature does not belong to this licence.' };
|
|
return { state: 'valid', license: parsed.license };
|
|
} catch (err) {
|
|
if (err instanceof LicenseError) return { state: 'invalid', reason: err.message };
|
|
throw err;
|
|
}
|
|
}
|