@unitpay/sdk
Version:
Official UnitPay SDK for TypeScript — Node, browser, and edge. Portal sessions, subscriptions, invoices, credits, usage tracking.
528 lines (516 loc) • 17.1 kB
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, {
ApiError: () => ApiError,
AuthenticationError: () => AuthenticationError,
BadRequestError: () => BadRequestError,
ConflictError: () => ConflictError,
ConnectionError: () => ConnectionError,
Customers: () => Customers,
InternalServerError: () => InternalServerError,
NotFoundError: () => NotFoundError,
PagePromise: () => PagePromise,
PortalSessions: () => PortalSessions,
RateLimitError: () => RateLimitError,
SDK_VERSION: () => SDK_VERSION,
Subscriptions: () => Subscriptions,
TimeoutError: () => TimeoutError,
UnitPay: () => UnitPay,
Usage: () => Usage,
ValidationError: () => ValidationError
});
module.exports = __toCommonJS(index_exports);
// src/case-convert.ts
var toCamelCase = (str) => str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
var deepCamelCase = (obj) => {
if (obj === null || obj === void 0) return obj;
if (typeof obj !== "object") return obj;
if (Array.isArray(obj)) return obj.map(deepCamelCase);
const result = {};
for (const [key, value] of Object.entries(obj)) {
if (key === "metadata" || key === "properties") {
result[toCamelCase(key)] = value;
} else {
result[toCamelCase(key)] = deepCamelCase(value);
}
}
return result;
};
// src/errors.ts
var ApiError = class _ApiError extends Error {
status;
code;
requestId;
headers;
constructor(params) {
super(params.message);
this.name = "ApiError";
this.status = params.status;
this.code = params.code;
this.requestId = params.requestId;
this.headers = params.headers;
}
toString() {
return `${this.name} [${this.status}] ${this.code}: ${this.message} (requestId: ${this.requestId ?? "none"})`;
}
static generate(status, body, headers) {
const code = body?.error?.code ?? "unknown_error";
const message = body?.error?.message ?? `Request failed with status ${status}`;
const requestId = headers.get("x-request-id");
const headerRecord = {};
headers.forEach((value, key) => {
headerRecord[key] = value;
});
const params = { status, code, message, requestId, headers: headerRecord };
switch (status) {
case 400:
return new BadRequestError(params);
case 401:
return new AuthenticationError(params);
case 404:
return new NotFoundError(params);
case 409:
return new ConflictError(params);
case 422:
return new ValidationError(params);
case 429: {
const retryAfterHeader = headers.get("retry-after");
const retryAfter = retryAfterHeader ? Number.parseInt(retryAfterHeader, 10) : null;
return new RateLimitError({ ...params, retryAfter });
}
default:
if (status >= 500) return new InternalServerError(params);
return new _ApiError(params);
}
}
};
var BadRequestError = class extends ApiError {
constructor(params) {
super(params);
this.name = "BadRequestError";
}
};
var AuthenticationError = class extends ApiError {
constructor(params) {
super(params);
this.name = "AuthenticationError";
}
};
var NotFoundError = class extends ApiError {
constructor(params) {
super(params);
this.name = "NotFoundError";
}
};
var ConflictError = class extends ApiError {
constructor(params) {
super(params);
this.name = "ConflictError";
}
};
var ValidationError = class extends ApiError {
constructor(params) {
super(params);
this.name = "ValidationError";
}
};
var RateLimitError = class extends ApiError {
retryAfter;
constructor(params) {
super(params);
this.name = "RateLimitError";
this.retryAfter = params.retryAfter;
}
};
var InternalServerError = class extends ApiError {
constructor(params) {
super(params);
this.name = "InternalServerError";
}
};
var ConnectionError = class extends ApiError {
constructor(message) {
super({ status: 0, code: "connection_error", message, requestId: null, headers: {} });
this.name = "ConnectionError";
}
};
var TimeoutError = class extends ApiError {
constructor(timeoutMs) {
super({
status: 0,
code: "timeout_error",
message: `Request timed out after ${timeoutMs}ms`,
requestId: null,
headers: {}
});
this.name = "TimeoutError";
}
};
// src/pagination.ts
var PagePromise = class {
fetchPage;
initialPromise;
constructor(fetchPage) {
this.fetchPage = fetchPage;
this.initialPromise = fetchPage();
}
// biome-ignore lint/suspicious/noThenProperty: intentional PromiseLike implementation (Stripe SDK pattern)
then(onfulfilled, onrejected) {
return this.initialPromise.then(onfulfilled, onrejected);
}
catch(onrejected) {
return this.initialPromise.catch(onrejected);
}
async *[Symbol.asyncIterator]() {
let page = await this.initialPromise;
for (const item of page.data) yield item;
while (page.hasMore && page.nextCursor) {
page = await this.fetchPage(page.nextCursor);
for (const item of page.data) yield item;
}
}
async toArray() {
const items = [];
for await (const item of this) items.push(item);
return items;
}
};
// src/resource.ts
var ApiResource = class {
client;
constructor(client) {
this.client = client;
}
_get(path, query, options) {
return this.client.request("GET", path, void 0, { ...options, query });
}
_post(path, body, options) {
return this.client.request("POST", path, body, options);
}
_put(path, body, options) {
return this.client.request("PUT", path, body, options);
}
_patch(path, body, options) {
return this.client.request("PATCH", path, body, options);
}
_delete(path, options) {
return this.client.request("DELETE", path, void 0, options);
}
_getPage(path, params) {
return new PagePromise((cursor) => {
const query = { ...params };
if (cursor) query.cursor = cursor;
return this.client.request("GET", path, void 0, { query });
});
}
};
// src/resources/customers.ts
var Customers = class extends ApiResource {
create(params, options) {
return this._post("/customers", params, options);
}
get(id, options) {
return this._get(`/customers/${id}`, void 0, options);
}
list(params) {
return this._getPage("/customers", params);
}
update(id, params, options) {
return this._patch(`/customers/${id}`, params, options);
}
archive(id, options) {
return this._post(`/customers/${id}/archive`, void 0, options);
}
listInvoices(id, params) {
return this._getPage(`/customers/${id}/invoices`, params);
}
listPaymentMethods(id, params) {
return this._getPage(`/customers/${id}/payment-methods`, params);
}
listCreditAccounts(id, params) {
return this._getPage(`/customers/${id}/credit-accounts`, params);
}
/** Active entitlement matrix for the customer. */
entitlements(id, params) {
return this._getPage(`/customers/${id}/entitlements`, params);
}
/** Does this customer have access to `featureSlug` right now? */
check(id, params, options) {
return this._post(`/customers/${id}/check`, params, options);
}
/** Resolve up to 50 feature slugs in one round trip. */
checkBatch(id, featureSlugs, options) {
return this._post(
`/customers/${id}/check/batch`,
{ featureSlugs },
options
);
}
};
// src/resources/portal-sessions.ts
var PortalSessions = class extends ApiResource {
create(params, options) {
return this._post("/portal-sessions", params, options);
}
revoke(params, options) {
return this._post("/portal-sessions/revoke", params, options);
}
};
// src/resources/subscriptions.ts
var Subscriptions = class extends ApiResource {
create(params, options) {
return this._post("/subscriptions", params, options);
}
get(id, options) {
return this._get(`/subscriptions/${id}`, void 0, options);
}
list(params) {
return this._getPage("/subscriptions", params);
}
change(id, params, options) {
return this._post(`/subscriptions/${id}/change`, params, options);
}
cancel(id, params, options) {
return this._post(`/subscriptions/${id}/cancel`, params, options);
}
uncancel(id, options) {
return this._post(`/subscriptions/${id}/uncancel`, void 0, options);
}
};
// src/resources/usage.ts
var Usage = class extends ApiResource {
track(params, options) {
const body = Array.isArray(params) ? { events: params } : params;
return this._post("/usage/track", body, options);
}
};
// src/version.ts
var SDK_VERSION = "1.32.0";
// src/client.ts
var DEFAULT_BASE_URL = "https://api.useunitpay.com/v1";
var DEFAULT_TIMEOUT = 3e4;
var DEFAULT_RETRIES = 2;
var SDK_USER_AGENT = `unitpay-node/${SDK_VERSION}`;
var readEnv = (key) => {
if (typeof process !== "undefined" && process.env) {
return process.env[key];
}
return;
};
var UnitPay = class {
apiKey;
baseUrl;
timeout;
maxRetries;
fetchFn;
idempotencyKeyPrefix;
listeners = /* @__PURE__ */ new Map();
// ── Lazy-loaded resources ────────────────────────────────────────────────
_portalSessions;
get portalSessions() {
this._portalSessions ??= new PortalSessions(this);
return this._portalSessions;
}
_customers;
get customers() {
this._customers ??= new Customers(this);
return this._customers;
}
_subscriptions;
get subscriptions() {
this._subscriptions ??= new Subscriptions(this);
return this._subscriptions;
}
_usage;
get usage() {
this._usage ??= new Usage(this);
return this._usage;
}
constructor(config) {
const apiKey = config?.apiKey ?? readEnv("UNITPAY_API_KEY");
if (!apiKey) {
throw new Error(
"UnitPay: `apiKey` is required. Pass it in the config or set UNITPAY_API_KEY in the environment. Portal tokens are not supported here \u2014 use `@unitpay/react` for customer-facing flows."
);
}
if (config?.portalToken !== void 0) {
throw new Error(
"UnitPay: `portalToken` is not a valid option for the server SDK. Portal tokens belong in `@unitpay/react`; pass them to `<UnitPayProvider portalToken={\u2026}>`."
);
}
this.apiKey = apiKey;
this.baseUrl = config?.baseUrl ?? DEFAULT_BASE_URL;
this.timeout = config?.timeout ?? DEFAULT_TIMEOUT;
this.maxRetries = config?.retries ?? DEFAULT_RETRIES;
this.fetchFn = config?.fetch ?? globalThis.fetch;
this.idempotencyKeyPrefix = config?.idempotencyKeyPrefix ?? "";
}
async track(...args) {
return this.usage.track(...args);
}
/**
* Entitlement gate — "does this customer have access to `featureSlug` right
* now?" Read-only; deductions happen via `track`. `POST /check`.
*/
check(params, options) {
return this.request("POST", "/check", params, options);
}
on(event, callback) {
if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
this.listeners.get(event).add(callback);
return this;
}
off(event, callback) {
this.listeners.get(event)?.delete(callback);
return this;
}
emit(event, data) {
this.listeners.get(event)?.forEach((cb) => cb(data));
}
async request(method, path, body, options) {
const url = new URL(`${this.baseUrl}${path}`);
if (options?.query) {
for (const [key, value] of Object.entries(options.query)) {
if (value !== void 0 && value !== null) {
url.searchParams.set(key, String(value));
}
}
}
const headers = {
Authorization: `Bearer ${this.apiKey}`,
"X-UnitPay-Client": SDK_USER_AGENT
};
if (body !== void 0) {
headers["Content-Type"] = "application/json";
}
if ((method === "POST" || method === "DELETE") && this.maxRetries > 0) {
const idempotencyKey = options?.idempotencyKey ?? `${this.idempotencyKeyPrefix}${crypto.randomUUID()}`;
headers["Idempotency-Key"] = idempotencyKey;
}
const requestBody = body ? JSON.stringify(body) : void 0;
let lastError;
const userSignal = options?.signal;
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
this.emit("request", { method, path, attempt });
let timeoutId;
try {
const timeoutMs = options?.timeout ?? this.timeout;
let controller;
if (!userSignal) {
controller = new AbortController();
timeoutId = setTimeout(() => controller.abort(), timeoutMs);
}
const signal = userSignal ?? controller.signal;
const response = await this.fetchFn(url.toString(), {
method,
headers,
body: requestBody,
signal
});
if (timeoutId !== void 0) clearTimeout(timeoutId);
const requestId = response.headers.get("x-request-id");
this.emit("response", { status: response.status, requestId });
if (response.ok) {
const text = await response.text();
if (!text) return void 0;
const json = JSON.parse(text);
return deepCamelCase(json);
}
let errorBody = null;
try {
errorBody = await response.json();
} catch {
}
const error = ApiError.generate(response.status, errorBody, response.headers);
const isRetryable = response.status === 429 || response.status === 409 || response.status >= 500;
if (!isRetryable || attempt === this.maxRetries) {
throw error;
}
lastError = error;
let backoffMs;
if (error instanceof RateLimitError && error.retryAfter !== null) {
backoffMs = Math.min(error.retryAfter * 1e3, 6e4);
} else {
const base = 500 * 2 ** attempt;
const jitter = 0.5 + Math.random() * 0.5;
backoffMs = Math.min(base * jitter, 1e4);
}
await new Promise((resolve) => setTimeout(resolve, backoffMs));
} catch (error) {
if (timeoutId !== void 0) clearTimeout(timeoutId);
if (error instanceof ApiError) throw error;
if (error instanceof DOMException && error.name === "AbortError") {
if (userSignal?.aborted) throw error;
throw new TimeoutError(options?.timeout ?? this.timeout);
}
if (attempt === this.maxRetries) {
throw error instanceof Error ? new ConnectionError(error.message) : new ConnectionError("Network request failed");
}
lastError = error instanceof Error ? error : new Error(String(error));
const base = 500 * 2 ** attempt;
const jitter = 0.5 + Math.random() * 0.5;
const backoffMs = Math.min(base * jitter, 1e4);
await new Promise((resolve) => setTimeout(resolve, backoffMs));
}
}
throw lastError ?? new ConnectionError("Request failed after all retries");
}
static async verifyWebhook(body, headers, secret) {
const { Webhook } = await import("svix");
const webhook = new Webhook(secret);
const event = webhook.verify(body, headers);
return deepCamelCase(event);
}
get redactedApiKey() {
if (this.apiKey.length <= 16) return "***";
return `${this.apiKey.slice(0, 12)}...${this.apiKey.slice(-4)}`;
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
ApiError,
AuthenticationError,
BadRequestError,
ConflictError,
ConnectionError,
Customers,
InternalServerError,
NotFoundError,
PagePromise,
PortalSessions,
RateLimitError,
SDK_VERSION,
Subscriptions,
TimeoutError,
UnitPay,
Usage,
ValidationError
});