Files
recap/vendor/keysat-licensing-client/dist/index.d.cts
T
Keysat 495b4aef36 Fix Dockerfile to copy all server/*.js modules; refresh vendor to v0.2.0
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).
2026-05-09 11:57:41 -05:00

378 lines
14 KiB
TypeScript

/**
* 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;
/**
* Optional tier slug (the policy the buyer chose). When set, the
* licensing service prices the invoice at the policy's
* `price_sats_override` and remembers the chosen policy on the
* invoice so the issued license carries that policy's
* entitlements / duration / max_machines / trial flag.
*
* When omitted, the service falls back to the product's default
* policy (the policy slugged "default", or the first active one).
*
* To list available tiers for a product without auth, see
* {@link Client.listPublicPolicies}.
*/
policySlug?: string;
}
/**
* One tier on the buyer-facing tier picker. Returned by
* {@link Client.listPublicPolicies}. The shape mirrors what the
* licensing service's `/buy/<slug>` page reads server-side, so an
* in-app tier picker can render identical text and pricing without
* the buyer ever leaving the app.
*/
interface PublicPolicy {
slug: string;
name: string;
/** Free-form per-tier blurb (operator-set in admin UI). May be empty. */
description: string;
/**
* Effective price in the smallest unit of the product's listed
* currency: sats for SAT-priced products, cents for USD/EUR-priced
* products. The product-level currency is on the parent
* {@link PublicPoliciesResponse.product.basePriceSats} (sats only) and
* via the daemon's `/v1/products/<slug>` endpoint for the full
* currency-typed view.
*/
priceSats: number;
/** 0 = perpetual; otherwise license lifetime in seconds. */
durationSeconds: number;
/** Seat cap. 0 = unlimited, 1 = single-seat, n = n-seat. */
maxMachines: number;
isTrial: boolean;
entitlements: string[];
/** True if the operator marked this tier as "Most popular". */
highlighted: boolean;
/** True if the policy is a recurring subscription. */
isRecurring: boolean;
/** Renewal cadence in days (0 for non-recurring). */
renewalPeriodDays: number;
/** First-cycle free-trial length (0 for none). */
trialDays: number;
}
interface PublicPoliciesResponse {
product: {
slug: string;
name: string;
description: string;
basePriceSats: number;
};
policies: PublicPolicy[];
}
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>;
/**
* 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.
*/
listPublicPolicies(productSlug: string): Promise<PublicPoliciesResponse>;
/**
* 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 PublicPoliciesResponse, type PublicPolicy, type PurchaseSession, type RedeemFreeOptions, type RedeemFreeResponse, type StartPurchaseOptions, type ValidateOptions, type ValidateResponse, Verifier, type VerifyOk, hasEntitlement, hashFingerprint, isExpiredAt, parseLicenseKey };