"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
});
