Vendor @keysat/licensing-client to avoid private-repo auth in Docker build
The keysat-client-ts repo is private. Previous builds were succeeding
purely because Docker layer caching reused a node_modules from when
the repo had been accessible — once anything invalidated the
server/package.json or server/package-lock.json hash (the rename did),
npm in a fresh container hit github with no credentials and 404'd.
Fix: copy the built dist/ from server/node_modules/@keysat/licensing-
client/ into vendor/keysat-licensing-client/, strip the prepare/build
scripts (we already have the compiled output), and switch the server
package.json dep to a file: path:
"@keysat/licensing-client": "file:../vendor/keysat-licensing-client"
Dockerfile now COPY's vendor/ before npm ci. No git, no SSH, no
credentials needed in the build container — and the npm step is
pure-local so it's deterministic.
Side cleanup: dropped the apt-install-git + url.insteadOf gymnastics
that existed solely to work around the now-removed git+https resolution.
The image is slightly smaller (no git in the builder stage). Switched
the npm flag to the modern --omit=dev (the legacy --production printed
a warning).
If keysat-client-ts updates, regenerate vendor/ by:
cp -r server/node_modules/@keysat/licensing-client/{dist,package.json,LICENSE,README.md} \
vendor/keysat-licensing-client/
# then strip prepare/build scripts and devDeps from the copied package.json
# (or just hand-edit if the upstream package.json hasn't changed)
This commit is contained in:
+553
@@ -0,0 +1,553 @@
|
||||
"use strict";
|
||||
var __create = Object.create;
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||
// If the importer is in node compatibility mode or this is not an ESM
|
||||
// file that has been converted to a CommonJS file using a Babel-
|
||||
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||
mod
|
||||
));
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
|
||||
// src/index.ts
|
||||
var index_exports = {};
|
||||
__export(index_exports, {
|
||||
Client: () => Client,
|
||||
FLAG_FINGERPRINT_BOUND: () => FLAG_FINGERPRINT_BOUND,
|
||||
FLAG_TRIAL: () => FLAG_TRIAL,
|
||||
KEY_PREFIX: () => KEY_PREFIX,
|
||||
KEY_VERSION: () => KEY_VERSION,
|
||||
KEY_VERSION_V1: () => KEY_VERSION_V1,
|
||||
KEY_VERSION_V2: () => KEY_VERSION_V2,
|
||||
LicensingError: () => LicensingError,
|
||||
PublicKey: () => PublicKey,
|
||||
Verifier: () => Verifier,
|
||||
hasEntitlement: () => hasEntitlement,
|
||||
hashFingerprint: () => hashFingerprint,
|
||||
isExpiredAt: () => isExpiredAt,
|
||||
parseLicenseKey: () => parseLicenseKey
|
||||
});
|
||||
module.exports = __toCommonJS(index_exports);
|
||||
|
||||
// src/verify.ts
|
||||
var ed = __toESM(require("@noble/ed25519"), 1);
|
||||
var import_sha512 = require("@noble/hashes/sha512");
|
||||
|
||||
// src/errors.ts
|
||||
var LicensingError = class extends Error {
|
||||
/**
|
||||
* Machine-readable reason code. Common values:
|
||||
* `"bad_format"`, `"bad_encoding"`, `"bad_version"`, `"bad_signature"`,
|
||||
* `"expired"`, `"server_error"`, `"http_error"`, `"other"`.
|
||||
*/
|
||||
code;
|
||||
constructor(code, message) {
|
||||
super(message);
|
||||
this.name = "LicensingError";
|
||||
this.code = code;
|
||||
}
|
||||
};
|
||||
|
||||
// src/base32.ts
|
||||
var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
||||
var DECODE_TABLE = (() => {
|
||||
const t = {};
|
||||
for (let i = 0; i < ALPHABET.length; i++) t[ALPHABET[i]] = i;
|
||||
return t;
|
||||
})();
|
||||
function decodeBase32NoPad(input) {
|
||||
const up = input.toUpperCase();
|
||||
const out = new Uint8Array(Math.floor(up.length * 5 / 8));
|
||||
let bits = 0;
|
||||
let value = 0;
|
||||
let outPos = 0;
|
||||
for (let i = 0; i < up.length; i++) {
|
||||
const ch = up[i];
|
||||
const v = DECODE_TABLE[ch];
|
||||
if (v === void 0) {
|
||||
throw new LicensingError("bad_encoding", `invalid base32 character '${ch}'`);
|
||||
}
|
||||
value = value << 5 | v;
|
||||
bits += 5;
|
||||
if (bits >= 8) {
|
||||
bits -= 8;
|
||||
out[outPos++] = value >> bits & 255;
|
||||
}
|
||||
}
|
||||
return out.subarray(0, outPos);
|
||||
}
|
||||
|
||||
// src/key.ts
|
||||
var KEY_PREFIX = "LIC1";
|
||||
var KEY_VERSION_V1 = 1;
|
||||
var KEY_VERSION_V2 = 2;
|
||||
var KEY_VERSION = KEY_VERSION_V2;
|
||||
var FLAG_FINGERPRINT_BOUND = 1;
|
||||
var FLAG_TRIAL = 2;
|
||||
var PAYLOAD_V1_LEN = 74;
|
||||
var PAYLOAD_V2_HEAD_LEN = 83;
|
||||
var SIGNATURE_LEN = 64;
|
||||
function isExpiredAt(payload, nowUnixSeconds) {
|
||||
return payload.expiresAt !== 0 && nowUnixSeconds >= payload.expiresAt;
|
||||
}
|
||||
function hasEntitlement(payload, slug) {
|
||||
return payload.entitlements.includes(slug);
|
||||
}
|
||||
function parseLicenseKey(raw) {
|
||||
const trimmed = raw.trim();
|
||||
const firstDash = trimmed.indexOf("-");
|
||||
if (firstDash < 0) throw new LicensingError("bad_format", "key is missing prefix delimiter");
|
||||
const prefix = trimmed.slice(0, firstDash);
|
||||
if (prefix !== KEY_PREFIX) throw new LicensingError("bad_format", `unknown key prefix '${prefix}'`);
|
||||
const body = trimmed.slice(firstDash + 1);
|
||||
const lastDash = body.lastIndexOf("-");
|
||||
if (lastDash < 0) throw new LicensingError("bad_format", "key is missing signature delimiter");
|
||||
const payloadB32 = body.slice(0, lastDash);
|
||||
const signatureB32 = body.slice(lastDash + 1);
|
||||
const payloadBytes = decodeBase32NoPad(payloadB32);
|
||||
const signature = decodeBase32NoPad(signatureB32);
|
||||
if (signature.length !== SIGNATURE_LEN) {
|
||||
throw new LicensingError(
|
||||
"bad_format",
|
||||
`signature is ${signature.length} bytes; expected ${SIGNATURE_LEN}`
|
||||
);
|
||||
}
|
||||
if (payloadBytes.length < 1) {
|
||||
throw new LicensingError("bad_format", "empty payload");
|
||||
}
|
||||
const version = payloadBytes[0];
|
||||
let payload;
|
||||
switch (version) {
|
||||
case KEY_VERSION_V1:
|
||||
payload = parseV1(payloadBytes);
|
||||
break;
|
||||
case KEY_VERSION_V2:
|
||||
payload = parseV2(payloadBytes);
|
||||
break;
|
||||
default:
|
||||
throw new LicensingError("bad_version", `unsupported key version ${version}`);
|
||||
}
|
||||
return {
|
||||
payload,
|
||||
signedBytes: payloadBytes,
|
||||
signature
|
||||
};
|
||||
}
|
||||
function parseV1(payloadBytes) {
|
||||
if (payloadBytes.length !== PAYLOAD_V1_LEN) {
|
||||
throw new LicensingError(
|
||||
"bad_format",
|
||||
`v1 payload is ${payloadBytes.length} bytes; expected ${PAYLOAD_V1_LEN}`
|
||||
);
|
||||
}
|
||||
const flags = payloadBytes[1];
|
||||
const productId = payloadBytes.slice(2, 18);
|
||||
const licenseId = payloadBytes.slice(18, 34);
|
||||
const issuedAt = readBigEndianI64(payloadBytes, 34);
|
||||
const fingerprintHash = payloadBytes.slice(42, 74);
|
||||
return {
|
||||
version: KEY_VERSION_V1,
|
||||
flags,
|
||||
productId,
|
||||
licenseId,
|
||||
issuedAt,
|
||||
expiresAt: 0,
|
||||
fingerprintHash,
|
||||
entitlements: [],
|
||||
productUuid: uuidString(productId),
|
||||
licenseUuid: uuidString(licenseId),
|
||||
isFingerprintBound: (flags & FLAG_FINGERPRINT_BOUND) !== 0,
|
||||
isTrial: (flags & FLAG_TRIAL) !== 0
|
||||
};
|
||||
}
|
||||
function parseV2(payloadBytes) {
|
||||
if (payloadBytes.length < PAYLOAD_V2_HEAD_LEN) {
|
||||
throw new LicensingError(
|
||||
"bad_format",
|
||||
`v2 payload is ${payloadBytes.length} bytes; expected >= ${PAYLOAD_V2_HEAD_LEN}`
|
||||
);
|
||||
}
|
||||
const flags = payloadBytes[1];
|
||||
const productId = payloadBytes.slice(2, 18);
|
||||
const licenseId = payloadBytes.slice(18, 34);
|
||||
const issuedAt = readBigEndianI64(payloadBytes, 34);
|
||||
const expiresAt = readBigEndianI64(payloadBytes, 42);
|
||||
const fingerprintHash = payloadBytes.slice(50, 82);
|
||||
const numEntitlements = payloadBytes[82];
|
||||
const entitlements = [];
|
||||
let cursor = PAYLOAD_V2_HEAD_LEN;
|
||||
const decoder = new TextDecoder("utf-8", { fatal: true });
|
||||
for (let i = 0; i < numEntitlements; i++) {
|
||||
if (cursor >= payloadBytes.length) {
|
||||
throw new LicensingError("bad_format", "truncated entitlement list");
|
||||
}
|
||||
const len = payloadBytes[cursor];
|
||||
cursor += 1;
|
||||
if (cursor + len > payloadBytes.length) {
|
||||
throw new LicensingError("bad_format", "truncated entitlement");
|
||||
}
|
||||
try {
|
||||
entitlements.push(decoder.decode(payloadBytes.slice(cursor, cursor + len)));
|
||||
} catch {
|
||||
throw new LicensingError("bad_format", "entitlement not utf-8");
|
||||
}
|
||||
cursor += len;
|
||||
}
|
||||
if (cursor !== payloadBytes.length) {
|
||||
throw new LicensingError("bad_format", "trailing bytes in payload");
|
||||
}
|
||||
return {
|
||||
version: KEY_VERSION_V2,
|
||||
flags,
|
||||
productId,
|
||||
licenseId,
|
||||
issuedAt,
|
||||
expiresAt,
|
||||
fingerprintHash,
|
||||
entitlements,
|
||||
productUuid: uuidString(productId),
|
||||
licenseUuid: uuidString(licenseId),
|
||||
isFingerprintBound: (flags & FLAG_FINGERPRINT_BOUND) !== 0,
|
||||
isTrial: (flags & FLAG_TRIAL) !== 0
|
||||
};
|
||||
}
|
||||
function readBigEndianI64(buf, offset) {
|
||||
const view = new DataView(buf.buffer, buf.byteOffset + offset, 8);
|
||||
const hi = view.getInt32(0, false);
|
||||
const lo = view.getUint32(4, false);
|
||||
return hi * 2 ** 32 + lo;
|
||||
}
|
||||
function uuidString(b) {
|
||||
const h = Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
|
||||
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
||||
}
|
||||
|
||||
// src/fingerprint.ts
|
||||
var import_sha256 = require("@noble/hashes/sha256");
|
||||
function hashFingerprint(raw) {
|
||||
return (0, import_sha256.sha256)(new TextEncoder().encode(raw));
|
||||
}
|
||||
|
||||
// src/verify.ts
|
||||
ed.etc.sha512Sync = (...m) => (0, import_sha512.sha512)(ed.etc.concatBytes(...m));
|
||||
var Verifier = class {
|
||||
pubkey;
|
||||
constructor(pubkey) {
|
||||
this.pubkey = pubkey;
|
||||
}
|
||||
/** Verify a license key string. Throws on any failure. */
|
||||
verify(keyStr) {
|
||||
const key = parseLicenseKey(keyStr);
|
||||
const ok = ed.verify(key.signature, key.signedBytes, this.pubkey.raw);
|
||||
if (!ok) throw new LicensingError("bad_signature", "signature did not verify");
|
||||
return {
|
||||
payload: key.payload,
|
||||
licenseId: key.payload.licenseUuid,
|
||||
productId: key.payload.productUuid
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Verify AND enforce that, if the key is fingerprint-bound, the given
|
||||
* fingerprint matches. If the key is not bound, the fingerprint is
|
||||
* ignored. Throws on any failure.
|
||||
*/
|
||||
verifyWithFingerprint(keyStr, fingerprint) {
|
||||
const result = this.verify(keyStr);
|
||||
if (result.payload.isFingerprintBound) {
|
||||
const expected = hashFingerprint(fingerprint);
|
||||
const stored = result.payload.fingerprintHash;
|
||||
if (!equalBytes(expected, stored)) {
|
||||
throw new LicensingError("bad_signature", "fingerprint does not match bound key");
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* Verify a key and additionally reject it with an `expired` error if
|
||||
* `nowUnixSeconds` is at or past its `expiresAt`. Perpetual keys
|
||||
* (`expiresAt === 0`) are accepted regardless of `nowUnixSeconds`. This is
|
||||
* offline-only — no grace window logic; use `Client.validate` for that.
|
||||
*/
|
||||
verifyWithTime(keyStr, nowUnixSeconds) {
|
||||
const result = this.verify(keyStr);
|
||||
if (isExpiredAt(result.payload, nowUnixSeconds)) {
|
||||
throw new LicensingError("expired", "license has expired");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
function equalBytes(a, b) {
|
||||
if (a.length !== b.length) return false;
|
||||
let diff = 0;
|
||||
for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
|
||||
return diff === 0;
|
||||
}
|
||||
|
||||
// src/online.ts
|
||||
var Client = class {
|
||||
base;
|
||||
constructor(baseUrl) {
|
||||
this.base = baseUrl.replace(/\/+$/, "");
|
||||
}
|
||||
/** The normalized base URL this client is pinned to. */
|
||||
baseUrl() {
|
||||
return this.base;
|
||||
}
|
||||
/** Fetch the server's PEM-encoded public key. */
|
||||
async fetchPubkeyPem() {
|
||||
const data = await this.get("/v1/pubkey");
|
||||
return data.public_key_pem;
|
||||
}
|
||||
/**
|
||||
* Server-authoritative validation. Returns the full response including
|
||||
* expiry / entitlements / seat fields introduced in v2.
|
||||
*
|
||||
* Two-argument form kept for call-site compatibility with earlier SDK
|
||||
* versions; pass an options object for the full set of fields.
|
||||
*/
|
||||
async validate(key, productSlugOrOptions, fingerprint) {
|
||||
const opts = typeof productSlugOrOptions === "string" ? { productSlug: productSlugOrOptions, fingerprint } : productSlugOrOptions ?? {};
|
||||
const raw = await this.post("/v1/validate", {
|
||||
key,
|
||||
product_slug: opts.productSlug,
|
||||
fingerprint: opts.fingerprint,
|
||||
hostname: opts.hostname,
|
||||
platform: opts.platform
|
||||
});
|
||||
return this.toValidateResponse(raw);
|
||||
}
|
||||
/** Lightweight heartbeat. Server updates `last_heartbeat_at`. */
|
||||
async heartbeat(key, fingerprint) {
|
||||
const raw = await this.post("/v1/machines/heartbeat", {
|
||||
key,
|
||||
fingerprint
|
||||
});
|
||||
return this.toMachineResponse(raw);
|
||||
}
|
||||
/** Explicitly activate a seat for the given fingerprint. */
|
||||
async activate(key, fingerprint, opts = {}) {
|
||||
const raw = await this.post("/v1/machines/activate", {
|
||||
key,
|
||||
fingerprint,
|
||||
hostname: opts.hostname,
|
||||
platform: opts.platform
|
||||
});
|
||||
return this.toMachineResponse(raw);
|
||||
}
|
||||
/** Free a seat held by the given fingerprint. */
|
||||
async deactivate(key, fingerprint, reason) {
|
||||
const raw = await this.post("/v1/machines/deactivate", {
|
||||
key,
|
||||
fingerprint,
|
||||
reason
|
||||
});
|
||||
return this.toMachineResponse(raw);
|
||||
}
|
||||
/** Start a purchase. Returns the checkout URL and invoice id. */
|
||||
async startPurchase(productSlug, opts = {}) {
|
||||
const raw = await this.post("/v1/purchase", {
|
||||
product: productSlug,
|
||||
buyer_email: opts.buyerEmail,
|
||||
buyer_note: opts.buyerNote,
|
||||
redirect_url: opts.redirectUrl,
|
||||
code: opts.code
|
||||
});
|
||||
return {
|
||||
invoiceId: raw.invoice_id,
|
||||
btcpayInvoiceId: raw.btcpay_invoice_id,
|
||||
checkoutUrl: raw.checkout_url,
|
||||
amountSats: raw.amount_sats,
|
||||
pollUrl: raw.poll_url
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Redeem a `free_license` code: bypass BTCPay entirely and receive the
|
||||
* signed license key directly. Throws if the code is unknown / disabled
|
||||
* / expired / wrong product / not a free_license code, or if the cap
|
||||
* has been reached.
|
||||
*/
|
||||
async redeemFreeLicense(productSlug, code, opts = {}) {
|
||||
const raw = await this.post("/v1/redeem", {
|
||||
product: productSlug,
|
||||
code,
|
||||
buyer_email: opts.buyerEmail,
|
||||
buyer_note: opts.buyerNote
|
||||
});
|
||||
return {
|
||||
licenseId: raw.license_id,
|
||||
licenseKey: raw.license_key,
|
||||
invoiceId: raw.invoice_id,
|
||||
redemptionId: raw.redemption_id
|
||||
};
|
||||
}
|
||||
/** Poll a purchase by its invoice id. */
|
||||
async pollPurchase(invoiceId) {
|
||||
const raw = await this.get(
|
||||
`/v1/purchase/${encodeURIComponent(invoiceId)}`
|
||||
);
|
||||
return {
|
||||
invoiceId: raw.invoice_id,
|
||||
status: raw.status,
|
||||
productId: raw.product_id,
|
||||
amountSats: raw.amount_sats,
|
||||
licenseKey: raw.license_key ?? void 0,
|
||||
licenseId: raw.license_id ?? void 0
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Convenience: open the checkout, poll until a license key is issued,
|
||||
* then return it. Suitable for CLI usage or for an app UI that shows a
|
||||
* spinner while the buyer pays.
|
||||
*/
|
||||
async waitForLicense(invoiceId, options = {}) {
|
||||
const interval = options.intervalMs ?? 5e3;
|
||||
const deadline = options.timeoutMs ? Date.now() + options.timeoutMs : Infinity;
|
||||
while (true) {
|
||||
const poll = await this.pollPurchase(invoiceId);
|
||||
if (poll.licenseKey) return poll.licenseKey;
|
||||
if (poll.status === "expired" || poll.status === "invalid") {
|
||||
throw new LicensingError("server_error", `invoice ended in status ${poll.status}`);
|
||||
}
|
||||
if (Date.now() > deadline) {
|
||||
throw new LicensingError("server_error", "timed out waiting for license issuance");
|
||||
}
|
||||
await sleep(interval);
|
||||
}
|
||||
}
|
||||
// --- internals ---
|
||||
toValidateResponse(raw) {
|
||||
const entitlements = Array.isArray(raw.entitlements) ? raw.entitlements.filter((x) => typeof x === "string") : void 0;
|
||||
return {
|
||||
ok: !!raw.ok,
|
||||
reason: raw.reason,
|
||||
licenseId: raw.license_id,
|
||||
productId: raw.product_id,
|
||||
productSlug: raw.product_slug,
|
||||
issuedAt: raw.issued_at,
|
||||
expiresAt: raw.expires_at,
|
||||
graceUntil: raw.grace_until,
|
||||
inGracePeriod: raw.in_grace_period,
|
||||
isTrial: raw.is_trial,
|
||||
entitlements,
|
||||
status: raw.status,
|
||||
machineId: raw.machine_id,
|
||||
maxMachines: raw.max_machines
|
||||
};
|
||||
}
|
||||
toMachineResponse(raw) {
|
||||
return {
|
||||
ok: !!raw.ok,
|
||||
reason: raw.reason,
|
||||
machineId: raw.machine_id,
|
||||
activeCount: raw.active_count,
|
||||
maxMachines: raw.max_machines
|
||||
};
|
||||
}
|
||||
async get(path) {
|
||||
return this.request(path, { method: "GET" });
|
||||
}
|
||||
async post(path, body) {
|
||||
return this.request(path, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
}
|
||||
async request(path, init) {
|
||||
let resp;
|
||||
try {
|
||||
resp = await fetch(`${this.base}${path}`, init);
|
||||
} catch (e) {
|
||||
throw new LicensingError("http_error", e instanceof Error ? e.message : String(e));
|
||||
}
|
||||
const text = await resp.text();
|
||||
if (!resp.ok) {
|
||||
throw new LicensingError("server_error", `HTTP ${resp.status}: ${text}`);
|
||||
}
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch {
|
||||
throw new LicensingError("server_error", `non-JSON response: ${text}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
function sleep(ms) {
|
||||
return new Promise((res) => setTimeout(res, ms));
|
||||
}
|
||||
|
||||
// src/pubkey.ts
|
||||
var PublicKey = class _PublicKey {
|
||||
/** Raw 32-byte Ed25519 public key material. */
|
||||
raw;
|
||||
constructor(raw) {
|
||||
if (raw.length !== 32) {
|
||||
throw new LicensingError(
|
||||
"bad_format",
|
||||
`public key must be 32 bytes; got ${raw.length}`
|
||||
);
|
||||
}
|
||||
this.raw = raw;
|
||||
}
|
||||
/** Parse a PEM blob as emitted by the service. */
|
||||
static fromPem(pem) {
|
||||
const stripped = pem.replace(/-----BEGIN [^-]+-----/g, "").replace(/-----END [^-]+-----/g, "").replace(/\s+/g, "");
|
||||
if (!stripped) {
|
||||
throw new LicensingError("bad_format", "empty PEM input");
|
||||
}
|
||||
const der = base64Decode(stripped);
|
||||
if (der.length < 32) {
|
||||
throw new LicensingError("bad_format", "PEM body too short to contain a public key");
|
||||
}
|
||||
const raw = der.slice(der.length - 32);
|
||||
return new _PublicKey(raw);
|
||||
}
|
||||
/** Construct from raw bytes (no PEM envelope). */
|
||||
static fromBytes(bytes) {
|
||||
return new _PublicKey(bytes);
|
||||
}
|
||||
};
|
||||
function base64Decode(b64) {
|
||||
const nodeBuffer = globalThis.Buffer;
|
||||
if (nodeBuffer) {
|
||||
return new Uint8Array(nodeBuffer.from(b64, "base64"));
|
||||
}
|
||||
const bin = atob(b64);
|
||||
const out = new Uint8Array(bin.length);
|
||||
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
||||
return out;
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
Client,
|
||||
FLAG_FINGERPRINT_BOUND,
|
||||
FLAG_TRIAL,
|
||||
KEY_PREFIX,
|
||||
KEY_VERSION,
|
||||
KEY_VERSION_V1,
|
||||
KEY_VERSION_V2,
|
||||
LicensingError,
|
||||
PublicKey,
|
||||
Verifier,
|
||||
hasEntitlement,
|
||||
hashFingerprint,
|
||||
isExpiredAt,
|
||||
parseLicenseKey
|
||||
});
|
||||
+306
@@ -0,0 +1,306 @@
|
||||
/**
|
||||
* License-key parsing. Matches the service's wire format exactly.
|
||||
*
|
||||
* ## Wire format
|
||||
*
|
||||
* A key string looks like `LIC1-<payload_b32>-<signature_b32>`. Both halves
|
||||
* are Crockford base32 (no padding) of the raw bytes.
|
||||
*
|
||||
* ### v1 payload (74 bytes, fixed)
|
||||
*
|
||||
* ```text
|
||||
* offset size field
|
||||
* 0 1 version = 1
|
||||
* 1 1 flags
|
||||
* 2 16 product_id (UUID bytes)
|
||||
* 18 16 license_id (UUID bytes)
|
||||
* 34 8 issued_at (i64 unix seconds, big-endian)
|
||||
* 42 32 fingerprint_hash (SHA-256, or all-zero)
|
||||
* ```
|
||||
*
|
||||
* ### v2 payload (83 bytes + variable entitlements)
|
||||
*
|
||||
* ```text
|
||||
* offset size field
|
||||
* 0 1 version = 2
|
||||
* 1 1 flags
|
||||
* 2 16 product_id
|
||||
* 18 16 license_id
|
||||
* 34 8 issued_at
|
||||
* 42 8 expires_at (i64, 0 = perpetual)
|
||||
* 50 32 fingerprint_hash
|
||||
* 82 1 num_entitlements (u8)
|
||||
* 83 * entitlements — each: [u8 len][len utf-8 bytes]
|
||||
* ```
|
||||
*
|
||||
* Clients verifying a v1 key treat `expiresAt` as 0 and `entitlements` as
|
||||
* empty, so application code can branch on flags / fields uniformly.
|
||||
*/
|
||||
declare const KEY_PREFIX = "LIC1";
|
||||
/** v1 format identifier. */
|
||||
declare const KEY_VERSION_V1 = 1;
|
||||
/** v2 format identifier. */
|
||||
declare const KEY_VERSION_V2 = 2;
|
||||
/** Highest format version this client understands. */
|
||||
declare const KEY_VERSION = 2;
|
||||
/** Set when the key is bound to a specific machine fingerprint hash. */
|
||||
declare const FLAG_FINGERPRINT_BOUND = 1;
|
||||
/** Set on trial keys. */
|
||||
declare const FLAG_TRIAL = 2;
|
||||
/** Decoded fields of the signed payload. */
|
||||
interface LicensePayload {
|
||||
/** Format version (1 or 2). */
|
||||
version: number;
|
||||
/** Feature flags. */
|
||||
flags: number;
|
||||
/** Raw 16-byte product id (UUID). */
|
||||
productId: Uint8Array;
|
||||
/** Raw 16-byte license id (UUID). */
|
||||
licenseId: Uint8Array;
|
||||
/** Unix seconds issued. */
|
||||
issuedAt: number;
|
||||
/** Unix seconds expiry; `0` for perpetual. Always `0` on v1 keys. */
|
||||
expiresAt: number;
|
||||
/** SHA-256 hash of the bound machine fingerprint, or all-zero. */
|
||||
fingerprintHash: Uint8Array;
|
||||
/** Entitlement slugs granted by this license. Empty on v1 keys. */
|
||||
entitlements: string[];
|
||||
/** Product UUID in canonical string form. */
|
||||
productUuid: string;
|
||||
/** License UUID in canonical string form. */
|
||||
licenseUuid: string;
|
||||
/** True if the key is fingerprint-bound. */
|
||||
isFingerprintBound: boolean;
|
||||
/** True if the key is flagged as a trial. */
|
||||
isTrial: boolean;
|
||||
}
|
||||
/** A parsed (not yet verified) license key. */
|
||||
interface LicenseKey {
|
||||
payload: LicensePayload;
|
||||
/**
|
||||
* Raw payload bytes (what the server signed over). Length is 74 on v1,
|
||||
* `>= 83` on v2.
|
||||
*/
|
||||
signedBytes: Uint8Array;
|
||||
/** Raw 64-byte signature. */
|
||||
signature: Uint8Array;
|
||||
}
|
||||
/** True if `nowUnixSeconds` is at or after the key's `expiresAt`. */
|
||||
declare function isExpiredAt(payload: LicensePayload, nowUnixSeconds: number): boolean;
|
||||
/** True if the license grants the given entitlement slug. */
|
||||
declare function hasEntitlement(payload: LicensePayload, slug: string): boolean;
|
||||
/** Parse a `LIC1-...-...` string. Does NOT verify. */
|
||||
declare function parseLicenseKey(raw: string): LicenseKey;
|
||||
|
||||
/**
|
||||
* Issuer public key. Accepts either raw 32-byte Ed25519 key material or a
|
||||
* PEM-encoded SubjectPublicKeyInfo blob (which is what the service returns
|
||||
* from `/v1/pubkey`).
|
||||
*/
|
||||
/** Parsed Ed25519 public key, ready for signature verification. */
|
||||
declare class PublicKey {
|
||||
/** Raw 32-byte Ed25519 public key material. */
|
||||
readonly raw: Uint8Array;
|
||||
constructor(raw: Uint8Array);
|
||||
/** Parse a PEM blob as emitted by the service. */
|
||||
static fromPem(pem: string): PublicKey;
|
||||
/** Construct from raw bytes (no PEM envelope). */
|
||||
static fromBytes(bytes: Uint8Array): PublicKey;
|
||||
}
|
||||
|
||||
/** Offline Ed25519 signature verification. */
|
||||
|
||||
interface VerifyOk {
|
||||
/** Parsed payload fields. */
|
||||
payload: LicensePayload;
|
||||
/** License UUID as a canonical string. */
|
||||
licenseId: string;
|
||||
/** Product UUID as a canonical string. */
|
||||
productId: string;
|
||||
}
|
||||
/** Verifies license keys against a single issuing server's public key. */
|
||||
declare class Verifier {
|
||||
private pubkey;
|
||||
constructor(pubkey: PublicKey);
|
||||
/** Verify a license key string. Throws on any failure. */
|
||||
verify(keyStr: string): VerifyOk;
|
||||
/**
|
||||
* Verify AND enforce that, if the key is fingerprint-bound, the given
|
||||
* fingerprint matches. If the key is not bound, the fingerprint is
|
||||
* ignored. Throws on any failure.
|
||||
*/
|
||||
verifyWithFingerprint(keyStr: string, fingerprint: string): VerifyOk;
|
||||
/**
|
||||
* Verify a key and additionally reject it with an `expired` error if
|
||||
* `nowUnixSeconds` is at or past its `expiresAt`. Perpetual keys
|
||||
* (`expiresAt === 0`) are accepted regardless of `nowUnixSeconds`. This is
|
||||
* offline-only — no grace window logic; use `Client.validate` for that.
|
||||
*/
|
||||
verifyWithTime(keyStr: string, nowUnixSeconds: number): VerifyOk;
|
||||
}
|
||||
|
||||
/**
|
||||
* Online operations against a running `licensing-service` instance.
|
||||
*
|
||||
* All methods use the global `fetch` available in Node 18+ and every modern
|
||||
* browser. No additional runtime required.
|
||||
*/
|
||||
interface ValidateResponse {
|
||||
ok: boolean;
|
||||
/**
|
||||
* Machine-readable reason on failure. One of:
|
||||
* `bad_format`, `bad_signature`, `not_found`, `revoked`, `suspended`,
|
||||
* `expired`, `product_mismatch`, `fingerprint_mismatch`,
|
||||
* `too_many_machines`, `rate_limited`, `invalid_state`.
|
||||
*/
|
||||
reason?: string;
|
||||
licenseId?: string;
|
||||
productId?: string;
|
||||
productSlug?: string;
|
||||
issuedAt?: string;
|
||||
/** Expiry timestamp (RFC 3339) if the license has one. */
|
||||
expiresAt?: string;
|
||||
/** End of the grace window (RFC 3339) when in a grace period. */
|
||||
graceUntil?: string;
|
||||
/** True when the key is past `expiresAt` but still inside the grace window. */
|
||||
inGracePeriod?: boolean;
|
||||
/** True if this license is flagged as a trial. */
|
||||
isTrial?: boolean;
|
||||
/** Entitlement slugs granted by the license. */
|
||||
entitlements?: string[];
|
||||
/** License status string: `active`, `suspended`, `revoked`. */
|
||||
status?: string;
|
||||
/** Machine id created or matched by this call (when fingerprint was sent). */
|
||||
machineId?: string;
|
||||
/** Seat cap: `0` unlimited, `1` single-seat, `n` n-seat. */
|
||||
maxMachines?: number;
|
||||
}
|
||||
interface ValidateOptions {
|
||||
/** Product slug the caller expects the key to cover. */
|
||||
productSlug?: string;
|
||||
/** Raw machine fingerprint; enables seat binding / cap enforcement. */
|
||||
fingerprint?: string;
|
||||
/** Client-supplied hostname, stored against the machine row on activation. */
|
||||
hostname?: string;
|
||||
/** Client-supplied platform descriptor, e.g. `'linux-x86_64'`. */
|
||||
platform?: string;
|
||||
}
|
||||
interface MachineResponse {
|
||||
ok: boolean;
|
||||
reason?: string;
|
||||
machineId?: string;
|
||||
activeCount?: number;
|
||||
maxMachines?: number;
|
||||
}
|
||||
interface PurchaseSession {
|
||||
/** Our internal invoice id — use with `pollPurchase`. */
|
||||
invoiceId: string;
|
||||
/** BTCPay's invoice id (opaque). */
|
||||
btcpayInvoiceId: string;
|
||||
/** URL to open in the buyer's browser to pay. */
|
||||
checkoutUrl: string;
|
||||
/** Price in satoshis. */
|
||||
amountSats: number;
|
||||
/** Where the service recommends polling. */
|
||||
pollUrl: string;
|
||||
}
|
||||
interface PollResponse {
|
||||
invoiceId: string;
|
||||
/** `pending | settled | expired | invalid`. */
|
||||
status: string;
|
||||
productId: string;
|
||||
amountSats: number;
|
||||
/** Populated once the license has been issued. */
|
||||
licenseKey?: string;
|
||||
licenseId?: string;
|
||||
}
|
||||
interface StartPurchaseOptions {
|
||||
/** Optional email for the receipt. */
|
||||
buyerEmail?: string;
|
||||
/** Optional URL the buyer should be returned to after payment. */
|
||||
redirectUrl?: string;
|
||||
/** Optional discount / referral code. */
|
||||
code?: string;
|
||||
/** Optional buyer note recorded on the invoice (admin-visible). */
|
||||
buyerNote?: string;
|
||||
}
|
||||
interface RedeemFreeOptions {
|
||||
/** Optional email recorded on the synthetic invoice + license. */
|
||||
buyerEmail?: string;
|
||||
/** Optional buyer note. */
|
||||
buyerNote?: string;
|
||||
}
|
||||
interface RedeemFreeResponse {
|
||||
licenseId: string;
|
||||
/** The fully-signed license key, ready for offline verification. */
|
||||
licenseKey: string;
|
||||
invoiceId: string;
|
||||
redemptionId: string;
|
||||
}
|
||||
/** An HTTP client pinned to one licensing-service base URL. */
|
||||
declare class Client {
|
||||
private base;
|
||||
constructor(baseUrl: string);
|
||||
/** The normalized base URL this client is pinned to. */
|
||||
baseUrl(): string;
|
||||
/** Fetch the server's PEM-encoded public key. */
|
||||
fetchPubkeyPem(): Promise<string>;
|
||||
/**
|
||||
* Server-authoritative validation. Returns the full response including
|
||||
* expiry / entitlements / seat fields introduced in v2.
|
||||
*
|
||||
* Two-argument form kept for call-site compatibility with earlier SDK
|
||||
* versions; pass an options object for the full set of fields.
|
||||
*/
|
||||
validate(key: string, productSlugOrOptions?: string | ValidateOptions, fingerprint?: string): Promise<ValidateResponse>;
|
||||
/** Lightweight heartbeat. Server updates `last_heartbeat_at`. */
|
||||
heartbeat(key: string, fingerprint: string): Promise<MachineResponse>;
|
||||
/** Explicitly activate a seat for the given fingerprint. */
|
||||
activate(key: string, fingerprint: string, opts?: {
|
||||
hostname?: string;
|
||||
platform?: string;
|
||||
}): Promise<MachineResponse>;
|
||||
/** Free a seat held by the given fingerprint. */
|
||||
deactivate(key: string, fingerprint: string, reason?: string): Promise<MachineResponse>;
|
||||
/** Start a purchase. Returns the checkout URL and invoice id. */
|
||||
startPurchase(productSlug: string, opts?: StartPurchaseOptions): Promise<PurchaseSession>;
|
||||
/**
|
||||
* Redeem a `free_license` code: bypass BTCPay entirely and receive the
|
||||
* signed license key directly. Throws if the code is unknown / disabled
|
||||
* / expired / wrong product / not a free_license code, or if the cap
|
||||
* has been reached.
|
||||
*/
|
||||
redeemFreeLicense(productSlug: string, code: string, opts?: RedeemFreeOptions): Promise<RedeemFreeResponse>;
|
||||
/** Poll a purchase by its invoice id. */
|
||||
pollPurchase(invoiceId: string): Promise<PollResponse>;
|
||||
/**
|
||||
* Convenience: open the checkout, poll until a license key is issued,
|
||||
* then return it. Suitable for CLI usage or for an app UI that shows a
|
||||
* spinner while the buyer pays.
|
||||
*/
|
||||
waitForLicense(invoiceId: string, options?: {
|
||||
intervalMs?: number;
|
||||
timeoutMs?: number;
|
||||
}): Promise<string>;
|
||||
private toValidateResponse;
|
||||
private toMachineResponse;
|
||||
private get;
|
||||
private post;
|
||||
private request;
|
||||
}
|
||||
|
||||
/** Hash a raw fingerprint string to the 32-byte form embedded in keys. */
|
||||
declare function hashFingerprint(raw: string): Uint8Array;
|
||||
|
||||
/** All errors thrown by this library inherit from `LicensingError`. */
|
||||
declare class LicensingError extends Error {
|
||||
/**
|
||||
* Machine-readable reason code. Common values:
|
||||
* `"bad_format"`, `"bad_encoding"`, `"bad_version"`, `"bad_signature"`,
|
||||
* `"expired"`, `"server_error"`, `"http_error"`, `"other"`.
|
||||
*/
|
||||
readonly code: string;
|
||||
constructor(code: string, message: string);
|
||||
}
|
||||
|
||||
export { Client, FLAG_FINGERPRINT_BOUND, FLAG_TRIAL, KEY_PREFIX, KEY_VERSION, KEY_VERSION_V1, KEY_VERSION_V2, type LicenseKey, type LicensePayload, LicensingError, type MachineResponse, type PollResponse, PublicKey, type PurchaseSession, type RedeemFreeOptions, type RedeemFreeResponse, type StartPurchaseOptions, type ValidateOptions, type ValidateResponse, Verifier, type VerifyOk, hasEntitlement, hashFingerprint, isExpiredAt, parseLicenseKey };
|
||||
+306
@@ -0,0 +1,306 @@
|
||||
/**
|
||||
* License-key parsing. Matches the service's wire format exactly.
|
||||
*
|
||||
* ## Wire format
|
||||
*
|
||||
* A key string looks like `LIC1-<payload_b32>-<signature_b32>`. Both halves
|
||||
* are Crockford base32 (no padding) of the raw bytes.
|
||||
*
|
||||
* ### v1 payload (74 bytes, fixed)
|
||||
*
|
||||
* ```text
|
||||
* offset size field
|
||||
* 0 1 version = 1
|
||||
* 1 1 flags
|
||||
* 2 16 product_id (UUID bytes)
|
||||
* 18 16 license_id (UUID bytes)
|
||||
* 34 8 issued_at (i64 unix seconds, big-endian)
|
||||
* 42 32 fingerprint_hash (SHA-256, or all-zero)
|
||||
* ```
|
||||
*
|
||||
* ### v2 payload (83 bytes + variable entitlements)
|
||||
*
|
||||
* ```text
|
||||
* offset size field
|
||||
* 0 1 version = 2
|
||||
* 1 1 flags
|
||||
* 2 16 product_id
|
||||
* 18 16 license_id
|
||||
* 34 8 issued_at
|
||||
* 42 8 expires_at (i64, 0 = perpetual)
|
||||
* 50 32 fingerprint_hash
|
||||
* 82 1 num_entitlements (u8)
|
||||
* 83 * entitlements — each: [u8 len][len utf-8 bytes]
|
||||
* ```
|
||||
*
|
||||
* Clients verifying a v1 key treat `expiresAt` as 0 and `entitlements` as
|
||||
* empty, so application code can branch on flags / fields uniformly.
|
||||
*/
|
||||
declare const KEY_PREFIX = "LIC1";
|
||||
/** v1 format identifier. */
|
||||
declare const KEY_VERSION_V1 = 1;
|
||||
/** v2 format identifier. */
|
||||
declare const KEY_VERSION_V2 = 2;
|
||||
/** Highest format version this client understands. */
|
||||
declare const KEY_VERSION = 2;
|
||||
/** Set when the key is bound to a specific machine fingerprint hash. */
|
||||
declare const FLAG_FINGERPRINT_BOUND = 1;
|
||||
/** Set on trial keys. */
|
||||
declare const FLAG_TRIAL = 2;
|
||||
/** Decoded fields of the signed payload. */
|
||||
interface LicensePayload {
|
||||
/** Format version (1 or 2). */
|
||||
version: number;
|
||||
/** Feature flags. */
|
||||
flags: number;
|
||||
/** Raw 16-byte product id (UUID). */
|
||||
productId: Uint8Array;
|
||||
/** Raw 16-byte license id (UUID). */
|
||||
licenseId: Uint8Array;
|
||||
/** Unix seconds issued. */
|
||||
issuedAt: number;
|
||||
/** Unix seconds expiry; `0` for perpetual. Always `0` on v1 keys. */
|
||||
expiresAt: number;
|
||||
/** SHA-256 hash of the bound machine fingerprint, or all-zero. */
|
||||
fingerprintHash: Uint8Array;
|
||||
/** Entitlement slugs granted by this license. Empty on v1 keys. */
|
||||
entitlements: string[];
|
||||
/** Product UUID in canonical string form. */
|
||||
productUuid: string;
|
||||
/** License UUID in canonical string form. */
|
||||
licenseUuid: string;
|
||||
/** True if the key is fingerprint-bound. */
|
||||
isFingerprintBound: boolean;
|
||||
/** True if the key is flagged as a trial. */
|
||||
isTrial: boolean;
|
||||
}
|
||||
/** A parsed (not yet verified) license key. */
|
||||
interface LicenseKey {
|
||||
payload: LicensePayload;
|
||||
/**
|
||||
* Raw payload bytes (what the server signed over). Length is 74 on v1,
|
||||
* `>= 83` on v2.
|
||||
*/
|
||||
signedBytes: Uint8Array;
|
||||
/** Raw 64-byte signature. */
|
||||
signature: Uint8Array;
|
||||
}
|
||||
/** True if `nowUnixSeconds` is at or after the key's `expiresAt`. */
|
||||
declare function isExpiredAt(payload: LicensePayload, nowUnixSeconds: number): boolean;
|
||||
/** True if the license grants the given entitlement slug. */
|
||||
declare function hasEntitlement(payload: LicensePayload, slug: string): boolean;
|
||||
/** Parse a `LIC1-...-...` string. Does NOT verify. */
|
||||
declare function parseLicenseKey(raw: string): LicenseKey;
|
||||
|
||||
/**
|
||||
* Issuer public key. Accepts either raw 32-byte Ed25519 key material or a
|
||||
* PEM-encoded SubjectPublicKeyInfo blob (which is what the service returns
|
||||
* from `/v1/pubkey`).
|
||||
*/
|
||||
/** Parsed Ed25519 public key, ready for signature verification. */
|
||||
declare class PublicKey {
|
||||
/** Raw 32-byte Ed25519 public key material. */
|
||||
readonly raw: Uint8Array;
|
||||
constructor(raw: Uint8Array);
|
||||
/** Parse a PEM blob as emitted by the service. */
|
||||
static fromPem(pem: string): PublicKey;
|
||||
/** Construct from raw bytes (no PEM envelope). */
|
||||
static fromBytes(bytes: Uint8Array): PublicKey;
|
||||
}
|
||||
|
||||
/** Offline Ed25519 signature verification. */
|
||||
|
||||
interface VerifyOk {
|
||||
/** Parsed payload fields. */
|
||||
payload: LicensePayload;
|
||||
/** License UUID as a canonical string. */
|
||||
licenseId: string;
|
||||
/** Product UUID as a canonical string. */
|
||||
productId: string;
|
||||
}
|
||||
/** Verifies license keys against a single issuing server's public key. */
|
||||
declare class Verifier {
|
||||
private pubkey;
|
||||
constructor(pubkey: PublicKey);
|
||||
/** Verify a license key string. Throws on any failure. */
|
||||
verify(keyStr: string): VerifyOk;
|
||||
/**
|
||||
* Verify AND enforce that, if the key is fingerprint-bound, the given
|
||||
* fingerprint matches. If the key is not bound, the fingerprint is
|
||||
* ignored. Throws on any failure.
|
||||
*/
|
||||
verifyWithFingerprint(keyStr: string, fingerprint: string): VerifyOk;
|
||||
/**
|
||||
* Verify a key and additionally reject it with an `expired` error if
|
||||
* `nowUnixSeconds` is at or past its `expiresAt`. Perpetual keys
|
||||
* (`expiresAt === 0`) are accepted regardless of `nowUnixSeconds`. This is
|
||||
* offline-only — no grace window logic; use `Client.validate` for that.
|
||||
*/
|
||||
verifyWithTime(keyStr: string, nowUnixSeconds: number): VerifyOk;
|
||||
}
|
||||
|
||||
/**
|
||||
* Online operations against a running `licensing-service` instance.
|
||||
*
|
||||
* All methods use the global `fetch` available in Node 18+ and every modern
|
||||
* browser. No additional runtime required.
|
||||
*/
|
||||
interface ValidateResponse {
|
||||
ok: boolean;
|
||||
/**
|
||||
* Machine-readable reason on failure. One of:
|
||||
* `bad_format`, `bad_signature`, `not_found`, `revoked`, `suspended`,
|
||||
* `expired`, `product_mismatch`, `fingerprint_mismatch`,
|
||||
* `too_many_machines`, `rate_limited`, `invalid_state`.
|
||||
*/
|
||||
reason?: string;
|
||||
licenseId?: string;
|
||||
productId?: string;
|
||||
productSlug?: string;
|
||||
issuedAt?: string;
|
||||
/** Expiry timestamp (RFC 3339) if the license has one. */
|
||||
expiresAt?: string;
|
||||
/** End of the grace window (RFC 3339) when in a grace period. */
|
||||
graceUntil?: string;
|
||||
/** True when the key is past `expiresAt` but still inside the grace window. */
|
||||
inGracePeriod?: boolean;
|
||||
/** True if this license is flagged as a trial. */
|
||||
isTrial?: boolean;
|
||||
/** Entitlement slugs granted by the license. */
|
||||
entitlements?: string[];
|
||||
/** License status string: `active`, `suspended`, `revoked`. */
|
||||
status?: string;
|
||||
/** Machine id created or matched by this call (when fingerprint was sent). */
|
||||
machineId?: string;
|
||||
/** Seat cap: `0` unlimited, `1` single-seat, `n` n-seat. */
|
||||
maxMachines?: number;
|
||||
}
|
||||
interface ValidateOptions {
|
||||
/** Product slug the caller expects the key to cover. */
|
||||
productSlug?: string;
|
||||
/** Raw machine fingerprint; enables seat binding / cap enforcement. */
|
||||
fingerprint?: string;
|
||||
/** Client-supplied hostname, stored against the machine row on activation. */
|
||||
hostname?: string;
|
||||
/** Client-supplied platform descriptor, e.g. `'linux-x86_64'`. */
|
||||
platform?: string;
|
||||
}
|
||||
interface MachineResponse {
|
||||
ok: boolean;
|
||||
reason?: string;
|
||||
machineId?: string;
|
||||
activeCount?: number;
|
||||
maxMachines?: number;
|
||||
}
|
||||
interface PurchaseSession {
|
||||
/** Our internal invoice id — use with `pollPurchase`. */
|
||||
invoiceId: string;
|
||||
/** BTCPay's invoice id (opaque). */
|
||||
btcpayInvoiceId: string;
|
||||
/** URL to open in the buyer's browser to pay. */
|
||||
checkoutUrl: string;
|
||||
/** Price in satoshis. */
|
||||
amountSats: number;
|
||||
/** Where the service recommends polling. */
|
||||
pollUrl: string;
|
||||
}
|
||||
interface PollResponse {
|
||||
invoiceId: string;
|
||||
/** `pending | settled | expired | invalid`. */
|
||||
status: string;
|
||||
productId: string;
|
||||
amountSats: number;
|
||||
/** Populated once the license has been issued. */
|
||||
licenseKey?: string;
|
||||
licenseId?: string;
|
||||
}
|
||||
interface StartPurchaseOptions {
|
||||
/** Optional email for the receipt. */
|
||||
buyerEmail?: string;
|
||||
/** Optional URL the buyer should be returned to after payment. */
|
||||
redirectUrl?: string;
|
||||
/** Optional discount / referral code. */
|
||||
code?: string;
|
||||
/** Optional buyer note recorded on the invoice (admin-visible). */
|
||||
buyerNote?: string;
|
||||
}
|
||||
interface RedeemFreeOptions {
|
||||
/** Optional email recorded on the synthetic invoice + license. */
|
||||
buyerEmail?: string;
|
||||
/** Optional buyer note. */
|
||||
buyerNote?: string;
|
||||
}
|
||||
interface RedeemFreeResponse {
|
||||
licenseId: string;
|
||||
/** The fully-signed license key, ready for offline verification. */
|
||||
licenseKey: string;
|
||||
invoiceId: string;
|
||||
redemptionId: string;
|
||||
}
|
||||
/** An HTTP client pinned to one licensing-service base URL. */
|
||||
declare class Client {
|
||||
private base;
|
||||
constructor(baseUrl: string);
|
||||
/** The normalized base URL this client is pinned to. */
|
||||
baseUrl(): string;
|
||||
/** Fetch the server's PEM-encoded public key. */
|
||||
fetchPubkeyPem(): Promise<string>;
|
||||
/**
|
||||
* Server-authoritative validation. Returns the full response including
|
||||
* expiry / entitlements / seat fields introduced in v2.
|
||||
*
|
||||
* Two-argument form kept for call-site compatibility with earlier SDK
|
||||
* versions; pass an options object for the full set of fields.
|
||||
*/
|
||||
validate(key: string, productSlugOrOptions?: string | ValidateOptions, fingerprint?: string): Promise<ValidateResponse>;
|
||||
/** Lightweight heartbeat. Server updates `last_heartbeat_at`. */
|
||||
heartbeat(key: string, fingerprint: string): Promise<MachineResponse>;
|
||||
/** Explicitly activate a seat for the given fingerprint. */
|
||||
activate(key: string, fingerprint: string, opts?: {
|
||||
hostname?: string;
|
||||
platform?: string;
|
||||
}): Promise<MachineResponse>;
|
||||
/** Free a seat held by the given fingerprint. */
|
||||
deactivate(key: string, fingerprint: string, reason?: string): Promise<MachineResponse>;
|
||||
/** Start a purchase. Returns the checkout URL and invoice id. */
|
||||
startPurchase(productSlug: string, opts?: StartPurchaseOptions): Promise<PurchaseSession>;
|
||||
/**
|
||||
* Redeem a `free_license` code: bypass BTCPay entirely and receive the
|
||||
* signed license key directly. Throws if the code is unknown / disabled
|
||||
* / expired / wrong product / not a free_license code, or if the cap
|
||||
* has been reached.
|
||||
*/
|
||||
redeemFreeLicense(productSlug: string, code: string, opts?: RedeemFreeOptions): Promise<RedeemFreeResponse>;
|
||||
/** Poll a purchase by its invoice id. */
|
||||
pollPurchase(invoiceId: string): Promise<PollResponse>;
|
||||
/**
|
||||
* Convenience: open the checkout, poll until a license key is issued,
|
||||
* then return it. Suitable for CLI usage or for an app UI that shows a
|
||||
* spinner while the buyer pays.
|
||||
*/
|
||||
waitForLicense(invoiceId: string, options?: {
|
||||
intervalMs?: number;
|
||||
timeoutMs?: number;
|
||||
}): Promise<string>;
|
||||
private toValidateResponse;
|
||||
private toMachineResponse;
|
||||
private get;
|
||||
private post;
|
||||
private request;
|
||||
}
|
||||
|
||||
/** Hash a raw fingerprint string to the 32-byte form embedded in keys. */
|
||||
declare function hashFingerprint(raw: string): Uint8Array;
|
||||
|
||||
/** All errors thrown by this library inherit from `LicensingError`. */
|
||||
declare class LicensingError extends Error {
|
||||
/**
|
||||
* Machine-readable reason code. Common values:
|
||||
* `"bad_format"`, `"bad_encoding"`, `"bad_version"`, `"bad_signature"`,
|
||||
* `"expired"`, `"server_error"`, `"http_error"`, `"other"`.
|
||||
*/
|
||||
readonly code: string;
|
||||
constructor(code: string, message: string);
|
||||
}
|
||||
|
||||
export { Client, FLAG_FINGERPRINT_BOUND, FLAG_TRIAL, KEY_PREFIX, KEY_VERSION, KEY_VERSION_V1, KEY_VERSION_V2, type LicenseKey, type LicensePayload, LicensingError, type MachineResponse, type PollResponse, PublicKey, type PurchaseSession, type RedeemFreeOptions, type RedeemFreeResponse, type StartPurchaseOptions, type ValidateOptions, type ValidateResponse, Verifier, type VerifyOk, hasEntitlement, hashFingerprint, isExpiredAt, parseLicenseKey };
|
||||
+503
@@ -0,0 +1,503 @@
|
||||
// src/verify.ts
|
||||
import * as ed from "@noble/ed25519";
|
||||
import { sha512 } from "@noble/hashes/sha512";
|
||||
|
||||
// src/errors.ts
|
||||
var LicensingError = class extends Error {
|
||||
/**
|
||||
* Machine-readable reason code. Common values:
|
||||
* `"bad_format"`, `"bad_encoding"`, `"bad_version"`, `"bad_signature"`,
|
||||
* `"expired"`, `"server_error"`, `"http_error"`, `"other"`.
|
||||
*/
|
||||
code;
|
||||
constructor(code, message) {
|
||||
super(message);
|
||||
this.name = "LicensingError";
|
||||
this.code = code;
|
||||
}
|
||||
};
|
||||
|
||||
// src/base32.ts
|
||||
var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
||||
var DECODE_TABLE = (() => {
|
||||
const t = {};
|
||||
for (let i = 0; i < ALPHABET.length; i++) t[ALPHABET[i]] = i;
|
||||
return t;
|
||||
})();
|
||||
function decodeBase32NoPad(input) {
|
||||
const up = input.toUpperCase();
|
||||
const out = new Uint8Array(Math.floor(up.length * 5 / 8));
|
||||
let bits = 0;
|
||||
let value = 0;
|
||||
let outPos = 0;
|
||||
for (let i = 0; i < up.length; i++) {
|
||||
const ch = up[i];
|
||||
const v = DECODE_TABLE[ch];
|
||||
if (v === void 0) {
|
||||
throw new LicensingError("bad_encoding", `invalid base32 character '${ch}'`);
|
||||
}
|
||||
value = value << 5 | v;
|
||||
bits += 5;
|
||||
if (bits >= 8) {
|
||||
bits -= 8;
|
||||
out[outPos++] = value >> bits & 255;
|
||||
}
|
||||
}
|
||||
return out.subarray(0, outPos);
|
||||
}
|
||||
|
||||
// src/key.ts
|
||||
var KEY_PREFIX = "LIC1";
|
||||
var KEY_VERSION_V1 = 1;
|
||||
var KEY_VERSION_V2 = 2;
|
||||
var KEY_VERSION = KEY_VERSION_V2;
|
||||
var FLAG_FINGERPRINT_BOUND = 1;
|
||||
var FLAG_TRIAL = 2;
|
||||
var PAYLOAD_V1_LEN = 74;
|
||||
var PAYLOAD_V2_HEAD_LEN = 83;
|
||||
var SIGNATURE_LEN = 64;
|
||||
function isExpiredAt(payload, nowUnixSeconds) {
|
||||
return payload.expiresAt !== 0 && nowUnixSeconds >= payload.expiresAt;
|
||||
}
|
||||
function hasEntitlement(payload, slug) {
|
||||
return payload.entitlements.includes(slug);
|
||||
}
|
||||
function parseLicenseKey(raw) {
|
||||
const trimmed = raw.trim();
|
||||
const firstDash = trimmed.indexOf("-");
|
||||
if (firstDash < 0) throw new LicensingError("bad_format", "key is missing prefix delimiter");
|
||||
const prefix = trimmed.slice(0, firstDash);
|
||||
if (prefix !== KEY_PREFIX) throw new LicensingError("bad_format", `unknown key prefix '${prefix}'`);
|
||||
const body = trimmed.slice(firstDash + 1);
|
||||
const lastDash = body.lastIndexOf("-");
|
||||
if (lastDash < 0) throw new LicensingError("bad_format", "key is missing signature delimiter");
|
||||
const payloadB32 = body.slice(0, lastDash);
|
||||
const signatureB32 = body.slice(lastDash + 1);
|
||||
const payloadBytes = decodeBase32NoPad(payloadB32);
|
||||
const signature = decodeBase32NoPad(signatureB32);
|
||||
if (signature.length !== SIGNATURE_LEN) {
|
||||
throw new LicensingError(
|
||||
"bad_format",
|
||||
`signature is ${signature.length} bytes; expected ${SIGNATURE_LEN}`
|
||||
);
|
||||
}
|
||||
if (payloadBytes.length < 1) {
|
||||
throw new LicensingError("bad_format", "empty payload");
|
||||
}
|
||||
const version = payloadBytes[0];
|
||||
let payload;
|
||||
switch (version) {
|
||||
case KEY_VERSION_V1:
|
||||
payload = parseV1(payloadBytes);
|
||||
break;
|
||||
case KEY_VERSION_V2:
|
||||
payload = parseV2(payloadBytes);
|
||||
break;
|
||||
default:
|
||||
throw new LicensingError("bad_version", `unsupported key version ${version}`);
|
||||
}
|
||||
return {
|
||||
payload,
|
||||
signedBytes: payloadBytes,
|
||||
signature
|
||||
};
|
||||
}
|
||||
function parseV1(payloadBytes) {
|
||||
if (payloadBytes.length !== PAYLOAD_V1_LEN) {
|
||||
throw new LicensingError(
|
||||
"bad_format",
|
||||
`v1 payload is ${payloadBytes.length} bytes; expected ${PAYLOAD_V1_LEN}`
|
||||
);
|
||||
}
|
||||
const flags = payloadBytes[1];
|
||||
const productId = payloadBytes.slice(2, 18);
|
||||
const licenseId = payloadBytes.slice(18, 34);
|
||||
const issuedAt = readBigEndianI64(payloadBytes, 34);
|
||||
const fingerprintHash = payloadBytes.slice(42, 74);
|
||||
return {
|
||||
version: KEY_VERSION_V1,
|
||||
flags,
|
||||
productId,
|
||||
licenseId,
|
||||
issuedAt,
|
||||
expiresAt: 0,
|
||||
fingerprintHash,
|
||||
entitlements: [],
|
||||
productUuid: uuidString(productId),
|
||||
licenseUuid: uuidString(licenseId),
|
||||
isFingerprintBound: (flags & FLAG_FINGERPRINT_BOUND) !== 0,
|
||||
isTrial: (flags & FLAG_TRIAL) !== 0
|
||||
};
|
||||
}
|
||||
function parseV2(payloadBytes) {
|
||||
if (payloadBytes.length < PAYLOAD_V2_HEAD_LEN) {
|
||||
throw new LicensingError(
|
||||
"bad_format",
|
||||
`v2 payload is ${payloadBytes.length} bytes; expected >= ${PAYLOAD_V2_HEAD_LEN}`
|
||||
);
|
||||
}
|
||||
const flags = payloadBytes[1];
|
||||
const productId = payloadBytes.slice(2, 18);
|
||||
const licenseId = payloadBytes.slice(18, 34);
|
||||
const issuedAt = readBigEndianI64(payloadBytes, 34);
|
||||
const expiresAt = readBigEndianI64(payloadBytes, 42);
|
||||
const fingerprintHash = payloadBytes.slice(50, 82);
|
||||
const numEntitlements = payloadBytes[82];
|
||||
const entitlements = [];
|
||||
let cursor = PAYLOAD_V2_HEAD_LEN;
|
||||
const decoder = new TextDecoder("utf-8", { fatal: true });
|
||||
for (let i = 0; i < numEntitlements; i++) {
|
||||
if (cursor >= payloadBytes.length) {
|
||||
throw new LicensingError("bad_format", "truncated entitlement list");
|
||||
}
|
||||
const len = payloadBytes[cursor];
|
||||
cursor += 1;
|
||||
if (cursor + len > payloadBytes.length) {
|
||||
throw new LicensingError("bad_format", "truncated entitlement");
|
||||
}
|
||||
try {
|
||||
entitlements.push(decoder.decode(payloadBytes.slice(cursor, cursor + len)));
|
||||
} catch {
|
||||
throw new LicensingError("bad_format", "entitlement not utf-8");
|
||||
}
|
||||
cursor += len;
|
||||
}
|
||||
if (cursor !== payloadBytes.length) {
|
||||
throw new LicensingError("bad_format", "trailing bytes in payload");
|
||||
}
|
||||
return {
|
||||
version: KEY_VERSION_V2,
|
||||
flags,
|
||||
productId,
|
||||
licenseId,
|
||||
issuedAt,
|
||||
expiresAt,
|
||||
fingerprintHash,
|
||||
entitlements,
|
||||
productUuid: uuidString(productId),
|
||||
licenseUuid: uuidString(licenseId),
|
||||
isFingerprintBound: (flags & FLAG_FINGERPRINT_BOUND) !== 0,
|
||||
isTrial: (flags & FLAG_TRIAL) !== 0
|
||||
};
|
||||
}
|
||||
function readBigEndianI64(buf, offset) {
|
||||
const view = new DataView(buf.buffer, buf.byteOffset + offset, 8);
|
||||
const hi = view.getInt32(0, false);
|
||||
const lo = view.getUint32(4, false);
|
||||
return hi * 2 ** 32 + lo;
|
||||
}
|
||||
function uuidString(b) {
|
||||
const h = Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
|
||||
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
||||
}
|
||||
|
||||
// src/fingerprint.ts
|
||||
import { sha256 } from "@noble/hashes/sha256";
|
||||
function hashFingerprint(raw) {
|
||||
return sha256(new TextEncoder().encode(raw));
|
||||
}
|
||||
|
||||
// src/verify.ts
|
||||
ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m));
|
||||
var Verifier = class {
|
||||
pubkey;
|
||||
constructor(pubkey) {
|
||||
this.pubkey = pubkey;
|
||||
}
|
||||
/** Verify a license key string. Throws on any failure. */
|
||||
verify(keyStr) {
|
||||
const key = parseLicenseKey(keyStr);
|
||||
const ok = ed.verify(key.signature, key.signedBytes, this.pubkey.raw);
|
||||
if (!ok) throw new LicensingError("bad_signature", "signature did not verify");
|
||||
return {
|
||||
payload: key.payload,
|
||||
licenseId: key.payload.licenseUuid,
|
||||
productId: key.payload.productUuid
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Verify AND enforce that, if the key is fingerprint-bound, the given
|
||||
* fingerprint matches. If the key is not bound, the fingerprint is
|
||||
* ignored. Throws on any failure.
|
||||
*/
|
||||
verifyWithFingerprint(keyStr, fingerprint) {
|
||||
const result = this.verify(keyStr);
|
||||
if (result.payload.isFingerprintBound) {
|
||||
const expected = hashFingerprint(fingerprint);
|
||||
const stored = result.payload.fingerprintHash;
|
||||
if (!equalBytes(expected, stored)) {
|
||||
throw new LicensingError("bad_signature", "fingerprint does not match bound key");
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* Verify a key and additionally reject it with an `expired` error if
|
||||
* `nowUnixSeconds` is at or past its `expiresAt`. Perpetual keys
|
||||
* (`expiresAt === 0`) are accepted regardless of `nowUnixSeconds`. This is
|
||||
* offline-only — no grace window logic; use `Client.validate` for that.
|
||||
*/
|
||||
verifyWithTime(keyStr, nowUnixSeconds) {
|
||||
const result = this.verify(keyStr);
|
||||
if (isExpiredAt(result.payload, nowUnixSeconds)) {
|
||||
throw new LicensingError("expired", "license has expired");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
function equalBytes(a, b) {
|
||||
if (a.length !== b.length) return false;
|
||||
let diff = 0;
|
||||
for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
|
||||
return diff === 0;
|
||||
}
|
||||
|
||||
// src/online.ts
|
||||
var Client = class {
|
||||
base;
|
||||
constructor(baseUrl) {
|
||||
this.base = baseUrl.replace(/\/+$/, "");
|
||||
}
|
||||
/** The normalized base URL this client is pinned to. */
|
||||
baseUrl() {
|
||||
return this.base;
|
||||
}
|
||||
/** Fetch the server's PEM-encoded public key. */
|
||||
async fetchPubkeyPem() {
|
||||
const data = await this.get("/v1/pubkey");
|
||||
return data.public_key_pem;
|
||||
}
|
||||
/**
|
||||
* Server-authoritative validation. Returns the full response including
|
||||
* expiry / entitlements / seat fields introduced in v2.
|
||||
*
|
||||
* Two-argument form kept for call-site compatibility with earlier SDK
|
||||
* versions; pass an options object for the full set of fields.
|
||||
*/
|
||||
async validate(key, productSlugOrOptions, fingerprint) {
|
||||
const opts = typeof productSlugOrOptions === "string" ? { productSlug: productSlugOrOptions, fingerprint } : productSlugOrOptions ?? {};
|
||||
const raw = await this.post("/v1/validate", {
|
||||
key,
|
||||
product_slug: opts.productSlug,
|
||||
fingerprint: opts.fingerprint,
|
||||
hostname: opts.hostname,
|
||||
platform: opts.platform
|
||||
});
|
||||
return this.toValidateResponse(raw);
|
||||
}
|
||||
/** Lightweight heartbeat. Server updates `last_heartbeat_at`. */
|
||||
async heartbeat(key, fingerprint) {
|
||||
const raw = await this.post("/v1/machines/heartbeat", {
|
||||
key,
|
||||
fingerprint
|
||||
});
|
||||
return this.toMachineResponse(raw);
|
||||
}
|
||||
/** Explicitly activate a seat for the given fingerprint. */
|
||||
async activate(key, fingerprint, opts = {}) {
|
||||
const raw = await this.post("/v1/machines/activate", {
|
||||
key,
|
||||
fingerprint,
|
||||
hostname: opts.hostname,
|
||||
platform: opts.platform
|
||||
});
|
||||
return this.toMachineResponse(raw);
|
||||
}
|
||||
/** Free a seat held by the given fingerprint. */
|
||||
async deactivate(key, fingerprint, reason) {
|
||||
const raw = await this.post("/v1/machines/deactivate", {
|
||||
key,
|
||||
fingerprint,
|
||||
reason
|
||||
});
|
||||
return this.toMachineResponse(raw);
|
||||
}
|
||||
/** Start a purchase. Returns the checkout URL and invoice id. */
|
||||
async startPurchase(productSlug, opts = {}) {
|
||||
const raw = await this.post("/v1/purchase", {
|
||||
product: productSlug,
|
||||
buyer_email: opts.buyerEmail,
|
||||
buyer_note: opts.buyerNote,
|
||||
redirect_url: opts.redirectUrl,
|
||||
code: opts.code
|
||||
});
|
||||
return {
|
||||
invoiceId: raw.invoice_id,
|
||||
btcpayInvoiceId: raw.btcpay_invoice_id,
|
||||
checkoutUrl: raw.checkout_url,
|
||||
amountSats: raw.amount_sats,
|
||||
pollUrl: raw.poll_url
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Redeem a `free_license` code: bypass BTCPay entirely and receive the
|
||||
* signed license key directly. Throws if the code is unknown / disabled
|
||||
* / expired / wrong product / not a free_license code, or if the cap
|
||||
* has been reached.
|
||||
*/
|
||||
async redeemFreeLicense(productSlug, code, opts = {}) {
|
||||
const raw = await this.post("/v1/redeem", {
|
||||
product: productSlug,
|
||||
code,
|
||||
buyer_email: opts.buyerEmail,
|
||||
buyer_note: opts.buyerNote
|
||||
});
|
||||
return {
|
||||
licenseId: raw.license_id,
|
||||
licenseKey: raw.license_key,
|
||||
invoiceId: raw.invoice_id,
|
||||
redemptionId: raw.redemption_id
|
||||
};
|
||||
}
|
||||
/** Poll a purchase by its invoice id. */
|
||||
async pollPurchase(invoiceId) {
|
||||
const raw = await this.get(
|
||||
`/v1/purchase/${encodeURIComponent(invoiceId)}`
|
||||
);
|
||||
return {
|
||||
invoiceId: raw.invoice_id,
|
||||
status: raw.status,
|
||||
productId: raw.product_id,
|
||||
amountSats: raw.amount_sats,
|
||||
licenseKey: raw.license_key ?? void 0,
|
||||
licenseId: raw.license_id ?? void 0
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Convenience: open the checkout, poll until a license key is issued,
|
||||
* then return it. Suitable for CLI usage or for an app UI that shows a
|
||||
* spinner while the buyer pays.
|
||||
*/
|
||||
async waitForLicense(invoiceId, options = {}) {
|
||||
const interval = options.intervalMs ?? 5e3;
|
||||
const deadline = options.timeoutMs ? Date.now() + options.timeoutMs : Infinity;
|
||||
while (true) {
|
||||
const poll = await this.pollPurchase(invoiceId);
|
||||
if (poll.licenseKey) return poll.licenseKey;
|
||||
if (poll.status === "expired" || poll.status === "invalid") {
|
||||
throw new LicensingError("server_error", `invoice ended in status ${poll.status}`);
|
||||
}
|
||||
if (Date.now() > deadline) {
|
||||
throw new LicensingError("server_error", "timed out waiting for license issuance");
|
||||
}
|
||||
await sleep(interval);
|
||||
}
|
||||
}
|
||||
// --- internals ---
|
||||
toValidateResponse(raw) {
|
||||
const entitlements = Array.isArray(raw.entitlements) ? raw.entitlements.filter((x) => typeof x === "string") : void 0;
|
||||
return {
|
||||
ok: !!raw.ok,
|
||||
reason: raw.reason,
|
||||
licenseId: raw.license_id,
|
||||
productId: raw.product_id,
|
||||
productSlug: raw.product_slug,
|
||||
issuedAt: raw.issued_at,
|
||||
expiresAt: raw.expires_at,
|
||||
graceUntil: raw.grace_until,
|
||||
inGracePeriod: raw.in_grace_period,
|
||||
isTrial: raw.is_trial,
|
||||
entitlements,
|
||||
status: raw.status,
|
||||
machineId: raw.machine_id,
|
||||
maxMachines: raw.max_machines
|
||||
};
|
||||
}
|
||||
toMachineResponse(raw) {
|
||||
return {
|
||||
ok: !!raw.ok,
|
||||
reason: raw.reason,
|
||||
machineId: raw.machine_id,
|
||||
activeCount: raw.active_count,
|
||||
maxMachines: raw.max_machines
|
||||
};
|
||||
}
|
||||
async get(path) {
|
||||
return this.request(path, { method: "GET" });
|
||||
}
|
||||
async post(path, body) {
|
||||
return this.request(path, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
}
|
||||
async request(path, init) {
|
||||
let resp;
|
||||
try {
|
||||
resp = await fetch(`${this.base}${path}`, init);
|
||||
} catch (e) {
|
||||
throw new LicensingError("http_error", e instanceof Error ? e.message : String(e));
|
||||
}
|
||||
const text = await resp.text();
|
||||
if (!resp.ok) {
|
||||
throw new LicensingError("server_error", `HTTP ${resp.status}: ${text}`);
|
||||
}
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch {
|
||||
throw new LicensingError("server_error", `non-JSON response: ${text}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
function sleep(ms) {
|
||||
return new Promise((res) => setTimeout(res, ms));
|
||||
}
|
||||
|
||||
// src/pubkey.ts
|
||||
var PublicKey = class _PublicKey {
|
||||
/** Raw 32-byte Ed25519 public key material. */
|
||||
raw;
|
||||
constructor(raw) {
|
||||
if (raw.length !== 32) {
|
||||
throw new LicensingError(
|
||||
"bad_format",
|
||||
`public key must be 32 bytes; got ${raw.length}`
|
||||
);
|
||||
}
|
||||
this.raw = raw;
|
||||
}
|
||||
/** Parse a PEM blob as emitted by the service. */
|
||||
static fromPem(pem) {
|
||||
const stripped = pem.replace(/-----BEGIN [^-]+-----/g, "").replace(/-----END [^-]+-----/g, "").replace(/\s+/g, "");
|
||||
if (!stripped) {
|
||||
throw new LicensingError("bad_format", "empty PEM input");
|
||||
}
|
||||
const der = base64Decode(stripped);
|
||||
if (der.length < 32) {
|
||||
throw new LicensingError("bad_format", "PEM body too short to contain a public key");
|
||||
}
|
||||
const raw = der.slice(der.length - 32);
|
||||
return new _PublicKey(raw);
|
||||
}
|
||||
/** Construct from raw bytes (no PEM envelope). */
|
||||
static fromBytes(bytes) {
|
||||
return new _PublicKey(bytes);
|
||||
}
|
||||
};
|
||||
function base64Decode(b64) {
|
||||
const nodeBuffer = globalThis.Buffer;
|
||||
if (nodeBuffer) {
|
||||
return new Uint8Array(nodeBuffer.from(b64, "base64"));
|
||||
}
|
||||
const bin = atob(b64);
|
||||
const out = new Uint8Array(bin.length);
|
||||
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
||||
return out;
|
||||
}
|
||||
export {
|
||||
Client,
|
||||
FLAG_FINGERPRINT_BOUND,
|
||||
FLAG_TRIAL,
|
||||
KEY_PREFIX,
|
||||
KEY_VERSION,
|
||||
KEY_VERSION_V1,
|
||||
KEY_VERSION_V2,
|
||||
LicensingError,
|
||||
PublicKey,
|
||||
Verifier,
|
||||
hasEntitlement,
|
||||
hashFingerprint,
|
||||
isExpiredAt,
|
||||
parseLicenseKey
|
||||
};
|
||||
Reference in New Issue
Block a user