495b4aef36
The runtime crash on v0.2.3:
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/app/server/util.js'
imported from /app/server/index.js
happened because the Dockerfile's stage-2 COPY only listed server/
index.js + server/license.js explicitly. When I started extracting
modules in v0.2.3 (util.js, gemini-helpers.js, audio.js, ytdlp.js,
cookies.js, config.js, license-middleware.js, history.js, library.js)
I forgot to update the COPY list, so those files were never copied
into the runner image. Local 'node' tests passed because the modules
exist on disk; the .s9pk container had only the two original files
and crashed on first import.
Fix:
COPY server/*.js ./server/
Glob picks up all top-level .js files automatically, including any
future extractions, while still skipping server/test/ and server/
node_modules/. This is the simplest forward-compatible form.
Bonus: refresh the vendored @keysat/licensing-client from 0.1.0 to
0.2.0. The new SDK adds:
• policySlug field on StartPurchaseOptions (so we can drive Core/
Pro tier selection programmatically from our backend)
• client.listPublicPolicies(productSlug) for fetching the tier
cards' data without auth
Both are prerequisites for the in-app buy flow planned in
~/.claude/plans/in-app-buy-flow.md. The vendor's own node_modules
(@noble/ed25519, @noble/hashes) is gitignored as before — Docker
builds re-install via `npm install --omit=dev --ignore-scripts` in
the vendor dir during stage 1.
Also includes the license-middleware update from earlier in the day:
a 30s license-file poll so a key set via the "Set Recap License"
StartOS action is picked up within seconds (instead of waiting for
the 6h scheduled validateOnline tick).
595 lines
19 KiB
JavaScript
595 lines
19 KiB
JavaScript
"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,
|
|
policy_slug: opts.policySlug
|
|
});
|
|
return {
|
|
invoiceId: raw.invoice_id,
|
|
btcpayInvoiceId: raw.btcpay_invoice_id,
|
|
checkoutUrl: raw.checkout_url,
|
|
amountSats: raw.amount_sats,
|
|
pollUrl: raw.poll_url
|
|
};
|
|
}
|
|
/**
|
|
* List public, buyer-visible policies (tiers) for a product. No
|
|
* auth — same data the licensing service's `/buy/<slug>` page
|
|
* uses server-side. Use this to render an in-app tier picker
|
|
* that stays in sync with the operator's admin-side tier setup.
|
|
*
|
|
* Returns each policy's slug, display name, price (in the
|
|
* product's listed currency's smallest unit — sats or cents),
|
|
* entitlements, recurring/trial flags. Internal fields (id,
|
|
* tip recipients, raw metadata) are deliberately omitted.
|
|
*/
|
|
async listPublicPolicies(productSlug) {
|
|
const raw = await this.get(
|
|
`/v1/products/${encodeURIComponent(productSlug)}/policies`
|
|
);
|
|
const product = raw.product;
|
|
const policies = raw.policies ?? [];
|
|
return {
|
|
product: {
|
|
slug: product.slug,
|
|
name: product.name,
|
|
description: product.description ?? "",
|
|
basePriceSats: product.base_price_sats
|
|
},
|
|
policies: policies.map((p) => ({
|
|
slug: p.slug,
|
|
name: p.name,
|
|
description: p.description ?? "",
|
|
priceSats: p.price_sats,
|
|
durationSeconds: p.duration_seconds ?? 0,
|
|
maxMachines: p.max_machines ?? 1,
|
|
isTrial: !!p.is_trial,
|
|
entitlements: p.entitlements ?? [],
|
|
highlighted: !!p.highlighted,
|
|
isRecurring: !!p.is_recurring,
|
|
renewalPeriodDays: p.renewal_period_days ?? 0,
|
|
trialDays: p.trial_days ?? 0
|
|
}))
|
|
};
|
|
}
|
|
/**
|
|
* 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
|
|
});
|