UNPKG

@unitpay/sdk

Version:

Official Node.js SDK for UnitPay - usage-based billing, invoicing, and analytics

1,491 lines (1,478 loc) 45.9 kB
"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 });