@unitpay/sdk
Version:
Official Node.js SDK for UnitPay - usage-based billing, invoicing, and analytics
1,491 lines (1,478 loc) • 45.9 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,
AuthorizationError: () => AuthorizationError,
ConflictError: () => ConflictError,
InitializationError: () => InitializationError,
InternalError: () => InternalError,
NetworkError: () => NetworkError,
NotFoundError: () => NotFoundError,
RateLimitError: () => RateLimitError,
RevMaxApiError: () => RevMaxApiError,
RevMaxAuthenticationError: () => RevMaxAuthenticationError,
RevMaxClient: () => UnitPayClient,
RevMaxError: () => RevMaxError,
RevMaxInitializationError: () => RevMaxInitializationError,
RevMaxNotFoundError: () => RevMaxNotFoundError,
RevMaxRateLimitError: () => RevMaxRateLimitError,
RevMaxValidationError: () => RevMaxValidationError,
TimeoutError: () => TimeoutError,
UnitPayClient: () => UnitPayClient,
UnitPayError: () => UnitPayError,
ValidationError: () => ValidationError,
createErrorFromResponse: () => createErrorFromResponse,
isPublishableKey: () => isPublishableKey,
isSecretKey: () => isSecretKey,
parseApiError: () => parseApiError,
parseApiKey: () => parseApiKey
});
module.exports = __toCommonJS(index_exports);
// src/utils/http.ts
var import_axios = __toESM(require("axios"));
// src/errors.ts
var UnitPayError = class _UnitPayError extends Error {
constructor(message, statusCode = 500, code = "UNITPAY_ERROR", details, metadata = {}) {
super(message);
this.name = "UnitPayError";
this.statusCode = statusCode;
this.code = code;
this.details = details;
this.metadata = metadata;
this.requestId = metadata.requestId;
Object.setPrototypeOf(this, _UnitPayError.prototype);
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
};
var AuthenticationError = class _AuthenticationError extends UnitPayError {
constructor(message = "Invalid or missing API key", metadata = {}) {
super(message, 401, "AUTHENTICATION_ERROR", void 0, metadata);
this.name = "AuthenticationError";
Object.setPrototypeOf(this, _AuthenticationError.prototype);
}
};
var AuthorizationError = class _AuthorizationError extends UnitPayError {
constructor(message = "You do not have permission to perform this action", metadata = {}) {
super(message, 403, "AUTHORIZATION_ERROR", void 0, metadata);
this.name = "AuthorizationError";
Object.setPrototypeOf(this, _AuthorizationError.prototype);
}
};
var NotFoundError = class _NotFoundError extends UnitPayError {
constructor(message = "Resource not found", resourceType, resourceId, metadata = {}) {
super(message, 404, "NOT_FOUND_ERROR", void 0, metadata);
this.name = "NotFoundError";
this.resourceType = resourceType;
this.resourceId = resourceId;
Object.setPrototypeOf(this, _NotFoundError.prototype);
}
};
var ValidationError = class _ValidationError extends UnitPayError {
constructor(message = "Validation failed", validationErrors, metadata = {}) {
super(message, 400, "VALIDATION_ERROR", validationErrors, metadata);
this.name = "ValidationError";
this.validationErrors = validationErrors;
Object.setPrototypeOf(this, _ValidationError.prototype);
}
};
var RateLimitError = class _RateLimitError extends UnitPayError {
constructor(message = "Rate limit exceeded", retryAfter, metadata = {}) {
super(message, 429, "RATE_LIMIT_ERROR", void 0, metadata);
this.name = "RateLimitError";
this.retryAfter = retryAfter;
Object.setPrototypeOf(this, _RateLimitError.prototype);
}
};
var ConflictError = class _ConflictError extends UnitPayError {
constructor(message = "Resource conflict", metadata = {}) {
super(message, 409, "CONFLICT_ERROR", void 0, metadata);
this.name = "ConflictError";
Object.setPrototypeOf(this, _ConflictError.prototype);
}
};
var NetworkError = class _NetworkError extends UnitPayError {
constructor(message = "Network error occurred", originalError, metadata = {}) {
super(message, 0, "NETWORK_ERROR", void 0, metadata);
this.name = "NetworkError";
this.originalError = originalError;
Object.setPrototypeOf(this, _NetworkError.prototype);
}
};
var TimeoutError = class _TimeoutError extends UnitPayError {
constructor(message = "Request timed out", timeoutMs, metadata = {}) {
super(message, 0, "TIMEOUT_ERROR", void 0, metadata);
this.name = "TimeoutError";
this.timeoutMs = timeoutMs;
Object.setPrototypeOf(this, _TimeoutError.prototype);
}
};
var InternalError = class _InternalError extends UnitPayError {
constructor(message = "Internal server error", metadata = {}) {
super(message, 500, "INTERNAL_ERROR", void 0, metadata);
this.name = "InternalError";
Object.setPrototypeOf(this, _InternalError.prototype);
}
};
var InitializationError = class _InitializationError extends UnitPayError {
constructor(message = "Failed to initialize the SDK", metadata = {}) {
super(message, 0, "INITIALIZATION_ERROR", void 0, metadata);
this.name = "InitializationError";
Object.setPrototypeOf(this, _InitializationError.prototype);
}
};
var ApiError = class _ApiError extends UnitPayError {
constructor(message = "API request failed", statusCode, errorCode, metadata = {}) {
super(message, statusCode || 500, errorCode || "API_ERROR", void 0, metadata);
this.name = "ApiError";
this.errorCode = errorCode;
Object.setPrototypeOf(this, _ApiError.prototype);
}
};
var RevMaxError = UnitPayError;
var RevMaxApiError = ApiError;
var RevMaxAuthenticationError = AuthenticationError;
var RevMaxRateLimitError = RateLimitError;
var RevMaxValidationError = ValidationError;
var RevMaxNotFoundError = NotFoundError;
var RevMaxInitializationError = InitializationError;
function createErrorFromResponse(statusCode, message, details, metadata = {}) {
switch (statusCode) {
case 400:
return new ValidationError(message, details, metadata);
case 401:
return new AuthenticationError(message, metadata);
case 403:
return new AuthorizationError(message, metadata);
case 404:
return new NotFoundError(message, void 0, void 0, metadata);
case 409:
return new ConflictError(message, metadata);
case 422:
return new ValidationError(message, details, metadata);
case 429:
const retryAfter = metadata.retryAfter;
return new RateLimitError(message, retryAfter, metadata);
case 500:
case 502:
case 503:
case 504:
return new InternalError(message, metadata);
default:
return new ApiError(message, statusCode, "API_ERROR", metadata);
}
}
function parseApiError(error) {
const err = error;
const message = err.message || "Unknown error";
const statusCode = err.response?.status;
const responseData = err.response?.data || {};
const requestId = err.config?.headers?.["X-Request-ID"] || "unknown";
const metadata = {
requestId,
url: err.config?.url,
method: err.config?.method,
responseData,
retryAfter: err.response?.headers?.["retry-after"] ? parseInt(err.response.headers["retry-after"], 10) : void 0
};
if (!statusCode) {
if (err.code === "ECONNABORTED") {
return new TimeoutError(message, void 0, metadata);
}
return new NetworkError(message, err, metadata);
}
return createErrorFromResponse(
statusCode,
responseData.message || responseData.error || message,
responseData.errors,
metadata
);
}
// src/utils/http.ts
var DEFAULT_BASE_URL = "https://api-nest-mu.vercel.app/v1";
var DEFAULT_TIMEOUT = 3e4;
var DEFAULT_RETRIES = 0;
var DEFAULT_RETRY_DELAY = 300;
var Logger = class {
constructor(config) {
this.levelPriority = {
debug: 0,
info: 1,
warn: 2,
error: 3
};
this.defaultHandler = (level, message, data) => {
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
const logMessage = `[${timestamp}] [UnitPay] [${level.toUpperCase()}] ${message}`;
const logFn = level === "error" ? console.error : level === "warn" ? console.warn : console.log;
if (data) {
logFn(logMessage, data);
} else {
logFn(logMessage);
}
};
this.enabled = config.logging?.enabled ?? config.debug ?? false;
this.minLevel = config.logging?.level ?? "info";
this.handler = config.logging?.handler ?? this.defaultHandler;
}
shouldLog(level) {
return this.enabled && this.levelPriority[level] >= this.levelPriority[this.minLevel];
}
debug(message, data) {
if (this.shouldLog("debug")) this.handler("debug", message, data);
}
info(message, data) {
if (this.shouldLog("info")) this.handler("info", message, data);
}
warn(message, data) {
if (this.shouldLog("warn")) this.handler("warn", message, data);
}
error(message, data) {
if (this.shouldLog("error")) this.handler("error", message, data);
}
};
var Telemetry = class {
constructor(config) {
this.activeRequests = /* @__PURE__ */ new Map();
this.requestCount = 0;
this.successCount = 0;
this.errorCount = 0;
this.totalDuration = 0;
this.errorTypes = /* @__PURE__ */ new Map();
this.enabled = config.telemetry?.enabled ?? false;
this.sampleRate = config.telemetry?.sampleRate ?? 1;
this.handler = config.telemetry?.handler ?? (() => {
});
}
shouldCollect() {
return this.enabled && Math.random() <= this.sampleRate;
}
generateRequestId() {
return `req_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
}
normalizePath(path) {
if (!path) return "";
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
return normalizedPath.replace(/\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "/:id").replace(/\/\d+(?=\/|$)/g, "/:id");
}
startRequest(method, path, requestId) {
const shouldTrack = this.shouldCollect();
const id = requestId || this.generateRequestId();
if (shouldTrack) {
const metrics = {
requestId: id,
method: method.toUpperCase(),
path: this.normalizePath(path),
startTime: Date.now()
};
this.activeRequests.set(id, metrics);
}
return { requestId: id, isTracked: shouldTrack };
}
endRequest(requestId, statusCode, error, retryCount) {
const metrics = this.activeRequests.get(requestId);
if (!metrics) return;
metrics.endTime = Date.now();
metrics.duration = metrics.endTime - metrics.startTime;
metrics.statusCode = statusCode;
metrics.success = !error && (statusCode ? statusCode < 400 : true);
metrics.retryCount = retryCount || 0;
if (error) {
metrics.errorMessage = error.message;
metrics.errorType = error.name || "UnknownError";
const currentCount = this.errorTypes.get(metrics.errorType) || 0;
this.errorTypes.set(metrics.errorType, currentCount + 1);
this.errorCount++;
} else {
this.successCount++;
}
this.requestCount++;
this.totalDuration += metrics.duration;
this.handler(metrics);
this.activeRequests.delete(requestId);
}
getStats() {
return {
requestCount: this.requestCount,
successCount: this.successCount,
errorCount: this.errorCount,
successRate: this.requestCount > 0 ? this.successCount / this.requestCount : 1,
averageDuration: this.requestCount > 0 ? this.totalDuration / this.requestCount : 0,
errorBreakdown: Object.fromEntries(this.errorTypes)
};
}
resetStats() {
this.requestCount = 0;
this.successCount = 0;
this.errorCount = 0;
this.totalDuration = 0;
this.errorTypes.clear();
}
};
function calculateBackoff(retryCount, initialDelay) {
return initialDelay * Math.pow(2, retryCount);
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function isRetryable(error) {
if (error.response) {
const status = error.response.status;
return status === 429 || status >= 500 && status < 600;
}
return !error.response;
}
var HttpClient = class {
constructor(config) {
this.logger = new Logger(config);
this.telemetry = new Telemetry(config);
this.retries = config.retries ?? DEFAULT_RETRIES;
this.retryDelay = config.retryDelay ?? DEFAULT_RETRY_DELAY;
const baseURL = config.baseUrl || config.baseURL || DEFAULT_BASE_URL;
const isBrowser = typeof globalThis !== "undefined" && typeof globalThis.window !== "undefined";
const headers = {
"Content-Type": "application/json",
Accept: "application/json",
"x-api-key": config.apiKey,
...config.headers
};
if (!isBrowser) {
headers["User-Agent"] = "unitpay-node/1.0.0";
}
this.client = import_axios.default.create({
baseURL,
timeout: config.timeout || DEFAULT_TIMEOUT,
headers
});
this.client.interceptors.request.use(
(requestConfig) => {
const customConfig = requestConfig;
customConfig.metadata = customConfig.metadata || {};
const { requestId, isTracked } = this.telemetry.startRequest(
requestConfig.method || "GET",
requestConfig.url || "",
customConfig.metadata.requestId
);
requestConfig.headers = requestConfig.headers || {};
requestConfig.headers["X-Request-ID"] = requestId;
customConfig.metadata.requestId = requestId;
customConfig.metadata.telemetryTracked = isTracked;
this.logger.debug(`Request: ${requestConfig.method?.toUpperCase()} ${requestConfig.url}`, {
requestId
});
return requestConfig;
},
(error) => Promise.reject(error)
);
this.client.interceptors.response.use(
(response) => {
const customConfig = response.config;
const requestId = customConfig.metadata?.requestId;
const isTracked = customConfig.metadata?.telemetryTracked;
if (isTracked && requestId) {
this.telemetry.endRequest(requestId, response.status);
}
this.logger.debug(`Response: ${response.status}`, { requestId });
return response;
},
(error) => {
const customConfig = error.config;
const requestId = customConfig?.metadata?.requestId;
const isTracked = customConfig?.metadata?.telemetryTracked;
const retryCount = customConfig?.metadata?.retryCount || 0;
if (isTracked && requestId) {
this.telemetry.endRequest(requestId, error.response?.status, error, retryCount);
}
this.logger.error(`Error: ${error.message}`, {
requestId,
status: error.response?.status
});
return Promise.reject(error);
}
);
}
/**
* Make a request with retry logic
*/
async request(config) {
let lastError;
config.metadata = config.metadata || {};
for (let retry = 0; retry <= this.retries; retry++) {
try {
config.metadata.retryCount = retry;
if (retry > 0) {
this.logger.info(`Retry attempt ${retry}/${this.retries}`, {
requestId: config.metadata.requestId
});
}
const response = await this.client.request(config);
return response.data;
} catch (error) {
lastError = error;
if (retry < this.retries && isRetryable(lastError)) {
const retryAfterHeader = lastError.response?.headers?.["retry-after"];
const backoffTime = retryAfterHeader ? parseInt(retryAfterHeader, 10) * 1e3 : calculateBackoff(retry, this.retryDelay);
this.logger.debug(`Retrying in ${backoffTime}ms...`);
await sleep(backoffTime);
continue;
}
break;
}
}
throw parseApiError(lastError);
}
/**
* Make a GET request
*/
async get(path, params) {
const config = {
method: "GET",
url: path
};
if (params) {
config.params = Object.fromEntries(
Object.entries(params).filter(([, v]) => v !== void 0 && v !== null)
);
}
return this.request(config);
}
/**
* Make a POST request
*/
async post(path, data) {
return this.request({
method: "POST",
url: path,
data
});
}
/**
* Make a PUT request
*/
async put(path, data) {
return this.request({
method: "PUT",
url: path,
data
});
}
/**
* Make a PATCH request
*/
async patch(path, data) {
return this.request({
method: "PATCH",
url: path,
data
});
}
/**
* Make a DELETE request
*/
async delete(path) {
return this.request({
method: "DELETE",
url: path
});
}
/**
* Get telemetry statistics
*/
getTelemetryStats() {
return this.telemetry.getStats();
}
/**
* Reset telemetry statistics
*/
resetTelemetryStats() {
this.telemetry.resetStats();
}
};
// src/utils/validation.ts
function validateRequired(value, fieldName) {
if (value === void 0 || value === null || value === "") {
throw new ValidationError(`${fieldName} is required`);
}
}
function validateNonNegative(value, fieldName) {
if (typeof value !== "number" || isNaN(value)) {
throw new ValidationError(`${fieldName} must be a number`);
}
if (value < 0) {
throw new ValidationError(`${fieldName} must be non-negative`);
}
}
function validateDateString(value, fieldName) {
const date = value instanceof Date ? value : new Date(value);
if (isNaN(date.getTime())) {
throw new ValidationError(`${fieldName} must be a valid ISO date string`);
}
}
function validateUsageRecord(record) {
validateRequired(record.customerExternalId, "customerExternalId");
validateRequired(record.agentId, "agentId");
const signalName = record.signalName || record.metricName;
if (!signalName) {
throw new ValidationError("signalName is required");
}
if (record.quantity !== void 0 && record.quantity !== null) {
validateNonNegative(record.quantity, "quantity");
}
if (record.usageDate !== void 0) {
validateDateString(record.usageDate, "usageDate");
}
}
function validateUsageRecords(records) {
if (!Array.isArray(records)) {
throw new ValidationError("records must be an array");
}
if (records.length === 0) {
throw new ValidationError("records array cannot be empty");
}
records.forEach((record, index) => {
try {
validateUsageRecord(record);
} catch (error) {
if (error instanceof ValidationError) {
throw new ValidationError(`Invalid record at index ${index}: ${error.message}`);
}
throw error;
}
});
}
var KEY_PREFIXES = {
secret: {
live: "upay_sk_live_",
test: "upay_sk_test_"
},
publishable: {
live: "upay_pk_live_",
test: "upay_pk_test_"
},
legacy: "revx_pk_"
};
function parseApiKey(apiKey) {
if (!apiKey || typeof apiKey !== "string") {
return null;
}
if (apiKey.startsWith(KEY_PREFIXES.secret.live)) {
return { type: "secret", environment: "live", isLegacy: false };
}
if (apiKey.startsWith(KEY_PREFIXES.secret.test)) {
return { type: "secret", environment: "test", isLegacy: false };
}
if (apiKey.startsWith(KEY_PREFIXES.publishable.live)) {
return { type: "publishable", environment: "live", isLegacy: false };
}
if (apiKey.startsWith(KEY_PREFIXES.publishable.test)) {
return { type: "publishable", environment: "test", isLegacy: false };
}
if (apiKey.startsWith(KEY_PREFIXES.legacy)) {
return { type: "secret", environment: "test", isLegacy: true };
}
return null;
}
function isSecretKey(apiKey) {
const parsed = parseApiKey(apiKey);
return parsed !== null && parsed.type === "secret";
}
function isPublishableKey(apiKey) {
const parsed = parseApiKey(apiKey);
return parsed !== null && parsed.type === "publishable";
}
function getMinKeyLength(prefix) {
return prefix.length + 16;
}
function validateApiKey(apiKey) {
if (!apiKey || typeof apiKey !== "string") {
throw new ValidationError("API key is required");
}
const parsed = parseApiKey(apiKey);
if (!parsed) {
throw new ValidationError(
"Invalid API key format. Expected upay_sk_live_*, upay_sk_test_*, upay_pk_live_*, upay_pk_test_*, or legacy revx_pk_*"
);
}
let prefix;
if (parsed.isLegacy) {
prefix = KEY_PREFIXES.legacy;
} else {
prefix = KEY_PREFIXES[parsed.type][parsed.environment];
}
const minLength = getMinKeyLength(prefix);
if (apiKey.length < minLength) {
throw new ValidationError("Invalid API key format. Key is too short");
}
if (parsed.isLegacy && typeof console !== "undefined") {
console.warn(
"[UnitPay SDK] You are using a legacy API key format (revx_pk_*). Please migrate to the new format (upay_sk_* / upay_pk_*) for better security."
);
}
}
// src/resources/usage.ts
var UsageResource = class {
constructor(http) {
this.http = http;
}
/**
* Track events for a customer - supports both single record and batch operations
* Compatible with RevMax SDK trackEvent method
*
* @param params - Event tracking parameters (single record or batch)
* @returns Tracked event data
*
* @example
* ```typescript
* // Single event
* await client.usage.trackEvent({
* agentId: 'agent_123',
* customerExternalId: 'customer_456',
* signalName: 'api_call',
* quantity: 1
* });
*
* // With metadata and usage cost (RevMax compatible)
* await client.usage.trackEvent({
* agentId: 'agent_123',
* customerExternalId: 'customer_456',
* metricName: 'meeting_booked',
* metadata: {
* usageCost: [
* { serviceName: 'SST-DeepGram', units: 10 },
* { serviceName: 'TTS-ElevanLabs', units: 10 }
* ]
* }
* });
*
* // Batch tracking
* await client.usage.trackEvent({
* records: [
* { customerExternalId: 'cust_1', agentId: 'agent_123', signalName: 'api_call', quantity: 10 },
* { customerExternalId: 'cust_2', agentId: 'agent_123', signalName: 'storage', quantity: 100 }
* ]
* });
* ```
*/
async trackEvent(params) {
if ("records" in params) {
const records = params.records.map((record2) => this.normalizeRecord(record2));
validateUsageRecords(records);
const response2 = await this.http.post("/sdk/usage/record", {
records
});
return this.convertToBatchResponse(response2);
}
const record = this.normalizeRecord(params);
validateUsageRecord(record);
const response = await this.http.post("/sdk/usage/record", {
records: [record]
});
if (response.results.success.length === 1) {
const successResult = response.results.success[0];
return {
id: successResult.eventId,
success: true,
eventId: successResult.eventId,
rawEventId: successResult.rawEventId,
timestamp: successResult.timestamp
};
}
return this.convertToBatchResponse(response);
}
/**
* Record a single usage event
* UnitPay native method
*
* @param record - The usage record to track
* @returns The response containing processing results
*
* @example
* ```typescript
* const result = await client.usage.record({
* customerExternalId: 'cust_123',
* agentId: 'agent_abc',
* signalName: 'api_call',
* quantity: 1,
* metadata: { model: 'gpt-4' }
* });
* ```
*/
async record(record) {
const normalizedRecord = this.normalizeRecord(record);
validateUsageRecord(normalizedRecord);
return this.http.post("/sdk/usage/record", {
records: [normalizedRecord]
});
}
/**
* Record multiple usage events in a batch
* UnitPay native method
*
* @param records - Array of usage records to track
* @returns The response containing processing results for all records
*
* @example
* ```typescript
* const result = await client.usage.recordBatch([
* { customerExternalId: 'cust_123', agentId: 'agent_abc', signalName: 'api_call', quantity: 5 },
* { customerExternalId: 'cust_456', agentId: 'agent_abc', signalName: 'api_call', quantity: 3 },
* ]);
* console.log(`Processed ${result.successful} of ${result.processed} records`);
* ```
*/
async recordBatch(records) {
const normalizedRecords = records.map((record) => this.normalizeRecord(record));
validateUsageRecords(normalizedRecords);
return this.http.post("/sdk/usage/record", {
records: normalizedRecords
});
}
/**
* Normalize a usage record, handling RevMax compatibility
*/
normalizeRecord(record) {
const normalized = { ...record };
if (normalized.metricName && !normalized.signalName) {
normalized.signalName = normalized.metricName;
}
if (normalized.quantity === void 0) {
normalized.quantity = 1;
}
if (normalized.usageDate instanceof Date) {
normalized.usageDate = normalized.usageDate.toISOString();
}
return normalized;
}
/**
* Convert UnitPay native response to RevMax BatchEventResponse format
*/
convertToBatchResponse(response) {
return {
success: response.failed === 0,
totalRecords: response.processed,
successCount: response.successful,
failureCount: response.failed,
results: [
...response.results.success.map((result) => ({
success: true,
responseData: {
id: result.eventId,
success: true,
eventId: result.eventId,
rawEventId: result.rawEventId,
timestamp: result.timestamp
}
})),
...response.results.failed.map((result) => ({
success: false,
error: result.error,
originalData: result.record
}))
]
};
}
};
// src/resources/customers.ts
var CustomersResource = class {
constructor(http) {
this.http = http;
}
/**
* Create a new customer
* Compatible with RevMax SDK customers.create
*
* @param params - Customer creation parameters
* @returns The created customer
*
* @example
* ```typescript
* const customer = await client.customers.create({
* name: 'Acme Corp',
* externalId: 'acme_123',
* email: 'billing@acme.com',
* agentId: 'agent_abc' // auto-creates subscription
* });
* ```
*/
async create(params) {
validateRequired(params.name, "name");
return this.http.post("/sdk/customers", params);
}
/**
* Get a single customer by ID
* Compatible with RevMax SDK customers.get
*
* @param id - The customer's ID
* @returns The customer with subscriptions
*
* @example
* ```typescript
* const customer = await client.customers.get('cust_123');
* console.log(customer.name);
* ```
*/
async get(id) {
validateRequired(id, "id");
return this.http.get(`/sdk/customers/${id}`);
}
/**
* Update an existing customer
* Compatible with RevMax SDK customers.update
*
* @param id - The customer's ID
* @param params - Fields to update
* @returns The updated customer
*
* @example
* ```typescript
* const customer = await client.customers.update('cust_123', {
* name: 'New Company Name',
* email: 'new-email@company.com'
* });
* ```
*/
async update(id, params) {
validateRequired(id, "id");
return this.http.patch(`/sdk/customers/${id}`, params);
}
/**
* Delete a customer
* Compatible with RevMax SDK customers.delete
*
* @param id - The customer's ID
*
* @example
* ```typescript
* await client.customers.delete('cust_123');
* ```
*/
async delete(id) {
validateRequired(id, "id");
await this.http.delete(`/sdk/customers/${id}`);
}
/**
* List customers with pagination and filtering
* Compatible with RevMax SDK customers.list
*
* @param params - List parameters
* @returns Paginated list of customers
*
* @example
* ```typescript
* // RevMax compatible format
* const { data, totalResults, hasMore } = await client.customers.list({
* limit: 10,
* page: 1
* });
*
* // Simple list (returns array for backward compatibility)
* const customers = await client.customers.list();
* ```
*/
async list(params) {
const response = await this.http.get("/sdk/customers", params);
if (Array.isArray(response)) {
return {
data: response,
totalResults: response.length,
page: params?.page || 1,
limit: params?.limit || response.length,
hasMore: false
};
}
return response;
}
};
// src/resources/invoices.ts
var InvoicesResource = class {
constructor(http) {
this.http = http;
}
/**
* List invoices with optional filters
*
* @param params - Optional filter parameters
* @returns Paginated list of invoices
*
* @example
* ```typescript
* const { invoices, totalResults } = await client.invoices.list({
* customerId: 'cust_123',
* status: 'pending',
* page: 1,
* limit: 20
* });
* ```
*/
async list(params) {
return this.http.get("/sdk/invoices", params);
}
/**
* Get a single invoice by ID
*
* @param invoiceId - The invoice's ID
* @returns The invoice with full details including line items and payments
*
* @example
* ```typescript
* const invoice = await client.invoices.get('inv_abc');
* console.log(`Invoice ${invoice.invoiceNumber}: ${invoice.totalAmount}`);
* ```
*/
async get(invoiceId) {
validateRequired(invoiceId, "invoiceId");
return this.http.get(`/sdk/invoices/${invoiceId}`);
}
};
// src/resources/analytics.ts
var AnalyticsResource = class {
constructor(http) {
this.http = http;
}
/**
* Get usage analytics for a date range
*
* @param params - Analytics query parameters
* @returns Usage analytics with summary and time-series data
*
* @example
* ```typescript
* const analytics = await client.analytics.usage({
* startDate: '2024-01-01',
* endDate: '2024-01-31',
* groupBy: 'daily',
* customerId: 'cust_123'
* });
*
* console.log(`Total usage: ${analytics.summary.totalQuantity}`);
* console.log(`Total cost: $${analytics.summary.totalCost}`);
*
* // Time series data
* analytics.data.forEach(point => {
* console.log(`${point.date}: ${point.quantity} units, $${point.cost}`);
* });
* ```
*/
async usage(params) {
validateRequired(params.startDate, "startDate");
validateRequired(params.endDate, "endDate");
validateDateString(params.startDate, "startDate");
validateDateString(params.endDate, "endDate");
return this.http.get("/sdk/analytics/usage", params);
}
};
// src/resources/subscriptions.ts
var SubscriptionsResource = class {
constructor(http) {
this.http = http;
}
/**
* List subscriptions with optional filters
*
* @param params - Optional filter parameters
* @returns Paginated list of subscriptions
*
* @example
* ```typescript
* const { subscriptions, totalResults } = await client.subscriptions.list({
* status: 'active',
* customerId: 'cust_123',
* page: 1,
* limit: 20
* });
* ```
*/
async list(params) {
return this.http.get("/sdk/subscriptions", params);
}
/**
* Get a single subscription by ID
*
* @param subscriptionId - The subscription's ID
* @returns The subscription with full details including usage summary
*
* @example
* ```typescript
* const sub = await client.subscriptions.get('sub_abc');
* console.log(`Status: ${sub.status}`);
* console.log(`Usage this period: ${sub.usage.totalQuantity}`);
* ```
*/
async get(subscriptionId) {
validateRequired(subscriptionId, "subscriptionId");
return this.http.get(`/sdk/subscriptions/${subscriptionId}`);
}
};
// src/resources/portal-sessions.ts
var PortalSessionsResource = class {
constructor(http, assertSecretKey) {
this.http = http;
this.assertSecretKey = assertSecretKey;
}
/**
* Create a new portal session
*
* Creates a short-lived portal session for a customer.
* The returned URL can be used to redirect the customer to their billing portal.
*
* @param params - Portal session parameters
* @returns Created portal session with URL and token
* @throws ValidationError if neither customerId nor customerExternalId is provided
* @throws AuthenticationError if using a publishable key
*
* @example
* ```typescript
* // By customer ID
* const session = await client.portalSessions.create({
* customerId: 'cust_123',
* returnUrl: 'https://myapp.com/account'
* });
*
* // By external ID
* const session = await client.portalSessions.create({
* customerExternalId: 'your_customer_id',
* features: ['invoices', 'usage']
* });
*
* console.log(session.url); // Redirect customer here
* ```
*/
async create(params) {
this.assertSecretKey();
if (!params.customerId && !params.customerExternalId) {
throw new ValidationError(
"Either customerId or customerExternalId is required"
);
}
return this.http.post("/sdk/portal-sessions", params);
}
/**
* Get a portal session by ID
*
* @param sessionId - Portal session ID
* @returns Portal session details
*
* @example
* ```typescript
* const session = await client.portalSessions.get('ps_123');
* console.log(session.expiresAt);
* ```
*/
async get(sessionId) {
this.assertSecretKey();
if (!sessionId) {
throw new ValidationError("sessionId is required");
}
return this.http.get(`/sdk/portal-sessions/${sessionId}`);
}
/**
* List portal sessions
*
* @param params - Optional filters
* @returns List of portal sessions
*
* @example
* ```typescript
* // List all active sessions
* const { data } = await client.portalSessions.list();
*
* // List sessions for a specific customer
* const { data } = await client.portalSessions.list({
* customerId: 'cust_123',
* includeExpired: true
* });
* ```
*/
async list(params) {
this.assertSecretKey();
return this.http.get("/sdk/portal-sessions", {
customerId: params?.customerId,
limit: params?.limit,
includeExpired: params?.includeExpired
});
}
/**
* Revoke a portal session
*
* Immediately invalidates the session, preventing further access.
*
* @param sessionId - Portal session ID to revoke
*
* @example
* ```typescript
* await client.portalSessions.revoke('ps_123');
* ```
*/
async revoke(sessionId) {
this.assertSecretKey();
if (!sessionId) {
throw new ValidationError("sessionId is required");
}
await this.http.delete(`/sdk/portal-sessions/${sessionId}`);
}
};
// src/client.ts
var UnitPayClient = class {
/**
* Create a new UnitPay client
* Compatible with RevMax SDK constructor signature
*
* @param apiKeyOrConfig - Either an API key string or a configuration object
* @param options - Optional client options (when first param is API key)
*
* @example
* ```typescript
* // Simple initialization
* const client = new UnitPayClient('revx_pk_your_api_key');
*
* // With options (RevMax style)
* const client = new UnitPayClient('revx_pk_your_api_key', {
* timeout: 10000,
* retries: 3
* });
*
* // With config object (UnitPay style)
* const client = new UnitPayClient({
* apiKey: 'revx_pk_your_api_key',
* baseUrl: 'https://api.example.com/v1',
* debug: true
* });
* ```
*/
constructor(apiKeyOrConfig, options = {}) {
this._orgInfo = null;
let config;
if (typeof apiKeyOrConfig === "string") {
config = {
apiKey: apiKeyOrConfig,
...options
};
} else {
config = apiKeyOrConfig;
}
validateApiKey(config.apiKey);
this._apiKey = config.apiKey;
const keyInfo = parseApiKey(config.apiKey);
if (!keyInfo) {
throw new ValidationError("Invalid API key format");
}
this._keyType = keyInfo.type;
this._keyEnvironment = keyInfo.environment;
this._isLegacyKey = keyInfo.isLegacy;
this.http = new HttpClient(config);
this.usage = new UsageResource(this.http);
this.customers = new CustomersResource(this.http);
this.invoices = new InvoicesResource(this.http);
this.analytics = new AnalyticsResource(this.http);
this.subscriptions = new SubscriptionsResource(this.http);
this.portalSessions = new PortalSessionsResource(
this.http,
() => this.assertSecretKey()
);
}
/**
* Assert that the current API key is a secret key
* @throws ValidationError if using a publishable key
*/
assertSecretKey() {
if (this._keyType !== "secret") {
throw new ValidationError(
"This operation requires a secret key (upay_sk_*). Publishable keys (upay_pk_*) cannot perform this action. Use your secret key on the server side only."
);
}
}
/**
* Connect to the UnitPay API and verify credentials
* Compatible with RevMax SDK connect() method
*
* This must be called once before using any API methods that require verification.
* Note: For UnitPay, this is optional but recommended for validating your API key.
*
* @returns Promise resolving to the client instance for chaining
* @throws AuthenticationError if API key is invalid
* @throws InitializationError for other initialization errors
*
* @example
* ```typescript
* const client = new UnitPayClient('revx_pk_your_api_key');
*
* try {
* await client.connect();
* console.log('Connected to UnitPay API');
* } catch (error) {
* console.error('Failed to connect:', error);
* }
* ```
*/
async connect() {
try {
const result = await this.http.get("/sdk/verify");
if (!result || !result.organization || !result.organization.id) {
throw new InitializationError("Unexpected response format from API key verification");
}
this._orgInfo = result.organization;
return this;
} catch (error) {
const err = error;
const statusCode = err.statusCode || err.response?.status;
const errorMessage = err.message || "Unknown error";
const requestId = err.requestId || "unknown";
if (statusCode === 401) {
throw new AuthenticationError(
"Invalid API key. Please check your API key and try again.",
{ requestId }
);
}
throw new InitializationError(`Connection failed: ${errorMessage}`, { requestId });
}
}
/**
* Verify the API key and get organization details
* UnitPay native method (also works without connect())
*
* @returns Verification result with organization info
*
* @example
* ```typescript
* const result = await client.verify();
* if (result.verified) {
* console.log(`Connected to ${result.organization.name}`);
* }
* ```
*/
async verify() {
const result = await this.http.get("/sdk/verify");
if (result.organization && !this._orgInfo) {
this._orgInfo = result.organization;
}
return result;
}
/**
* Get organization information
* Compatible with RevMax SDK getOrganization() method
*
* @returns Organization information from API key verification
* @throws Error if organization info is not available (call connect() first)
*
* @example
* ```typescript
* await client.connect();
* const org = client.getOrganization();
* console.log(`Organization: ${org.name}`);
* ```
*/
getOrganization() {
if (!this._orgInfo) {
throw new Error(
"Organization information is not available. Make sure the API key is valid and you have called connect()"
);
}
return { ...this._orgInfo };
}
/**
* Track event for a customer (shorthand method)
* Compatible with RevMax SDK trackEvent() method
*
* @param params - Event tracking parameters
* @returns Tracked event data
*
* @example
* ```typescript
* // Simple event tracking
* await client.trackEvent({
* agentId: 'agent_123',
* customerExternalId: 'customer_456',
* signalName: 'api_call',
* quantity: 1
* });
*
* // With metadata and usage cost (RevMax compatible)
* await client.trackEvent({
* agentId: 'agent_123',
* customerExternalId: 'customer_456',
* metricName: 'meeting_booked',
* metadata: {
* usageCost: [
* { serviceName: 'SST-DeepGram', units: 10 },
* { serviceName: 'TTS-ElevanLabs', units: 10 }
* ]
* }
* });
*
* // Batch tracking
* await client.trackEvent({
* records: [
* { customerExternalId: 'cust_1', agentId: 'agent_123', signalName: 'api_call', quantity: 10 },
* { customerExternalId: 'cust_2', agentId: 'agent_123', signalName: 'storage', quantity: 100 }
* ]
* });
* ```
*/
async trackEvent(params) {
return this.usage.trackEvent(params);
}
/**
* Re-verify the API key
* Compatible with RevMax SDK verifyApiKey() method
*
* Useful if you suspect the API key status might have changed
*
* @returns Updated organization information
*/
async verifyApiKey() {
const result = await this.verify();
if (!result.organization) {
throw new InitializationError("Unexpected response format from API key verification");
}
this._orgInfo = result.organization;
return { ...this._orgInfo };
}
/**
* Get current telemetry statistics
* Compatible with RevMax SDK getTelemetryStats() method
*
* @returns Telemetry statistics
*
* @example
* ```typescript
* const stats = client.getTelemetryStats();
* console.log(`Total Requests: ${stats.requestCount}`);
* console.log(`Success Rate: ${(stats.successRate * 100).toFixed(2)}%`);
* ```
*/
getTelemetryStats() {
return this.http.getTelemetryStats();
}
/**
* Reset telemetry statistics
* Compatible with RevMax SDK resetTelemetryStats() method
*/
resetTelemetryStats() {
this.http.resetTelemetryStats();
}
/**
* Get the API key type (secret or publishable)
*
* @returns 'secret' for upay_sk_* keys, 'publishable' for upay_pk_* keys
*
* @example
* ```typescript
* if (client.getKeyType() === 'secret') {
* // Can create portal sessions
* const session = await client.portalSessions.create({ ... });
* }
* ```
*/
getKeyType() {
return this._keyType;
}
/**
* Get the API key environment (live or test)
*
* @returns 'live' for production keys, 'test' for test keys
*
* @example
* ```typescript
* if (client.getKeyEnvironment() === 'test') {
* console.log('Running in test mode');
* }
* ```
*/
getKeyEnvironment() {
return this._keyEnvironment;
}
/**
* Check if using a legacy API key format
*
* @returns true if using revx_pk_* format (deprecated)
*/
isLegacyKey() {
return this._isLegacyKey;
}
/**
* Check if the API key is a secret key (upay_sk_* or legacy revx_pk_*)
*
* @returns true if the key can perform server-side operations
*/
isSecretKey() {
return this._keyType === "secret";
}
/**
* Check if the API key is a publishable key (upay_pk_*)
*
* @returns true if the key is safe for frontend use
*/
isPublishableKey() {
return this._keyType === "publishable";
}
/**
* Get a masked version of the API key for debugging
*
* @returns Masked API key showing only prefix and last 4 characters
*/
getMaskedApiKey() {
if (this._apiKey.length <= 20) {
return this._apiKey.substring(0, 8) + "...";
}
return this._apiKey.substring(0, 16) + "..." + this._apiKey.slice(-4);
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
ApiError,
AuthenticationError,
AuthorizationError,
ConflictError,
InitializationError,
InternalError,
NetworkError,
NotFoundError,
RateLimitError,
RevMaxApiError,
RevMaxAuthenticationError,
RevMaxClient,
RevMaxError,
RevMaxInitializationError,
RevMaxNotFoundError,
RevMaxRateLimitError,
RevMaxValidationError,
TimeoutError,
UnitPayClient,
UnitPayError,
ValidationError,
createErrorFromResponse,
isPublishableKey,
isSecretKey,
parseApiError,
parseApiKey
});