@nehorai/credits 1.6.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +226 -2
  3. package/dist/adapters/index.cjs +298 -40
  4. package/dist/adapters/index.cjs.map +1 -1
  5. package/dist/adapters/index.d.cts +2 -2
  6. package/dist/adapters/index.d.ts +2 -2
  7. package/dist/adapters/index.js +298 -40
  8. package/dist/adapters/index.js.map +1 -1
  9. package/dist/core/index.cjs +696 -74
  10. package/dist/core/index.cjs.map +1 -1
  11. package/dist/core/index.d.cts +583 -5
  12. package/dist/core/index.d.ts +583 -5
  13. package/dist/core/index.js +648 -74
  14. package/dist/core/index.js.map +1 -1
  15. package/dist/index.cjs +1779 -364
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +41 -6
  18. package/dist/index.d.ts +41 -6
  19. package/dist/index.js +1723 -364
  20. package/dist/index.js.map +1 -1
  21. package/dist/repository/index.cjs +1413 -181
  22. package/dist/repository/index.cjs.map +1 -1
  23. package/dist/repository/index.d.cts +142 -16
  24. package/dist/repository/index.d.ts +142 -16
  25. package/dist/repository/index.js +1406 -181
  26. package/dist/repository/index.js.map +1 -1
  27. package/dist/sdk/index.cjs +23 -1
  28. package/dist/sdk/index.cjs.map +1 -1
  29. package/dist/sdk/index.d.cts +2 -2
  30. package/dist/sdk/index.d.ts +2 -2
  31. package/dist/sdk/index.js +23 -1
  32. package/dist/sdk/index.js.map +1 -1
  33. package/dist/service/index.cjs +483 -105
  34. package/dist/service/index.cjs.map +1 -1
  35. package/dist/service/index.d.cts +70 -10
  36. package/dist/service/index.d.ts +70 -10
  37. package/dist/service/index.js +483 -105
  38. package/dist/service/index.js.map +1 -1
  39. package/dist/types-BmG5Omzj.d.ts +646 -0
  40. package/dist/types-C8hZMe8a.d.cts +646 -0
  41. package/dist/{types-CjLj3GW5.d.cts → types-DJ8yqaVr.d.cts} +39 -1
  42. package/dist/{types-CjLj3GW5.d.ts → types-DJ8yqaVr.d.ts} +39 -1
  43. package/package.json +9 -10
  44. package/dist/types-CZNv1U3e.d.cts +0 -305
  45. package/dist/types-G4WK0rM-.d.ts +0 -305
@@ -213,6 +213,28 @@ function parseApiError(statusCode, body) {
213
213
  }
214
214
  }
215
215
 
216
+ // src/core/amount.ts
217
+ function toCents(value) {
218
+ if (typeof value !== "number" || !Number.isFinite(value)) return null;
219
+ const cents = Math.round(value * 100);
220
+ if (!Number.isSafeInteger(cents)) return null;
221
+ return value === cents / 100 ? cents : null;
222
+ }
223
+ function sumAmounts(...values) {
224
+ let cents = 0;
225
+ for (const value of values) {
226
+ const valueCents = toCents(value);
227
+ if (valueCents === null) return floatSum(values);
228
+ cents += valueCents;
229
+ }
230
+ return Number.isSafeInteger(cents) ? cents / 100 : floatSum(values);
231
+ }
232
+ function floatSum(values) {
233
+ let total = 0;
234
+ for (const value of values) total += value;
235
+ return total;
236
+ }
237
+
216
238
  // src/sdk/client.ts
217
239
  var CreditsClient = class {
218
240
  baseUrl;
@@ -375,7 +397,7 @@ var CreditsClient = class {
375
397
  */
376
398
  async getAvailableCredits() {
377
399
  const balance = await this.getBalance();
378
- return balance.balance + balance.bonusCredits - balance.reserved;
400
+ return sumAmounts(balance.balance, balance.bonusCredits, -balance.reserved);
379
401
  }
380
402
  /**
381
403
  * Execute an operation with automatic credit handling
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/sdk/index.ts","../../src/sdk/types.ts","../../src/sdk/errors.ts","../../src/sdk/client.ts","../../src/sdk/admin-client.ts"],"sourcesContent":["/**\n * Credits SDK - barrel exports\n *\n * Provides TypeScript SDK for consuming the credits REST API\n * from external services or other applications.\n */\n\n// Clients\nexport { CreditsClient } from \"./client.js\";\nexport { AdminCreditsClient } from \"./admin-client.js\";\n\n// Types\nexport type {\n CreditsClientConfig,\n AdminCreditsClientConfig,\n UserCredits,\n CreditReservation,\n CreditCheckResult,\n ReservationResult,\n PaginationOptions,\n UsageHistoryEntry,\n UsageHistoryResponse,\n ApiResponse,\n SDKErrorCodeType,\n SubscriptionTier,\n} from \"./types.js\";\n\nexport { SDKErrorCode } from \"./types.js\";\n\n// Errors\nexport {\n CreditsSDKError,\n NetworkError,\n AuthenticationError,\n AuthorizationError,\n InsufficientCreditsError,\n ReservationNotFoundError,\n ReservationExpiredError,\n ValidationError,\n ServerError,\n parseApiError,\n} from \"./errors.js\";\n\n// Admin client types\nexport type {\n CreditsConfig,\n ListUsersResponse,\n ListUsersOptions,\n} from \"./admin-client.js\";\n","/**\n * SDK types for the credits system\n *\n * These types are used by the client SDKs for REST API consumption.\n */\n\n// Re-export core types for SDK consumers\nexport type {\n PortableUserCredits as UserCredits,\n PortableReservation as CreditReservation,\n CreditCheckResult,\n SubscriptionTier,\n} from \"../core/types.js\";\n\n/**\n * Configuration for CreditsClient\n */\nexport interface CreditsClientConfig {\n /** Base URL of the credits API (e.g., \"https://api.example.com\") */\n baseUrl: string;\n /** Function to get the auth token for session-based auth */\n getAuthToken?: () => Promise<string>;\n /** Optional custom fetch function for testing */\n fetch?: typeof fetch;\n}\n\n/**\n * Configuration for AdminCreditsClient\n */\nexport interface AdminCreditsClientConfig {\n /** Base URL of the credits API */\n baseUrl: string;\n /** Admin API key */\n apiKey: string;\n /** Optional custom fetch function for testing */\n fetch?: typeof fetch;\n}\n\n/**\n * Result of a credit reservation\n */\nexport interface ReservationResult {\n /** The reservation ID */\n reservationId: string;\n /** Amount of credits reserved */\n amount: number;\n /** When the reservation expires (ISO 8601) */\n expiresAt: string;\n}\n\n/**\n * Pagination options for API requests\n */\nexport interface PaginationOptions {\n /** Page number (1-indexed) */\n page?: number;\n /** Number of items per page */\n limit?: number;\n}\n\n/**\n * Usage history entry\n */\nexport interface UsageHistoryEntry {\n /** Unique ID of the usage log */\n id: string;\n /** Type of operation */\n operationType: string;\n /** AI provider used */\n provider: string;\n /** Credits consumed */\n creditsUsed: number;\n /** Whether the operation was successful */\n success: boolean;\n /** Error message if unsuccessful */\n errorMessage?: string;\n /** ID of the related resource */\n resourceId?: string;\n /** Type of the related resource */\n resourceType?: string;\n /** When the operation occurred (ISO 8601) */\n createdAt: string;\n}\n\n/**\n * Response from usage history endpoint\n */\nexport interface UsageHistoryResponse {\n /** List of usage entries */\n entries: UsageHistoryEntry[];\n /** Whether there are more results */\n hasMore: boolean;\n /** Total count (if available) */\n total?: number;\n}\n\n/**\n * Generic API response wrapper\n */\nexport interface ApiResponse<T> {\n success: boolean;\n data?: T;\n error?: {\n code: string;\n message: string;\n details?: unknown;\n };\n}\n\n/**\n * SDK error codes\n */\nexport const SDKErrorCode = {\n NETWORK_ERROR: \"NETWORK_ERROR\",\n AUTHENTICATION_ERROR: \"AUTHENTICATION_ERROR\",\n AUTHORIZATION_ERROR: \"AUTHORIZATION_ERROR\",\n INSUFFICIENT_CREDITS: \"INSUFFICIENT_CREDITS\",\n RESERVATION_NOT_FOUND: \"RESERVATION_NOT_FOUND\",\n RESERVATION_EXPIRED: \"RESERVATION_EXPIRED\",\n VALIDATION_ERROR: \"VALIDATION_ERROR\",\n SERVER_ERROR: \"SERVER_ERROR\",\n UNKNOWN_ERROR: \"UNKNOWN_ERROR\",\n} as const;\n\nexport type SDKErrorCodeType = (typeof SDKErrorCode)[keyof typeof SDKErrorCode];\n","/**\n * SDK error classes\n *\n * Provides typed error handling for SDK operations.\n */\n\nimport { SDKErrorCode, type SDKErrorCodeType } from \"./types.js\";\n\n/**\n * Base error class for SDK errors\n */\nexport class CreditsSDKError extends Error {\n public readonly code: SDKErrorCodeType;\n public readonly details?: Record<string, unknown>;\n public readonly statusCode?: number;\n\n constructor(\n message: string,\n code: SDKErrorCodeType,\n options?: {\n details?: Record<string, unknown>;\n statusCode?: number;\n cause?: Error;\n }\n ) {\n super(message, { cause: options?.cause });\n this.name = \"CreditsSDKError\";\n this.code = code;\n this.details = options?.details;\n this.statusCode = options?.statusCode;\n }\n\n /**\n * Check if this is a specific error type\n */\n is(code: SDKErrorCodeType): boolean {\n return this.code === code;\n }\n\n /**\n * Create a JSON representation for logging\n */\n toJSON(): Record<string, unknown> {\n return {\n name: this.name,\n message: this.message,\n code: this.code,\n details: this.details,\n statusCode: this.statusCode,\n };\n }\n}\n\n/**\n * Error thrown when a network request fails\n */\nexport class NetworkError extends CreditsSDKError {\n constructor(message: string, cause?: Error) {\n super(message, SDKErrorCode.NETWORK_ERROR, { cause });\n this.name = \"NetworkError\";\n }\n}\n\n/**\n * Error thrown when authentication fails\n */\nexport class AuthenticationError extends CreditsSDKError {\n constructor(message: string = \"Authentication required\") {\n super(message, SDKErrorCode.AUTHENTICATION_ERROR, { statusCode: 401 });\n this.name = \"AuthenticationError\";\n }\n}\n\n/**\n * Error thrown when authorization fails\n */\nexport class AuthorizationError extends CreditsSDKError {\n constructor(message: string = \"Access denied\") {\n super(message, SDKErrorCode.AUTHORIZATION_ERROR, { statusCode: 403 });\n this.name = \"AuthorizationError\";\n }\n}\n\n/**\n * Error thrown when credits are insufficient\n */\nexport class InsufficientCreditsError extends CreditsSDKError {\n public readonly available: number;\n public readonly required: number;\n\n constructor(available: number, required: number) {\n super(\n `Insufficient credits: available ${available}, required ${required}`,\n SDKErrorCode.INSUFFICIENT_CREDITS,\n {\n statusCode: 402,\n details: { available, required },\n }\n );\n this.name = \"InsufficientCreditsError\";\n this.available = available;\n this.required = required;\n }\n}\n\n/**\n * Error thrown when a reservation is not found\n */\nexport class ReservationNotFoundError extends CreditsSDKError {\n public readonly reservationId: string;\n\n constructor(reservationId: string) {\n super(\n `Reservation not found: ${reservationId}`,\n SDKErrorCode.RESERVATION_NOT_FOUND,\n {\n statusCode: 404,\n details: { reservationId },\n }\n );\n this.name = \"ReservationNotFoundError\";\n this.reservationId = reservationId;\n }\n}\n\n/**\n * Error thrown when a reservation has expired\n */\nexport class ReservationExpiredError extends CreditsSDKError {\n public readonly reservationId: string;\n\n constructor(reservationId: string) {\n super(\n `Reservation expired: ${reservationId}`,\n SDKErrorCode.RESERVATION_EXPIRED,\n {\n statusCode: 410,\n details: { reservationId },\n }\n );\n this.name = \"ReservationExpiredError\";\n this.reservationId = reservationId;\n }\n}\n\n/**\n * Error thrown for validation failures\n */\nexport class ValidationError extends CreditsSDKError {\n public readonly field?: string;\n\n constructor(message: string, field?: string) {\n super(message, SDKErrorCode.VALIDATION_ERROR, {\n statusCode: 400,\n details: field ? { field } : undefined,\n });\n this.name = \"ValidationError\";\n this.field = field;\n }\n}\n\n/**\n * Error thrown for server errors\n */\nexport class ServerError extends CreditsSDKError {\n constructor(message: string = \"Internal server error\") {\n super(message, SDKErrorCode.SERVER_ERROR, { statusCode: 500 });\n this.name = \"ServerError\";\n }\n}\n\n/**\n * Parse an API error response into the appropriate SDK error\n */\nexport function parseApiError(\n statusCode: number,\n body: { error?: { code?: string; message?: string; details?: unknown } }\n): CreditsSDKError {\n const errorCode = body.error?.code || \"\";\n const errorMessage = body.error?.message || \"An error occurred\";\n const details = body.error?.details as Record<string, unknown> | undefined;\n\n switch (statusCode) {\n case 401:\n return new AuthenticationError(errorMessage);\n case 403:\n return new AuthorizationError(errorMessage);\n case 402:\n // Parse insufficient credits details\n if (details?.available !== undefined && details?.required !== undefined) {\n return new InsufficientCreditsError(\n details.available as number,\n details.required as number\n );\n }\n return new CreditsSDKError(errorMessage, SDKErrorCode.INSUFFICIENT_CREDITS, {\n statusCode,\n details,\n });\n case 404:\n if (errorCode === \"RESERVATION_NOT_FOUND\" && details?.reservationId) {\n return new ReservationNotFoundError(details.reservationId as string);\n }\n return new CreditsSDKError(errorMessage, SDKErrorCode.RESERVATION_NOT_FOUND, {\n statusCode,\n details,\n });\n case 410:\n if (errorCode === \"RESERVATION_EXPIRED\" && details?.reservationId) {\n return new ReservationExpiredError(details.reservationId as string);\n }\n return new CreditsSDKError(errorMessage, SDKErrorCode.RESERVATION_EXPIRED, {\n statusCode,\n details,\n });\n case 400:\n case 422:\n return new ValidationError(errorMessage, details?.field as string | undefined);\n case 500:\n default:\n if (statusCode >= 500) {\n return new ServerError(errorMessage);\n }\n return new CreditsSDKError(errorMessage, SDKErrorCode.UNKNOWN_ERROR, {\n statusCode,\n details,\n });\n }\n}\n","/**\n * CreditsClient - REST API client for the credits system\n *\n * Provides a type-safe interface for consuming credits API endpoints\n * from external services or other applications.\n *\n * @example\n * ```typescript\n * const client = new CreditsClient({\n * baseUrl: process.env.CREDITS_API_URL,\n * getAuthToken: async () => session.accessToken,\n * });\n *\n * // Check if user has sufficient credits\n * const check = await client.checkAvailability(\"story_generation\");\n * if (!check.hasCredits) {\n * console.log(\"Insufficient credits\");\n * return;\n * }\n *\n * // Two-phase commit pattern\n * const reservation = await client.reserve(\"story_generation\");\n * try {\n * const result = await doExpensiveWork();\n * await client.commit(reservation.reservationId);\n * return result;\n * } catch (error) {\n * await client.release(reservation.reservationId);\n * throw error;\n * }\n * ```\n */\n\nimport type {\n CreditsClientConfig,\n UserCredits,\n CreditCheckResult,\n ReservationResult,\n UsageHistoryResponse,\n PaginationOptions,\n} from \"./types.js\";\nimport { parseApiError, NetworkError } from \"./errors.js\";\n\nexport class CreditsClient {\n private readonly baseUrl: string;\n private readonly getAuthToken?: () => Promise<string>;\n private readonly fetchFn: typeof fetch;\n\n constructor(config: CreditsClientConfig) {\n // Ensure baseUrl doesn't have trailing slash\n this.baseUrl = config.baseUrl.replace(/\\/$/, \"\");\n this.getAuthToken = config.getAuthToken;\n this.fetchFn = config.fetch ?? fetch;\n }\n\n /**\n * Make an authenticated request to the API\n */\n private async request<T>(\n path: string,\n options: {\n method?: \"GET\" | \"POST\" | \"PUT\" | \"DELETE\";\n body?: unknown;\n params?: Record<string, string | number | undefined>;\n } = {}\n ): Promise<T> {\n const { method = \"GET\", body, params } = options;\n\n // Build URL with query params\n let url = `${this.baseUrl}${path}`;\n if (params) {\n const searchParams = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n if (value !== undefined) {\n searchParams.set(key, String(value));\n }\n }\n const queryString = searchParams.toString();\n if (queryString) {\n url += `?${queryString}`;\n }\n }\n\n // Build headers\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n\n // Add auth token if available\n if (this.getAuthToken) {\n try {\n const token = await this.getAuthToken();\n if (token) {\n headers[\"Authorization\"] = `Bearer ${token}`;\n }\n } catch {\n // Auth token not available, proceed without it\n }\n }\n\n // Make the request\n let response: Response;\n try {\n response = await this.fetchFn(url, {\n method,\n headers,\n body: body ? JSON.stringify(body) : undefined,\n });\n } catch (error) {\n throw new NetworkError(\n error instanceof Error ? error.message : \"Network request failed\",\n error instanceof Error ? error : undefined\n );\n }\n\n // Parse response\n const responseBody = (await response.json()) as {\n data?: T;\n error?: { code?: string; message?: string; details?: unknown };\n };\n\n // Handle errors\n if (!response.ok) {\n throw parseApiError(response.status, responseBody);\n }\n\n // Return data\n return responseBody.data as T;\n }\n\n /**\n * Get the current user's credit balance\n *\n * @returns User's credit information\n * @throws AuthenticationError if not authenticated\n */\n async getBalance(): Promise<UserCredits> {\n return this.request<UserCredits>(\"/api/v1/credits/balance\");\n }\n\n /**\n * Check if the user has sufficient credits for an operation\n *\n * @param operationType - Type of operation to check\n * @param customCost - Optional custom cost to check against\n * @returns Credit availability status\n * @throws AuthenticationError if not authenticated\n */\n async checkAvailability(\n operationType: string,\n customCost?: number\n ): Promise<CreditCheckResult> {\n return this.request<CreditCheckResult>(\"/api/v1/credits/availability\", {\n params: {\n operationType,\n customCost: customCost?.toString(),\n },\n });\n }\n\n /**\n * Reserve credits for an operation\n *\n * This is the first step in the two-phase commit pattern.\n * Reserved credits are held for 5 minutes before expiring.\n *\n * @param operationType - Type of operation\n * @param options - Optional reservation options\n * @returns Reservation details including ID and expiry\n * @throws InsufficientCreditsError if not enough credits\n * @throws AuthenticationError if not authenticated\n */\n async reserve(\n operationType: string,\n options?: {\n customCost?: number;\n resourceId?: string;\n resourceType?: string;\n }\n ): Promise<ReservationResult> {\n return this.request<ReservationResult>(\"/api/v1/credits/reserve\", {\n method: \"POST\",\n body: {\n operationType,\n ...options,\n },\n });\n }\n\n /**\n * Commit a reservation (deduct credits)\n *\n * This is the second step in the two-phase commit pattern.\n * Call this after your operation succeeds.\n *\n * @param reservationId - ID of the reservation to commit\n * @throws ReservationNotFoundError if reservation doesn't exist\n * @throws ReservationExpiredError if reservation has expired\n * @throws AuthenticationError if not authenticated\n */\n async commit(reservationId: string): Promise<void> {\n await this.request<{ message: string }>(\"/api/v1/credits/commit\", {\n method: \"POST\",\n body: { reservationId },\n });\n }\n\n /**\n * Release a reservation (refund credits)\n *\n * Call this if your operation fails after reserving credits.\n *\n * @param reservationId - ID of the reservation to release\n * @throws ReservationNotFoundError if reservation doesn't exist\n * @throws AuthenticationError if not authenticated\n */\n async release(reservationId: string): Promise<void> {\n await this.request<{ message: string }>(\"/api/v1/credits/release\", {\n method: \"POST\",\n body: { reservationId },\n });\n }\n\n /**\n * Get usage history for the current user\n *\n * @param options - Optional pagination options\n * @returns Paginated list of usage entries\n * @throws AuthenticationError if not authenticated\n */\n async getHistory(options?: PaginationOptions): Promise<UsageHistoryResponse> {\n return this.request<UsageHistoryResponse>(\"/api/v1/credits/history\", {\n params: {\n page: options?.page,\n limit: options?.limit,\n },\n });\n }\n\n /**\n * Get the available credits (balance + bonus - reserved)\n *\n * Convenience method that calculates available credits from balance.\n *\n * @returns Number of available credits\n * @throws AuthenticationError if not authenticated\n */\n async getAvailableCredits(): Promise<number> {\n const balance = await this.getBalance();\n return balance.balance + balance.bonusCredits - balance.reserved;\n }\n\n /**\n * Execute an operation with automatic credit handling\n *\n * This is a convenience method that handles the full two-phase commit\n * pattern automatically.\n *\n * @param operationType - Type of operation\n * @param operation - The operation to execute\n * @param options - Optional reservation options\n * @returns Result of the operation\n * @throws InsufficientCreditsError if not enough credits\n * @throws AuthenticationError if not authenticated\n *\n * @example\n * ```typescript\n * const result = await client.withCredits(\n * \"story_generation\",\n * async () => {\n * return generateStory(data);\n * }\n * );\n * ```\n */\n async withCredits<T>(\n operationType: string,\n operation: () => Promise<T>,\n options?: {\n customCost?: number;\n resourceId?: string;\n resourceType?: string;\n }\n ): Promise<T> {\n const reservation = await this.reserve(operationType, options);\n\n try {\n const result = await operation();\n await this.commit(reservation.reservationId);\n return result;\n } catch (error) {\n // Always attempt to release on failure\n try {\n await this.release(reservation.reservationId);\n } catch {\n // Ignore release errors, original error is more important\n }\n throw error;\n }\n }\n}\n","/**\n * AdminCreditsClient - Admin REST API client for the credits system\n *\n * Provides administrative access to the credits system for managing\n * user credits, configurations, and subscriptions.\n *\n * @example\n * ```typescript\n * const adminClient = new AdminCreditsClient({\n * baseUrl: process.env.CREDITS_API_URL,\n * apiKey: process.env.CREDITS_ADMIN_API_KEY,\n * });\n *\n * // Add credits to a user\n * await adminClient.addCredits(\"user-123\", 100, \"Support credit grant\");\n *\n * // Update subscription tier\n * await adminClient.updateSubscription(\"user-123\", \"premium\", \"2027-01-01T00:00:00Z\");\n *\n * // Update operation costs\n * await adminClient.updateCosts({\n * story_generation: 8,\n * image_generation: 12,\n * });\n * ```\n */\n\nimport type { AdminCreditsClientConfig, UserCredits } from \"./types.js\";\nimport { parseApiError, NetworkError } from \"./errors.js\";\n\n/**\n * Credits configuration returned by the API\n */\nexport interface CreditsConfig {\n operationCosts: Record<string, number>;\n tierConfigs: Record<\n string,\n {\n monthlyLimit: number;\n features: string[];\n }\n >;\n}\n\n/**\n * Response from listing users\n */\nexport interface ListUsersResponse {\n users: Array<{\n userId: string;\n balance: number;\n bonusCredits?: number;\n tier: string;\n monthlyUsed?: number;\n }>;\n hasMore: boolean;\n total?: number;\n}\n\n/**\n * Options for listing users\n */\nexport interface ListUsersOptions {\n page?: number;\n limit?: number;\n tier?: string;\n search?: string;\n}\n\nexport class AdminCreditsClient {\n private readonly baseUrl: string;\n private readonly apiKey: string;\n private readonly fetchFn: typeof fetch;\n\n constructor(config: AdminCreditsClientConfig) {\n // Ensure baseUrl doesn't have trailing slash\n this.baseUrl = config.baseUrl.replace(/\\/$/, \"\");\n this.apiKey = config.apiKey;\n this.fetchFn = config.fetch ?? fetch;\n }\n\n /**\n * Make an authenticated admin request to the API\n */\n private async request<T>(\n path: string,\n options: {\n method?: \"GET\" | \"POST\" | \"PUT\" | \"DELETE\";\n body?: unknown;\n params?: Record<string, string | number | undefined>;\n } = {}\n ): Promise<T> {\n const { method = \"GET\", body, params } = options;\n\n // Build URL with query params\n let url = `${this.baseUrl}${path}`;\n if (params) {\n const searchParams = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n if (value !== undefined) {\n searchParams.set(key, String(value));\n }\n }\n const queryString = searchParams.toString();\n if (queryString) {\n url += `?${queryString}`;\n }\n }\n\n // Build headers with admin API key\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"x-api-key\": this.apiKey,\n };\n\n // Make the request\n let response: Response;\n try {\n response = await this.fetchFn(url, {\n method,\n headers,\n body: body ? JSON.stringify(body) : undefined,\n });\n } catch (error) {\n throw new NetworkError(\n error instanceof Error ? error.message : \"Network request failed\",\n error instanceof Error ? error : undefined\n );\n }\n\n // Parse response\n const responseBody = (await response.json()) as {\n data?: T;\n error?: { code?: string; message?: string; details?: unknown };\n };\n\n // Handle errors\n if (!response.ok) {\n throw parseApiError(response.status, responseBody);\n }\n\n // Return data\n return responseBody.data as T;\n }\n\n /**\n * Get a specific user's credits\n *\n * @param userId - ID of the user\n * @returns User's credit information\n * @throws AuthorizationError if API key is invalid\n */\n async getUserCredits(userId: string): Promise<UserCredits> {\n return this.request<UserCredits>(`/api/admin/credits/users/${userId}`);\n }\n\n /**\n * Add credits to a user's account\n *\n * Credits are added to bonusCredits, which don't expire on monthly reset.\n *\n * @param userId - ID of the user\n * @param amount - Amount of credits to add\n * @param description - Reason for adding credits\n * @throws AuthorizationError if API key is invalid\n */\n async addCredits(\n userId: string,\n amount: number,\n description: string\n ): Promise<void> {\n await this.request<{ message: string; newBalance: number }>(\n `/api/admin/credits/users/${userId}/add`,\n {\n method: \"POST\",\n body: { amount, description },\n }\n );\n }\n\n /**\n * Update a user's subscription tier\n *\n * @param userId - ID of the user\n * @param tier - New subscription tier\n * @param expiresAt - Optional expiration date (ISO 8601)\n * @throws AuthorizationError if API key is invalid\n */\n async updateSubscription(\n userId: string,\n tier: string,\n expiresAt?: string\n ): Promise<void> {\n const body: { tier: string; expiresAt?: string } = { tier };\n if (expiresAt) {\n body.expiresAt = expiresAt;\n }\n\n await this.request<{ message: string }>(\n `/api/admin/credits/users/${userId}/subscription`,\n {\n method: \"PUT\",\n body,\n }\n );\n }\n\n /**\n * Get the current credits configuration\n *\n * @returns Credits system configuration\n * @throws AuthorizationError if API key is invalid\n */\n async getConfig(): Promise<CreditsConfig> {\n return this.request<CreditsConfig>(\"/api/admin/credits/config\");\n }\n\n /**\n * Update operation costs\n *\n * @param costs - New operation costs (operation type -> cost)\n * @throws AuthorizationError if API key is invalid\n */\n async updateCosts(costs: Record<string, number>): Promise<void> {\n await this.request<{ message: string }>(\"/api/admin/credits/config/costs\", {\n method: \"PUT\",\n body: { operationCosts: costs },\n });\n }\n\n /**\n * List all users with their credits\n *\n * @param options - Pagination and filtering options\n * @returns Paginated list of users\n * @throws AuthorizationError if API key is invalid\n */\n async listUsers(options?: ListUsersOptions): Promise<ListUsersResponse> {\n return this.request<ListUsersResponse>(\"/api/admin/credits/users\", {\n params: {\n page: options?.page,\n limit: options?.limit,\n tier: options?.tier,\n search: options?.search,\n },\n });\n }\n\n /**\n * Deduct credits from a user's account\n *\n * @param userId - ID of the user\n * @param amount - Amount of credits to deduct\n * @param description - Reason for deducting credits\n * @throws AuthorizationError if API key is invalid\n */\n async deductCredits(\n userId: string,\n amount: number,\n description: string\n ): Promise<void> {\n await this.request<{ message: string; newBalance: number }>(\n `/api/admin/credits/users/${userId}/deduct`,\n {\n method: \"POST\",\n body: { amount, description },\n }\n );\n }\n\n /**\n * Get usage statistics for a user\n *\n * @param userId - ID of the user\n * @param options - Optional date range\n * @returns Usage statistics\n * @throws AuthorizationError if API key is invalid\n */\n async getUserStats(\n userId: string,\n options?: { startDate?: string; endDate?: string }\n ): Promise<{\n totalCreditsUsed: number;\n operationBreakdown: Record<string, number>;\n successRate: number;\n }> {\n return this.request<{\n totalCreditsUsed: number;\n operationBreakdown: Record<string, number>;\n successRate: number;\n }>(`/api/admin/credits/users/${userId}/stats`, {\n params: {\n startDate: options?.startDate,\n endDate: options?.endDate,\n },\n });\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACgHO,IAAM,eAAe;AAAA,EAC1B,eAAe;AAAA,EACf,sBAAsB;AAAA,EACtB,qBAAqB;AAAA,EACrB,sBAAsB;AAAA,EACtB,uBAAuB;AAAA,EACvB,qBAAqB;AAAA,EACrB,kBAAkB;AAAA,EAClB,cAAc;AAAA,EACd,eAAe;AACjB;;;AC/GO,IAAM,kBAAN,cAA8B,MAAM;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EAEhB,YACE,SACA,MACA,SAKA;AACA,UAAM,SAAS,EAAE,OAAO,SAAS,MAAM,CAAC;AACxC,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,SAAK,UAAU,SAAS;AACxB,SAAK,aAAa,SAAS;AAAA,EAC7B;AAAA;AAAA;AAAA;AAAA,EAKA,GAAG,MAAiC;AAClC,WAAO,KAAK,SAAS;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA,EAKA,SAAkC;AAChC,WAAO;AAAA,MACL,MAAM,KAAK;AAAA,MACX,SAAS,KAAK;AAAA,MACd,MAAM,KAAK;AAAA,MACX,SAAS,KAAK;AAAA,MACd,YAAY,KAAK;AAAA,IACnB;AAAA,EACF;AACF;AAKO,IAAM,eAAN,cAA2B,gBAAgB;AAAA,EAChD,YAAY,SAAiB,OAAe;AAC1C,UAAM,SAAS,aAAa,eAAe,EAAE,MAAM,CAAC;AACpD,SAAK,OAAO;AAAA,EACd;AACF;AAKO,IAAM,sBAAN,cAAkC,gBAAgB;AAAA,EACvD,YAAY,UAAkB,2BAA2B;AACvD,UAAM,SAAS,aAAa,sBAAsB,EAAE,YAAY,IAAI,CAAC;AACrE,SAAK,OAAO;AAAA,EACd;AACF;AAKO,IAAM,qBAAN,cAAiC,gBAAgB;AAAA,EACtD,YAAY,UAAkB,iBAAiB;AAC7C,UAAM,SAAS,aAAa,qBAAqB,EAAE,YAAY,IAAI,CAAC;AACpE,SAAK,OAAO;AAAA,EACd;AACF;AAKO,IAAM,2BAAN,cAAuC,gBAAgB;AAAA,EAC5C;AAAA,EACA;AAAA,EAEhB,YAAY,WAAmB,UAAkB;AAC/C;AAAA,MACE,mCAAmC,SAAS,cAAc,QAAQ;AAAA,MAClE,aAAa;AAAA,MACb;AAAA,QACE,YAAY;AAAA,QACZ,SAAS,EAAE,WAAW,SAAS;AAAA,MACjC;AAAA,IACF;AACA,SAAK,OAAO;AACZ,SAAK,YAAY;AACjB,SAAK,WAAW;AAAA,EAClB;AACF;AAKO,IAAM,2BAAN,cAAuC,gBAAgB;AAAA,EAC5C;AAAA,EAEhB,YAAY,eAAuB;AACjC;AAAA,MACE,0BAA0B,aAAa;AAAA,MACvC,aAAa;AAAA,MACb;AAAA,QACE,YAAY;AAAA,QACZ,SAAS,EAAE,cAAc;AAAA,MAC3B;AAAA,IACF;AACA,SAAK,OAAO;AACZ,SAAK,gBAAgB;AAAA,EACvB;AACF;AAKO,IAAM,0BAAN,cAAsC,gBAAgB;AAAA,EAC3C;AAAA,EAEhB,YAAY,eAAuB;AACjC;AAAA,MACE,wBAAwB,aAAa;AAAA,MACrC,aAAa;AAAA,MACb;AAAA,QACE,YAAY;AAAA,QACZ,SAAS,EAAE,cAAc;AAAA,MAC3B;AAAA,IACF;AACA,SAAK,OAAO;AACZ,SAAK,gBAAgB;AAAA,EACvB;AACF;AAKO,IAAM,kBAAN,cAA8B,gBAAgB;AAAA,EACnC;AAAA,EAEhB,YAAY,SAAiB,OAAgB;AAC3C,UAAM,SAAS,aAAa,kBAAkB;AAAA,MAC5C,YAAY;AAAA,MACZ,SAAS,QAAQ,EAAE,MAAM,IAAI;AAAA,IAC/B,CAAC;AACD,SAAK,OAAO;AACZ,SAAK,QAAQ;AAAA,EACf;AACF;AAKO,IAAM,cAAN,cAA0B,gBAAgB;AAAA,EAC/C,YAAY,UAAkB,yBAAyB;AACrD,UAAM,SAAS,aAAa,cAAc,EAAE,YAAY,IAAI,CAAC;AAC7D,SAAK,OAAO;AAAA,EACd;AACF;AAKO,SAAS,cACd,YACA,MACiB;AACjB,QAAM,YAAY,KAAK,OAAO,QAAQ;AACtC,QAAM,eAAe,KAAK,OAAO,WAAW;AAC5C,QAAM,UAAU,KAAK,OAAO;AAE5B,UAAQ,YAAY;AAAA,IAClB,KAAK;AACH,aAAO,IAAI,oBAAoB,YAAY;AAAA,IAC7C,KAAK;AACH,aAAO,IAAI,mBAAmB,YAAY;AAAA,IAC5C,KAAK;AAEH,UAAI,SAAS,cAAc,UAAa,SAAS,aAAa,QAAW;AACvE,eAAO,IAAI;AAAA,UACT,QAAQ;AAAA,UACR,QAAQ;AAAA,QACV;AAAA,MACF;AACA,aAAO,IAAI,gBAAgB,cAAc,aAAa,sBAAsB;AAAA,QAC1E;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH,KAAK;AACH,UAAI,cAAc,2BAA2B,SAAS,eAAe;AACnE,eAAO,IAAI,yBAAyB,QAAQ,aAAuB;AAAA,MACrE;AACA,aAAO,IAAI,gBAAgB,cAAc,aAAa,uBAAuB;AAAA,QAC3E;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH,KAAK;AACH,UAAI,cAAc,yBAAyB,SAAS,eAAe;AACjE,eAAO,IAAI,wBAAwB,QAAQ,aAAuB;AAAA,MACpE;AACA,aAAO,IAAI,gBAAgB,cAAc,aAAa,qBAAqB;AAAA,QACzE;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH,KAAK;AAAA,IACL,KAAK;AACH,aAAO,IAAI,gBAAgB,cAAc,SAAS,KAA2B;AAAA,IAC/E,KAAK;AAAA,IACL;AACE,UAAI,cAAc,KAAK;AACrB,eAAO,IAAI,YAAY,YAAY;AAAA,MACrC;AACA,aAAO,IAAI,gBAAgB,cAAc,aAAa,eAAe;AAAA,QACnE;AAAA,QACA;AAAA,MACF,CAAC;AAAA,EACL;AACF;;;ACzLO,IAAM,gBAAN,MAAoB;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY,QAA6B;AAEvC,SAAK,UAAU,OAAO,QAAQ,QAAQ,OAAO,EAAE;AAC/C,SAAK,eAAe,OAAO;AAC3B,SAAK,UAAU,OAAO,SAAS;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,QACZ,MACA,UAII,CAAC,GACO;AACZ,UAAM,EAAE,SAAS,OAAO,MAAM,OAAO,IAAI;AAGzC,QAAI,MAAM,GAAG,KAAK,OAAO,GAAG,IAAI;AAChC,QAAI,QAAQ;AACV,YAAM,eAAe,IAAI,gBAAgB;AACzC,iBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACjD,YAAI,UAAU,QAAW;AACvB,uBAAa,IAAI,KAAK,OAAO,KAAK,CAAC;AAAA,QACrC;AAAA,MACF;AACA,YAAM,cAAc,aAAa,SAAS;AAC1C,UAAI,aAAa;AACf,eAAO,IAAI,WAAW;AAAA,MACxB;AAAA,IACF;AAGA,UAAM,UAAkC;AAAA,MACtC,gBAAgB;AAAA,IAClB;AAGA,QAAI,KAAK,cAAc;AACrB,UAAI;AACF,cAAM,QAAQ,MAAM,KAAK,aAAa;AACtC,YAAI,OAAO;AACT,kBAAQ,eAAe,IAAI,UAAU,KAAK;AAAA,QAC5C;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF;AAGA,QAAI;AACJ,QAAI;AACF,iBAAW,MAAM,KAAK,QAAQ,KAAK;AAAA,QACjC;AAAA,QACA;AAAA,QACA,MAAM,OAAO,KAAK,UAAU,IAAI,IAAI;AAAA,MACtC,CAAC;AAAA,IACH,SAAS,OAAO;AACd,YAAM,IAAI;AAAA,QACR,iBAAiB,QAAQ,MAAM,UAAU;AAAA,QACzC,iBAAiB,QAAQ,QAAQ;AAAA,MACnC;AAAA,IACF;AAGA,UAAM,eAAgB,MAAM,SAAS,KAAK;AAM1C,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,cAAc,SAAS,QAAQ,YAAY;AAAA,IACnD;AAGA,WAAO,aAAa;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,aAAmC;AACvC,WAAO,KAAK,QAAqB,yBAAyB;AAAA,EAC5D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,kBACJ,eACA,YAC4B;AAC5B,WAAO,KAAK,QAA2B,gCAAgC;AAAA,MACrE,QAAQ;AAAA,QACN;AAAA,QACA,YAAY,YAAY,SAAS;AAAA,MACnC;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcA,MAAM,QACJ,eACA,SAK4B;AAC5B,WAAO,KAAK,QAA2B,2BAA2B;AAAA,MAChE,QAAQ;AAAA,MACR,MAAM;AAAA,QACJ;AAAA,QACA,GAAG;AAAA,MACL;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,MAAM,OAAO,eAAsC;AACjD,UAAM,KAAK,QAA6B,0BAA0B;AAAA,MAChE,QAAQ;AAAA,MACR,MAAM,EAAE,cAAc;AAAA,IACxB,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,QAAQ,eAAsC;AAClD,UAAM,KAAK,QAA6B,2BAA2B;AAAA,MACjE,QAAQ;AAAA,MACR,MAAM,EAAE,cAAc;AAAA,IACxB,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,WAAW,SAA4D;AAC3E,WAAO,KAAK,QAA8B,2BAA2B;AAAA,MACnE,QAAQ;AAAA,QACN,MAAM,SAAS;AAAA,QACf,OAAO,SAAS;AAAA,MAClB;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,sBAAuC;AAC3C,UAAM,UAAU,MAAM,KAAK,WAAW;AACtC,WAAO,QAAQ,UAAU,QAAQ,eAAe,QAAQ;AAAA,EAC1D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAyBA,MAAM,YACJ,eACA,WACA,SAKY;AACZ,UAAM,cAAc,MAAM,KAAK,QAAQ,eAAe,OAAO;AAE7D,QAAI;AACF,YAAM,SAAS,MAAM,UAAU;AAC/B,YAAM,KAAK,OAAO,YAAY,aAAa;AAC3C,aAAO;AAAA,IACT,SAAS,OAAO;AAEd,UAAI;AACF,cAAM,KAAK,QAAQ,YAAY,aAAa;AAAA,MAC9C,QAAQ;AAAA,MAER;AACA,YAAM;AAAA,IACR;AAAA,EACF;AACF;;;ACvOO,IAAM,qBAAN,MAAyB;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY,QAAkC;AAE5C,SAAK,UAAU,OAAO,QAAQ,QAAQ,OAAO,EAAE;AAC/C,SAAK,SAAS,OAAO;AACrB,SAAK,UAAU,OAAO,SAAS;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,QACZ,MACA,UAII,CAAC,GACO;AACZ,UAAM,EAAE,SAAS,OAAO,MAAM,OAAO,IAAI;AAGzC,QAAI,MAAM,GAAG,KAAK,OAAO,GAAG,IAAI;AAChC,QAAI,QAAQ;AACV,YAAM,eAAe,IAAI,gBAAgB;AACzC,iBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACjD,YAAI,UAAU,QAAW;AACvB,uBAAa,IAAI,KAAK,OAAO,KAAK,CAAC;AAAA,QACrC;AAAA,MACF;AACA,YAAM,cAAc,aAAa,SAAS;AAC1C,UAAI,aAAa;AACf,eAAO,IAAI,WAAW;AAAA,MACxB;AAAA,IACF;AAGA,UAAM,UAAkC;AAAA,MACtC,gBAAgB;AAAA,MAChB,aAAa,KAAK;AAAA,IACpB;AAGA,QAAI;AACJ,QAAI;AACF,iBAAW,MAAM,KAAK,QAAQ,KAAK;AAAA,QACjC;AAAA,QACA;AAAA,QACA,MAAM,OAAO,KAAK,UAAU,IAAI,IAAI;AAAA,MACtC,CAAC;AAAA,IACH,SAAS,OAAO;AACd,YAAM,IAAI;AAAA,QACR,iBAAiB,QAAQ,MAAM,UAAU;AAAA,QACzC,iBAAiB,QAAQ,QAAQ;AAAA,MACnC;AAAA,IACF;AAGA,UAAM,eAAgB,MAAM,SAAS,KAAK;AAM1C,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,cAAc,SAAS,QAAQ,YAAY;AAAA,IACnD;AAGA,WAAO,aAAa;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,eAAe,QAAsC;AACzD,WAAO,KAAK,QAAqB,4BAA4B,MAAM,EAAE;AAAA,EACvE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAM,WACJ,QACA,QACA,aACe;AACf,UAAM,KAAK;AAAA,MACT,4BAA4B,MAAM;AAAA,MAClC;AAAA,QACE,QAAQ;AAAA,QACR,MAAM,EAAE,QAAQ,YAAY;AAAA,MAC9B;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,mBACJ,QACA,MACA,WACe;AACf,UAAM,OAA6C,EAAE,KAAK;AAC1D,QAAI,WAAW;AACb,WAAK,YAAY;AAAA,IACnB;AAEA,UAAM,KAAK;AAAA,MACT,4BAA4B,MAAM;AAAA,MAClC;AAAA,QACE,QAAQ;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,YAAoC;AACxC,WAAO,KAAK,QAAuB,2BAA2B;AAAA,EAChE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,YAAY,OAA8C;AAC9D,UAAM,KAAK,QAA6B,mCAAmC;AAAA,MACzE,QAAQ;AAAA,MACR,MAAM,EAAE,gBAAgB,MAAM;AAAA,IAChC,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,UAAU,SAAwD;AACtE,WAAO,KAAK,QAA2B,4BAA4B;AAAA,MACjE,QAAQ;AAAA,QACN,MAAM,SAAS;AAAA,QACf,OAAO,SAAS;AAAA,QAChB,MAAM,SAAS;AAAA,QACf,QAAQ,SAAS;AAAA,MACnB;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,cACJ,QACA,QACA,aACe;AACf,UAAM,KAAK;AAAA,MACT,4BAA4B,MAAM;AAAA,MAClC;AAAA,QACE,QAAQ;AAAA,QACR,MAAM,EAAE,QAAQ,YAAY;AAAA,MAC9B;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,aACJ,QACA,SAKC;AACD,WAAO,KAAK,QAIT,4BAA4B,MAAM,UAAU;AAAA,MAC7C,QAAQ;AAAA,QACN,WAAW,SAAS;AAAA,QACpB,SAAS,SAAS;AAAA,MACpB;AAAA,IACF,CAAC;AAAA,EACH;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/sdk/index.ts","../../src/sdk/types.ts","../../src/sdk/errors.ts","../../src/core/amount.ts","../../src/sdk/client.ts","../../src/sdk/admin-client.ts"],"sourcesContent":["/**\n * Credits SDK - barrel exports\n *\n * Provides TypeScript SDK for consuming the credits REST API\n * from external services or other applications.\n */\n\n// Clients\nexport { CreditsClient } from \"./client.js\";\nexport { AdminCreditsClient } from \"./admin-client.js\";\n\n// Types\nexport type {\n CreditsClientConfig,\n AdminCreditsClientConfig,\n UserCredits,\n CreditReservation,\n CreditCheckResult,\n ReservationResult,\n PaginationOptions,\n UsageHistoryEntry,\n UsageHistoryResponse,\n ApiResponse,\n SDKErrorCodeType,\n SubscriptionTier,\n} from \"./types.js\";\n\nexport { SDKErrorCode } from \"./types.js\";\n\n// Errors\nexport {\n CreditsSDKError,\n NetworkError,\n AuthenticationError,\n AuthorizationError,\n InsufficientCreditsError,\n ReservationNotFoundError,\n ReservationExpiredError,\n ValidationError,\n ServerError,\n parseApiError,\n} from \"./errors.js\";\n\n// Admin client types\nexport type {\n CreditsConfig,\n ListUsersResponse,\n ListUsersOptions,\n} from \"./admin-client.js\";\n","/**\n * SDK types for the credits system\n *\n * These types are used by the client SDKs for REST API consumption.\n */\n\n// Re-export core types for SDK consumers\nexport type {\n PortableUserCredits as UserCredits,\n PortableReservation as CreditReservation,\n CreditCheckResult,\n SubscriptionTier,\n} from \"../core/types.js\";\n\n/**\n * Configuration for CreditsClient\n */\nexport interface CreditsClientConfig {\n /** Base URL of the credits API (e.g., \"https://api.example.com\") */\n baseUrl: string;\n /** Function to get the auth token for session-based auth */\n getAuthToken?: () => Promise<string>;\n /** Optional custom fetch function for testing */\n fetch?: typeof fetch;\n}\n\n/**\n * Configuration for AdminCreditsClient\n */\nexport interface AdminCreditsClientConfig {\n /** Base URL of the credits API */\n baseUrl: string;\n /** Admin API key */\n apiKey: string;\n /** Optional custom fetch function for testing */\n fetch?: typeof fetch;\n}\n\n/**\n * Result of a credit reservation\n */\nexport interface ReservationResult {\n /** The reservation ID */\n reservationId: string;\n /** Amount of credits reserved */\n amount: number;\n /** When the reservation expires (ISO 8601) */\n expiresAt: string;\n}\n\n/**\n * Pagination options for API requests\n */\nexport interface PaginationOptions {\n /** Page number (1-indexed) */\n page?: number;\n /** Number of items per page */\n limit?: number;\n}\n\n/**\n * Usage history entry\n */\nexport interface UsageHistoryEntry {\n /** Unique ID of the usage log */\n id: string;\n /** Type of operation */\n operationType: string;\n /** AI provider used */\n provider: string;\n /** Credits consumed */\n creditsUsed: number;\n /** Whether the operation was successful */\n success: boolean;\n /** Error message if unsuccessful */\n errorMessage?: string;\n /** ID of the related resource */\n resourceId?: string;\n /** Type of the related resource */\n resourceType?: string;\n /** When the operation occurred (ISO 8601) */\n createdAt: string;\n}\n\n/**\n * Response from usage history endpoint\n */\nexport interface UsageHistoryResponse {\n /** List of usage entries */\n entries: UsageHistoryEntry[];\n /** Whether there are more results */\n hasMore: boolean;\n /** Total count (if available) */\n total?: number;\n}\n\n/**\n * Generic API response wrapper\n */\nexport interface ApiResponse<T> {\n success: boolean;\n data?: T;\n error?: {\n code: string;\n message: string;\n details?: unknown;\n };\n}\n\n/**\n * SDK error codes\n */\nexport const SDKErrorCode = {\n NETWORK_ERROR: \"NETWORK_ERROR\",\n AUTHENTICATION_ERROR: \"AUTHENTICATION_ERROR\",\n AUTHORIZATION_ERROR: \"AUTHORIZATION_ERROR\",\n INSUFFICIENT_CREDITS: \"INSUFFICIENT_CREDITS\",\n RESERVATION_NOT_FOUND: \"RESERVATION_NOT_FOUND\",\n RESERVATION_EXPIRED: \"RESERVATION_EXPIRED\",\n VALIDATION_ERROR: \"VALIDATION_ERROR\",\n SERVER_ERROR: \"SERVER_ERROR\",\n UNKNOWN_ERROR: \"UNKNOWN_ERROR\",\n} as const;\n\nexport type SDKErrorCodeType = (typeof SDKErrorCode)[keyof typeof SDKErrorCode];\n","/**\n * SDK error classes\n *\n * Provides typed error handling for SDK operations.\n */\n\nimport { SDKErrorCode, type SDKErrorCodeType } from \"./types.js\";\n\n/**\n * Base error class for SDK errors\n */\nexport class CreditsSDKError extends Error {\n public readonly code: SDKErrorCodeType;\n public readonly details?: Record<string, unknown>;\n public readonly statusCode?: number;\n\n constructor(\n message: string,\n code: SDKErrorCodeType,\n options?: {\n details?: Record<string, unknown>;\n statusCode?: number;\n cause?: Error;\n }\n ) {\n super(message, { cause: options?.cause });\n this.name = \"CreditsSDKError\";\n this.code = code;\n this.details = options?.details;\n this.statusCode = options?.statusCode;\n }\n\n /**\n * Check if this is a specific error type\n */\n is(code: SDKErrorCodeType): boolean {\n return this.code === code;\n }\n\n /**\n * Create a JSON representation for logging\n */\n toJSON(): Record<string, unknown> {\n return {\n name: this.name,\n message: this.message,\n code: this.code,\n details: this.details,\n statusCode: this.statusCode,\n };\n }\n}\n\n/**\n * Error thrown when a network request fails\n */\nexport class NetworkError extends CreditsSDKError {\n constructor(message: string, cause?: Error) {\n super(message, SDKErrorCode.NETWORK_ERROR, { cause });\n this.name = \"NetworkError\";\n }\n}\n\n/**\n * Error thrown when authentication fails\n */\nexport class AuthenticationError extends CreditsSDKError {\n constructor(message: string = \"Authentication required\") {\n super(message, SDKErrorCode.AUTHENTICATION_ERROR, { statusCode: 401 });\n this.name = \"AuthenticationError\";\n }\n}\n\n/**\n * Error thrown when authorization fails\n */\nexport class AuthorizationError extends CreditsSDKError {\n constructor(message: string = \"Access denied\") {\n super(message, SDKErrorCode.AUTHORIZATION_ERROR, { statusCode: 403 });\n this.name = \"AuthorizationError\";\n }\n}\n\n/**\n * Error thrown when credits are insufficient\n */\nexport class InsufficientCreditsError extends CreditsSDKError {\n public readonly available: number;\n public readonly required: number;\n\n constructor(available: number, required: number) {\n super(\n `Insufficient credits: available ${available}, required ${required}`,\n SDKErrorCode.INSUFFICIENT_CREDITS,\n {\n statusCode: 402,\n details: { available, required },\n }\n );\n this.name = \"InsufficientCreditsError\";\n this.available = available;\n this.required = required;\n }\n}\n\n/**\n * Error thrown when a reservation is not found\n */\nexport class ReservationNotFoundError extends CreditsSDKError {\n public readonly reservationId: string;\n\n constructor(reservationId: string) {\n super(\n `Reservation not found: ${reservationId}`,\n SDKErrorCode.RESERVATION_NOT_FOUND,\n {\n statusCode: 404,\n details: { reservationId },\n }\n );\n this.name = \"ReservationNotFoundError\";\n this.reservationId = reservationId;\n }\n}\n\n/**\n * Error thrown when a reservation has expired\n */\nexport class ReservationExpiredError extends CreditsSDKError {\n public readonly reservationId: string;\n\n constructor(reservationId: string) {\n super(\n `Reservation expired: ${reservationId}`,\n SDKErrorCode.RESERVATION_EXPIRED,\n {\n statusCode: 410,\n details: { reservationId },\n }\n );\n this.name = \"ReservationExpiredError\";\n this.reservationId = reservationId;\n }\n}\n\n/**\n * Error thrown for validation failures\n */\nexport class ValidationError extends CreditsSDKError {\n public readonly field?: string;\n\n constructor(message: string, field?: string) {\n super(message, SDKErrorCode.VALIDATION_ERROR, {\n statusCode: 400,\n details: field ? { field } : undefined,\n });\n this.name = \"ValidationError\";\n this.field = field;\n }\n}\n\n/**\n * Error thrown for server errors\n */\nexport class ServerError extends CreditsSDKError {\n constructor(message: string = \"Internal server error\") {\n super(message, SDKErrorCode.SERVER_ERROR, { statusCode: 500 });\n this.name = \"ServerError\";\n }\n}\n\n/**\n * Parse an API error response into the appropriate SDK error\n */\nexport function parseApiError(\n statusCode: number,\n body: { error?: { code?: string; message?: string; details?: unknown } }\n): CreditsSDKError {\n const errorCode = body.error?.code || \"\";\n const errorMessage = body.error?.message || \"An error occurred\";\n const details = body.error?.details as Record<string, unknown> | undefined;\n\n switch (statusCode) {\n case 401:\n return new AuthenticationError(errorMessage);\n case 403:\n return new AuthorizationError(errorMessage);\n case 402:\n // Parse insufficient credits details\n if (details?.available !== undefined && details?.required !== undefined) {\n return new InsufficientCreditsError(\n details.available as number,\n details.required as number\n );\n }\n return new CreditsSDKError(errorMessage, SDKErrorCode.INSUFFICIENT_CREDITS, {\n statusCode,\n details,\n });\n case 404:\n if (errorCode === \"RESERVATION_NOT_FOUND\" && details?.reservationId) {\n return new ReservationNotFoundError(details.reservationId as string);\n }\n return new CreditsSDKError(errorMessage, SDKErrorCode.RESERVATION_NOT_FOUND, {\n statusCode,\n details,\n });\n case 410:\n if (errorCode === \"RESERVATION_EXPIRED\" && details?.reservationId) {\n return new ReservationExpiredError(details.reservationId as string);\n }\n return new CreditsSDKError(errorMessage, SDKErrorCode.RESERVATION_EXPIRED, {\n statusCode,\n details,\n });\n case 400:\n case 422:\n return new ValidationError(errorMessage, details?.field as string | undefined);\n case 500:\n default:\n if (statusCode >= 500) {\n return new ServerError(errorMessage);\n }\n return new CreditsSDKError(errorMessage, SDKErrorCode.UNKNOWN_ERROR, {\n statusCode,\n details,\n });\n }\n}\n","/**\n * Validation and exact comparison for credit amounts.\n *\n * Every amount ends up in a `numeric(12, 2)` column, so the ledger can only\n * represent values on the cent grid, with at most ten integer digits. A value\n * outside that grid is not \"close enough\" — PostgreSQL silently rounds it on\n * write, and the row that comes back no longer equals what the caller asked\n * for. Rejecting it before any row is touched is the only way the ledger and\n * the caller can agree on what happened.\n *\n * These live in core rather than in an adapter so every repository — shipped\n * or third-party — validates identically.\n */\n\nimport { CreditError, CreditErrorCode } from \"./errors.js\";\n\n/** `numeric(12, 2)`: 12 significant digits, 2 of them after the point. */\nexport const CREDIT_AMOUNT_SCALE = 2;\n/** Largest representable amount: ten integer digits plus two decimals. */\nexport const CREDIT_AMOUNT_MAX = 9_999_999_999.99;\nconst MAX_CENTS = 999_999_999_999;\n\n/**\n * Convert an amount to whole cents, or `null` if it is not exactly on the grid.\n *\n * `Math.round(value * 100) === value * 100` is the tempting check and it is\n * wrong: `1.005 * 100` is `100.49999999999999` in binary float, so a genuinely\n * invalid value can pass and a valid one can fail. Rounding first and then\n * confirming the value round-trips (`value === cents / 100`) is exact for\n * everything in range, because a cent count below 2^53 divided by 100 is the\n * nearest double to that decimal, which is what the literal parsed to.\n */\nexport function toCents(value: number): number | null {\n if (typeof value !== \"number\" || !Number.isFinite(value)) return null;\n const cents = Math.round(value * 100);\n if (!Number.isSafeInteger(cents)) return null;\n return value === cents / 100 ? cents : null;\n}\n\n/**\n * Add amounts on the cent grid, exactly.\n *\n * Deriving a total with `+` is not safe in a ledger that validates against the\n * grid: `0.10 + 0.20` is `0.30000000000000004` and `0.30 - 0.10` is\n * `0.19999999999999998`, neither of which is a value `numeric(12, 2)` can hold.\n * Two perfectly legal columns would therefore produce a total the validator\n * rejects with `INVALID_AMOUNT`, blaming the caller for a rounding artefact.\n * Summing whole cents is exact for every value in range, and `cents / 100` is\n * the nearest double to that decimal — the same one the literal would parse to.\n *\n * Subtract by negating: `sumAmounts(total, -amount)`. Negation is exact.\n *\n * This is arithmetic, not validation. If any input is itself off the grid the\n * float sum is returned unchanged — deliberately, so a corrupt value is never\n * rounded into legitimacy here — but that is not a guarantee the result will be\n * rejected downstream: two off-grid inputs can cancel, and `sumAmounts(0.005,\n * -0.005)` is a perfectly representable `0`. Inputs read from storage must be\n * validated as inputs, with `assertValidStoredAmount` or\n * `assertValidStoredAmountRaw`, before they are summed. Checking only the\n * derived result is not enough and never was.\n */\nexport function sumAmounts(...values: number[]): number {\n let cents = 0;\n for (const value of values) {\n const valueCents = toCents(value);\n if (valueCents === null) return floatSum(values);\n cents += valueCents;\n }\n return Number.isSafeInteger(cents) ? cents / 100 : floatSum(values);\n}\n\nfunction floatSum(values: number[]): number {\n let total = 0;\n for (const value of values) total += value;\n return total;\n}\n\n/** Amounts that may be *moved*: strictly positive and on the cent grid. */\nexport function isValidCreditAmount(value: unknown): value is number {\n if (typeof value !== \"number\") return false;\n const cents = toCents(value);\n return cents !== null && cents > 0 && cents <= MAX_CENTS;\n}\n\n/**\n * Reject an unusable amount before any row is written.\n *\n * @param context - merged into the error details, e.g. `{ userId }`.\n */\nexport function assertValidCreditAmount(\n value: unknown,\n context?: Record<string, unknown>\n): asserts value is number {\n if (isValidCreditAmount(value)) return;\n throw new CreditError(\n creditAmountReason(value),\n CreditErrorCode.INVALID_AMOUNT,\n { amount: value, max: CREDIT_AMOUNT_MAX, scale: CREDIT_AMOUNT_SCALE, ...context }\n );\n}\n\n/** Say precisely which rule the value broke, so the caller can fix it. */\nfunction creditAmountReason(value: unknown): string {\n if (typeof value !== \"number\") {\n return `Credit amount must be a number (got ${typeof value})`;\n }\n if (!Number.isFinite(value)) {\n return `Credit amount must be finite (got ${String(value)})`;\n }\n if (value <= 0) {\n return `Credit amount must be greater than zero (got ${value})`;\n }\n if (value > CREDIT_AMOUNT_MAX) {\n return `Credit amount ${value} exceeds the maximum of ${CREDIT_AMOUNT_MAX}`;\n }\n return `Credit amount ${value} is not exact to ${CREDIT_AMOUNT_SCALE} decimal places`;\n}\n\n/**\n * Amounts the ledger can *record*: on the cent grid and in range, but free to\n * be zero or negative.\n *\n * Not every numeric column holds something spendable. A release journal entry\n * records `amount: 0` because no credits changed hands, and `balanceAfter` goes\n * negative for an account that was corrected or allowed to overdraw. Forcing\n * those through {@link isValidCreditAmount} would reject legitimate ledger\n * rows; skipping validation entirely would let `Infinity` or a third decimal\n * reach the column and be silently rounded or rejected by the driver.\n */\nexport function isRepresentableAmount(value: unknown): value is number {\n if (typeof value !== \"number\") return false;\n const cents = toCents(value);\n return cents !== null && Math.abs(cents) <= MAX_CENTS;\n}\n\n/**\n * Reject a ledger value the `numeric(12, 2)` column cannot hold exactly.\n *\n * @param field - the column being written, so the error names the culprit.\n */\nexport function assertRepresentableAmount(\n value: unknown,\n field: string,\n context?: Record<string, unknown>\n): asserts value is number {\n if (isRepresentableAmount(value)) return;\n throw new CreditError(\n `${field} must be a finite number exact to ${CREDIT_AMOUNT_SCALE} decimal places ` +\n `within ±${CREDIT_AMOUNT_MAX} (got ${String(value)})`,\n CreditErrorCode.INVALID_AMOUNT,\n { field, value, max: CREDIT_AMOUNT_MAX, scale: CREDIT_AMOUNT_SCALE, ...context }\n );\n}\n\n/**\n * Check a whole set of optional ledger fields at once.\n *\n * The balance writers each take a handful of independently optional numbers,\n * and validating them one `if` at a time is where one quietly gets missed.\n * Undefined entries are skipped — they mean \"leave this column alone\".\n */\nexport function assertRepresentableFields(\n fields: Record<string, unknown>,\n context?: Record<string, unknown>\n): void {\n for (const [field, value] of Object.entries(fields)) {\n if (value === undefined) continue;\n assertRepresentableAmount(value, field, context);\n }\n}\n\n/**\n * The lowest `balance` that keeps every outstanding hold committable.\n *\n * Commit's funding guard is `balance + bonusCredits >= amount`, and every live\n * hold is counted in `reserved`, so any write that lowers `balance` must keep\n * `balance + bonusCredits >= reserved` — otherwise a hold that was fully backed\n * when it was placed throws `INSUFFICIENT_CREDITS` at commit time and is\n * stranded. Monthly resets, tier downgrades and explicit tier writes all lower\n * `balance` to a configured target; this is the floor they may not cut through.\n *\n * The floor is `reserved - bonusCredits`, never below zero: bonus credits also\n * fund commits, so they count toward the backing.\n */\nexport function backedBalanceFloor(reserved: number, bonusCredits: number): number {\n const floor = Math.max(0, sumAmounts(reserved, -bonusCredits));\n // A floor beyond the representable range means the row itself is corrupt —\n // e.g. a large negative `bonusCredits` pushing `reserved - bonusCredits`\n // past the `numeric(12,2)` cap. Refuse loudly: writing it would succeed in\n // memory and be rejected by the SQL column, splitting the adapters.\n assertRepresentableAmount(floor, \"backedBalanceFloor\", { reserved, bonusCredits });\n return floor;\n}\n\n/**\n * Validate a limit derived from tier configuration.\n *\n * `Infinity` is this codebase's sentinel for an unlimited tier, so it is\n * accepted here; {@link storedMonthlyLimit} decides what actually reaches the\n * column. Every other value ends up in `numeric(12, 2)` and has to fit, so a\n * tier configured with `monthlyCredits: 1.005` is caught here rather than\n * silently rounded to `1.01` by PostgreSQL.\n */\nexport function assertRepresentableTierAmount(\n value: unknown,\n field: string,\n context?: Record<string, unknown>\n): void {\n if (value === Number.POSITIVE_INFINITY) return;\n assertRepresentableAmount(value, field, context);\n}\n\n/**\n * Validate a stored amount in the representation storage actually returned.\n *\n * Checking the mapped float is not enough, because the mapping is lossy in both\n * directions. A `numeric` column with no scale constraint can hold\n * `9999999999.9900001`; `Number()` rounds that to `9999999999.99`, which is a\n * perfectly valid amount, so a validator looking at the mapped value sees\n * nothing wrong and the transition proceeds — while PostgreSQL goes on doing\n * exact arithmetic with the digits JavaScript threw away. `NaN` and values past\n * double range are worse: the mapper turns them into `0`, which misreports what\n * the row holds.\n *\n * So this runs on the driver's own output — a string, for `numeric` — and uses\n * exact integer arithmetic. It returns the amount as a float only after\n * establishing that the float is a faithful representation of it.\n *\n * @returns the stored amount, exactly representable as a JS number.\n */\n/**\n * How an unlimited tier's monthly limit is stored.\n *\n * `getConfigMonthlyLimit` returns `Infinity` for an unlimited tier, and no\n * numeric column can hold that. Both adapters therefore store the ceiling of\n * the representable range instead: any real usage compares below it, so\n * \"unlimited\" keeps behaving like unlimited, and a reader that knows nothing\n * about the sentinel sees a very large allowance rather than a wrong one.\n *\n * This exists so the two adapters cannot drift. The SQL adapter previously\n * stored `0` here, which reads as *no* allowance — the exact opposite — while\n * the in-memory adapter kept `Infinity`.\n */\nexport function storedMonthlyLimit(limit: number): number {\n return Number.isFinite(limit) ? limit : CREDIT_AMOUNT_MAX;\n}\n\nexport function assertValidStoredAmountRaw(\n raw: unknown,\n context: Record<string, unknown>\n): number {\n const cents = numericToCents(raw);\n if (cents !== null && cents > 0n && cents <= BigInt(MAX_CENTS)) {\n return Number(cents) / 100;\n }\n throw new CreditError(\n `Stored credit amount ${String(raw)} is not a valid amount, so this ` +\n \"transition was refused before any state changed. The row must be \" +\n \"repaired before it can be committed, released or expired.\",\n CreditErrorCode.INVALID_AMOUNT,\n {\n amount: typeof raw === \"string\" ? raw : String(raw),\n reason: \"corrupt_stored_amount\",\n max: CREDIT_AMOUNT_MAX,\n scale: CREDIT_AMOUNT_SCALE,\n ...context,\n }\n );\n}\n\n/**\n * Reject a *persisted* amount that the ledger cannot honour.\n *\n * A reservation row is written once and read back on every transition, and\n * nothing stops a direct SQL write, a botched migration, or an older version of\n * this library from leaving an amount the invariants cannot reason about. A\n * negative one is the dangerous case: `reserved >= amount` is trivially true\n * for it, and `balance - (-10)` *adds* credits, so a transition that trusts the\n * stored value mints money. Every transition therefore re-validates what it\n * locked before it changes anything.\n *\n * Deliberately fails release and expire as well as commit. Returning a hold of\n * an unknown size is not obviously safer than spending one — it hands back\n * coverage that may never have been taken — so a corrupt row stops moving\n * entirely and waits for an operator, rather than being partially honoured.\n *\n * This is the float-typed form, used by the in-memory adapter where the stored\n * value *is* a JS number and no conversion has happened. A repository whose\n * storage returns some other representation must use\n * {@link assertValidStoredAmountRaw} instead, or the conversion will hide the\n * corruption from the check.\n */\nexport function assertValidStoredAmount(\n value: unknown,\n context: Record<string, unknown>\n): asserts value is number {\n if (isValidCreditAmount(value)) return;\n throw new CreditError(\n `Stored credit amount ${String(value)} is not a valid amount, so this ` +\n \"transition was refused before any state changed. The row must be \" +\n \"repaired before it can be committed, released or expired.\",\n CreditErrorCode.INVALID_AMOUNT,\n { amount: value, reason: \"corrupt_stored_amount\", max: CREDIT_AMOUNT_MAX, ...context }\n );\n}\n\n/**\n * Parse a value read back from a `numeric` column into exact cents.\n *\n * Drivers return `numeric` as a string precisely so no precision is lost in\n * transit; comparing the parsed floats would throw that away again. Returns\n * `null` for anything unparseable so callers can treat it as \"not equal\"\n * rather than crashing on malformed ledger data.\n */\nexport function numericToCents(value: unknown): bigint | null {\n if (typeof value === \"number\") {\n const cents = toCents(value);\n return cents === null ? null : BigInt(cents);\n }\n if (typeof value !== \"string\") return null;\n\n const match = /^([+-]?)(\\d+)(?:\\.(\\d*))?$/.exec(value.trim());\n if (!match) return null;\n\n const [, sign, whole, fraction = \"\"] = match;\n // More than two decimals is only acceptable when the extra digits are zeros;\n // anything else never came from a numeric(12,2) column.\n if (/[1-9]/.test(fraction.slice(CREDIT_AMOUNT_SCALE))) return null;\n\n const cents =\n BigInt(whole) * 100n + BigInt(fraction.slice(0, CREDIT_AMOUNT_SCALE).padEnd(CREDIT_AMOUNT_SCALE, \"0\"));\n return sign === \"-\" ? -cents : cents;\n}\n\n/**\n * Exact equality for two amounts that may be numbers or `numeric` strings.\n *\n * Unparseable input is *not* equal to anything, including itself — a\n * comparison against corrupt data must never come back \"matches\".\n */\nexport function sameAmount(a: unknown, b: unknown): boolean {\n const left = numericToCents(a);\n const right = numericToCents(b);\n return left !== null && right !== null && left === right;\n}\n","/**\n * CreditsClient - REST API client for the credits system\n *\n * Provides a type-safe interface for consuming credits API endpoints\n * from external services or other applications.\n *\n * @example\n * ```typescript\n * const client = new CreditsClient({\n * baseUrl: process.env.CREDITS_API_URL,\n * getAuthToken: async () => session.accessToken,\n * });\n *\n * // Check if user has sufficient credits\n * const check = await client.checkAvailability(\"story_generation\");\n * if (!check.hasCredits) {\n * console.log(\"Insufficient credits\");\n * return;\n * }\n *\n * // Two-phase commit pattern\n * const reservation = await client.reserve(\"story_generation\");\n * try {\n * const result = await doExpensiveWork();\n * await client.commit(reservation.reservationId);\n * return result;\n * } catch (error) {\n * await client.release(reservation.reservationId);\n * throw error;\n * }\n * ```\n */\n\nimport type {\n CreditsClientConfig,\n UserCredits,\n CreditCheckResult,\n ReservationResult,\n UsageHistoryResponse,\n PaginationOptions,\n} from \"./types.js\";\nimport { parseApiError, NetworkError } from \"./errors.js\";\nimport { sumAmounts } from \"../core/amount.js\";\n\nexport class CreditsClient {\n private readonly baseUrl: string;\n private readonly getAuthToken?: () => Promise<string>;\n private readonly fetchFn: typeof fetch;\n\n constructor(config: CreditsClientConfig) {\n // Ensure baseUrl doesn't have trailing slash\n this.baseUrl = config.baseUrl.replace(/\\/$/, \"\");\n this.getAuthToken = config.getAuthToken;\n this.fetchFn = config.fetch ?? fetch;\n }\n\n /**\n * Make an authenticated request to the API\n */\n private async request<T>(\n path: string,\n options: {\n method?: \"GET\" | \"POST\" | \"PUT\" | \"DELETE\";\n body?: unknown;\n params?: Record<string, string | number | undefined>;\n } = {}\n ): Promise<T> {\n const { method = \"GET\", body, params } = options;\n\n // Build URL with query params\n let url = `${this.baseUrl}${path}`;\n if (params) {\n const searchParams = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n if (value !== undefined) {\n searchParams.set(key, String(value));\n }\n }\n const queryString = searchParams.toString();\n if (queryString) {\n url += `?${queryString}`;\n }\n }\n\n // Build headers\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n\n // Add auth token if available\n if (this.getAuthToken) {\n try {\n const token = await this.getAuthToken();\n if (token) {\n headers[\"Authorization\"] = `Bearer ${token}`;\n }\n } catch {\n // Auth token not available, proceed without it\n }\n }\n\n // Make the request\n let response: Response;\n try {\n response = await this.fetchFn(url, {\n method,\n headers,\n body: body ? JSON.stringify(body) : undefined,\n });\n } catch (error) {\n throw new NetworkError(\n error instanceof Error ? error.message : \"Network request failed\",\n error instanceof Error ? error : undefined\n );\n }\n\n // Parse response\n const responseBody = (await response.json()) as {\n data?: T;\n error?: { code?: string; message?: string; details?: unknown };\n };\n\n // Handle errors\n if (!response.ok) {\n throw parseApiError(response.status, responseBody);\n }\n\n // Return data\n return responseBody.data as T;\n }\n\n /**\n * Get the current user's credit balance\n *\n * @returns User's credit information\n * @throws AuthenticationError if not authenticated\n */\n async getBalance(): Promise<UserCredits> {\n return this.request<UserCredits>(\"/api/v1/credits/balance\");\n }\n\n /**\n * Check if the user has sufficient credits for an operation\n *\n * @param operationType - Type of operation to check\n * @param customCost - Optional custom cost to check against\n * @returns Credit availability status\n * @throws AuthenticationError if not authenticated\n */\n async checkAvailability(\n operationType: string,\n customCost?: number\n ): Promise<CreditCheckResult> {\n return this.request<CreditCheckResult>(\"/api/v1/credits/availability\", {\n params: {\n operationType,\n customCost: customCost?.toString(),\n },\n });\n }\n\n /**\n * Reserve credits for an operation\n *\n * This is the first step in the two-phase commit pattern.\n * Reserved credits are held for 5 minutes before expiring.\n *\n * @param operationType - Type of operation\n * @param options - Optional reservation options\n * @returns Reservation details including ID and expiry\n * @throws InsufficientCreditsError if not enough credits\n * @throws AuthenticationError if not authenticated\n */\n async reserve(\n operationType: string,\n options?: {\n customCost?: number;\n resourceId?: string;\n resourceType?: string;\n }\n ): Promise<ReservationResult> {\n return this.request<ReservationResult>(\"/api/v1/credits/reserve\", {\n method: \"POST\",\n body: {\n operationType,\n ...options,\n },\n });\n }\n\n /**\n * Commit a reservation (deduct credits)\n *\n * This is the second step in the two-phase commit pattern.\n * Call this after your operation succeeds.\n *\n * @param reservationId - ID of the reservation to commit\n * @throws ReservationNotFoundError if reservation doesn't exist\n * @throws ReservationExpiredError if reservation has expired\n * @throws AuthenticationError if not authenticated\n */\n async commit(reservationId: string): Promise<void> {\n await this.request<{ message: string }>(\"/api/v1/credits/commit\", {\n method: \"POST\",\n body: { reservationId },\n });\n }\n\n /**\n * Release a reservation (refund credits)\n *\n * Call this if your operation fails after reserving credits.\n *\n * @param reservationId - ID of the reservation to release\n * @throws ReservationNotFoundError if reservation doesn't exist\n * @throws AuthenticationError if not authenticated\n */\n async release(reservationId: string): Promise<void> {\n await this.request<{ message: string }>(\"/api/v1/credits/release\", {\n method: \"POST\",\n body: { reservationId },\n });\n }\n\n /**\n * Get usage history for the current user\n *\n * @param options - Optional pagination options\n * @returns Paginated list of usage entries\n * @throws AuthenticationError if not authenticated\n */\n async getHistory(options?: PaginationOptions): Promise<UsageHistoryResponse> {\n return this.request<UsageHistoryResponse>(\"/api/v1/credits/history\", {\n params: {\n page: options?.page,\n limit: options?.limit,\n },\n });\n }\n\n /**\n * Get the available credits (balance + bonus - reserved)\n *\n * Convenience method that calculates available credits from balance.\n *\n * @returns Number of available credits\n * @throws AuthenticationError if not authenticated\n */\n async getAvailableCredits(): Promise<number> {\n const balance = await this.getBalance();\n return sumAmounts(balance.balance, balance.bonusCredits, -balance.reserved);\n }\n\n /**\n * Execute an operation with automatic credit handling\n *\n * This is a convenience method that handles the full two-phase commit\n * pattern automatically.\n *\n * @param operationType - Type of operation\n * @param operation - The operation to execute\n * @param options - Optional reservation options\n * @returns Result of the operation\n * @throws InsufficientCreditsError if not enough credits\n * @throws AuthenticationError if not authenticated\n *\n * @example\n * ```typescript\n * const result = await client.withCredits(\n * \"story_generation\",\n * async () => {\n * return generateStory(data);\n * }\n * );\n * ```\n */\n async withCredits<T>(\n operationType: string,\n operation: () => Promise<T>,\n options?: {\n customCost?: number;\n resourceId?: string;\n resourceType?: string;\n }\n ): Promise<T> {\n const reservation = await this.reserve(operationType, options);\n\n try {\n const result = await operation();\n await this.commit(reservation.reservationId);\n return result;\n } catch (error) {\n // Always attempt to release on failure\n try {\n await this.release(reservation.reservationId);\n } catch {\n // Ignore release errors, original error is more important\n }\n throw error;\n }\n }\n}\n","/**\n * AdminCreditsClient - Admin REST API client for the credits system\n *\n * Provides administrative access to the credits system for managing\n * user credits, configurations, and subscriptions.\n *\n * @example\n * ```typescript\n * const adminClient = new AdminCreditsClient({\n * baseUrl: process.env.CREDITS_API_URL,\n * apiKey: process.env.CREDITS_ADMIN_API_KEY,\n * });\n *\n * // Add credits to a user\n * await adminClient.addCredits(\"user-123\", 100, \"Support credit grant\");\n *\n * // Update subscription tier\n * await adminClient.updateSubscription(\"user-123\", \"premium\", \"2027-01-01T00:00:00Z\");\n *\n * // Update operation costs\n * await adminClient.updateCosts({\n * story_generation: 8,\n * image_generation: 12,\n * });\n * ```\n */\n\nimport type { AdminCreditsClientConfig, UserCredits } from \"./types.js\";\nimport { parseApiError, NetworkError } from \"./errors.js\";\n\n/**\n * Credits configuration returned by the API\n */\nexport interface CreditsConfig {\n operationCosts: Record<string, number>;\n tierConfigs: Record<\n string,\n {\n monthlyLimit: number;\n features: string[];\n }\n >;\n}\n\n/**\n * Response from listing users\n */\nexport interface ListUsersResponse {\n users: Array<{\n userId: string;\n balance: number;\n bonusCredits?: number;\n tier: string;\n monthlyUsed?: number;\n }>;\n hasMore: boolean;\n total?: number;\n}\n\n/**\n * Options for listing users\n */\nexport interface ListUsersOptions {\n page?: number;\n limit?: number;\n tier?: string;\n search?: string;\n}\n\nexport class AdminCreditsClient {\n private readonly baseUrl: string;\n private readonly apiKey: string;\n private readonly fetchFn: typeof fetch;\n\n constructor(config: AdminCreditsClientConfig) {\n // Ensure baseUrl doesn't have trailing slash\n this.baseUrl = config.baseUrl.replace(/\\/$/, \"\");\n this.apiKey = config.apiKey;\n this.fetchFn = config.fetch ?? fetch;\n }\n\n /**\n * Make an authenticated admin request to the API\n */\n private async request<T>(\n path: string,\n options: {\n method?: \"GET\" | \"POST\" | \"PUT\" | \"DELETE\";\n body?: unknown;\n params?: Record<string, string | number | undefined>;\n } = {}\n ): Promise<T> {\n const { method = \"GET\", body, params } = options;\n\n // Build URL with query params\n let url = `${this.baseUrl}${path}`;\n if (params) {\n const searchParams = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n if (value !== undefined) {\n searchParams.set(key, String(value));\n }\n }\n const queryString = searchParams.toString();\n if (queryString) {\n url += `?${queryString}`;\n }\n }\n\n // Build headers with admin API key\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"x-api-key\": this.apiKey,\n };\n\n // Make the request\n let response: Response;\n try {\n response = await this.fetchFn(url, {\n method,\n headers,\n body: body ? JSON.stringify(body) : undefined,\n });\n } catch (error) {\n throw new NetworkError(\n error instanceof Error ? error.message : \"Network request failed\",\n error instanceof Error ? error : undefined\n );\n }\n\n // Parse response\n const responseBody = (await response.json()) as {\n data?: T;\n error?: { code?: string; message?: string; details?: unknown };\n };\n\n // Handle errors\n if (!response.ok) {\n throw parseApiError(response.status, responseBody);\n }\n\n // Return data\n return responseBody.data as T;\n }\n\n /**\n * Get a specific user's credits\n *\n * @param userId - ID of the user\n * @returns User's credit information\n * @throws AuthorizationError if API key is invalid\n */\n async getUserCredits(userId: string): Promise<UserCredits> {\n return this.request<UserCredits>(`/api/admin/credits/users/${userId}`);\n }\n\n /**\n * Add credits to a user's account\n *\n * Credits are added to bonusCredits, which don't expire on monthly reset.\n *\n * @param userId - ID of the user\n * @param amount - Amount of credits to add\n * @param description - Reason for adding credits\n * @throws AuthorizationError if API key is invalid\n */\n async addCredits(\n userId: string,\n amount: number,\n description: string\n ): Promise<void> {\n await this.request<{ message: string; newBalance: number }>(\n `/api/admin/credits/users/${userId}/add`,\n {\n method: \"POST\",\n body: { amount, description },\n }\n );\n }\n\n /**\n * Update a user's subscription tier\n *\n * @param userId - ID of the user\n * @param tier - New subscription tier\n * @param expiresAt - Optional expiration date (ISO 8601)\n * @throws AuthorizationError if API key is invalid\n */\n async updateSubscription(\n userId: string,\n tier: string,\n expiresAt?: string\n ): Promise<void> {\n const body: { tier: string; expiresAt?: string } = { tier };\n if (expiresAt) {\n body.expiresAt = expiresAt;\n }\n\n await this.request<{ message: string }>(\n `/api/admin/credits/users/${userId}/subscription`,\n {\n method: \"PUT\",\n body,\n }\n );\n }\n\n /**\n * Get the current credits configuration\n *\n * @returns Credits system configuration\n * @throws AuthorizationError if API key is invalid\n */\n async getConfig(): Promise<CreditsConfig> {\n return this.request<CreditsConfig>(\"/api/admin/credits/config\");\n }\n\n /**\n * Update operation costs\n *\n * @param costs - New operation costs (operation type -> cost)\n * @throws AuthorizationError if API key is invalid\n */\n async updateCosts(costs: Record<string, number>): Promise<void> {\n await this.request<{ message: string }>(\"/api/admin/credits/config/costs\", {\n method: \"PUT\",\n body: { operationCosts: costs },\n });\n }\n\n /**\n * List all users with their credits\n *\n * @param options - Pagination and filtering options\n * @returns Paginated list of users\n * @throws AuthorizationError if API key is invalid\n */\n async listUsers(options?: ListUsersOptions): Promise<ListUsersResponse> {\n return this.request<ListUsersResponse>(\"/api/admin/credits/users\", {\n params: {\n page: options?.page,\n limit: options?.limit,\n tier: options?.tier,\n search: options?.search,\n },\n });\n }\n\n /**\n * Deduct credits from a user's account\n *\n * @param userId - ID of the user\n * @param amount - Amount of credits to deduct\n * @param description - Reason for deducting credits\n * @throws AuthorizationError if API key is invalid\n */\n async deductCredits(\n userId: string,\n amount: number,\n description: string\n ): Promise<void> {\n await this.request<{ message: string; newBalance: number }>(\n `/api/admin/credits/users/${userId}/deduct`,\n {\n method: \"POST\",\n body: { amount, description },\n }\n );\n }\n\n /**\n * Get usage statistics for a user\n *\n * @param userId - ID of the user\n * @param options - Optional date range\n * @returns Usage statistics\n * @throws AuthorizationError if API key is invalid\n */\n async getUserStats(\n userId: string,\n options?: { startDate?: string; endDate?: string }\n ): Promise<{\n totalCreditsUsed: number;\n operationBreakdown: Record<string, number>;\n successRate: number;\n }> {\n return this.request<{\n totalCreditsUsed: number;\n operationBreakdown: Record<string, number>;\n successRate: number;\n }>(`/api/admin/credits/users/${userId}/stats`, {\n params: {\n startDate: options?.startDate,\n endDate: options?.endDate,\n },\n });\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,5 +1,5 @@
1
- import { b as PortableUserCredits, g as CreditCheckResult } from '../types-CjLj3GW5.cjs';
2
- export { c as CreditReservation, S as SubscriptionTier } from '../types-CjLj3GW5.cjs';
1
+ import { c as PortableUserCredits, g as CreditCheckResult } from '../types-DJ8yqaVr.cjs';
2
+ export { a as CreditReservation, S as SubscriptionTier } from '../types-DJ8yqaVr.cjs';
3
3
 
4
4
  /**
5
5
  * SDK types for the credits system
@@ -1,5 +1,5 @@
1
- import { b as PortableUserCredits, g as CreditCheckResult } from '../types-CjLj3GW5.js';
2
- export { c as CreditReservation, S as SubscriptionTier } from '../types-CjLj3GW5.js';
1
+ import { c as PortableUserCredits, g as CreditCheckResult } from '../types-DJ8yqaVr.js';
2
+ export { a as CreditReservation, S as SubscriptionTier } from '../types-DJ8yqaVr.js';
3
3
 
4
4
  /**
5
5
  * SDK types for the credits system
package/dist/sdk/index.js CHANGED
@@ -175,6 +175,28 @@ function parseApiError(statusCode, body) {
175
175
  }
176
176
  }
177
177
 
178
+ // src/core/amount.ts
179
+ function toCents(value) {
180
+ if (typeof value !== "number" || !Number.isFinite(value)) return null;
181
+ const cents = Math.round(value * 100);
182
+ if (!Number.isSafeInteger(cents)) return null;
183
+ return value === cents / 100 ? cents : null;
184
+ }
185
+ function sumAmounts(...values) {
186
+ let cents = 0;
187
+ for (const value of values) {
188
+ const valueCents = toCents(value);
189
+ if (valueCents === null) return floatSum(values);
190
+ cents += valueCents;
191
+ }
192
+ return Number.isSafeInteger(cents) ? cents / 100 : floatSum(values);
193
+ }
194
+ function floatSum(values) {
195
+ let total = 0;
196
+ for (const value of values) total += value;
197
+ return total;
198
+ }
199
+
178
200
  // src/sdk/client.ts
179
201
  var CreditsClient = class {
180
202
  baseUrl;
@@ -337,7 +359,7 @@ var CreditsClient = class {
337
359
  */
338
360
  async getAvailableCredits() {
339
361
  const balance = await this.getBalance();
340
- return balance.balance + balance.bonusCredits - balance.reserved;
362
+ return sumAmounts(balance.balance, balance.bonusCredits, -balance.reserved);
341
363
  }
342
364
  /**
343
365
  * Execute an operation with automatic credit handling
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/sdk/types.ts","../../src/sdk/errors.ts","../../src/sdk/client.ts","../../src/sdk/admin-client.ts"],"sourcesContent":["/**\n * SDK types for the credits system\n *\n * These types are used by the client SDKs for REST API consumption.\n */\n\n// Re-export core types for SDK consumers\nexport type {\n PortableUserCredits as UserCredits,\n PortableReservation as CreditReservation,\n CreditCheckResult,\n SubscriptionTier,\n} from \"../core/types.js\";\n\n/**\n * Configuration for CreditsClient\n */\nexport interface CreditsClientConfig {\n /** Base URL of the credits API (e.g., \"https://api.example.com\") */\n baseUrl: string;\n /** Function to get the auth token for session-based auth */\n getAuthToken?: () => Promise<string>;\n /** Optional custom fetch function for testing */\n fetch?: typeof fetch;\n}\n\n/**\n * Configuration for AdminCreditsClient\n */\nexport interface AdminCreditsClientConfig {\n /** Base URL of the credits API */\n baseUrl: string;\n /** Admin API key */\n apiKey: string;\n /** Optional custom fetch function for testing */\n fetch?: typeof fetch;\n}\n\n/**\n * Result of a credit reservation\n */\nexport interface ReservationResult {\n /** The reservation ID */\n reservationId: string;\n /** Amount of credits reserved */\n amount: number;\n /** When the reservation expires (ISO 8601) */\n expiresAt: string;\n}\n\n/**\n * Pagination options for API requests\n */\nexport interface PaginationOptions {\n /** Page number (1-indexed) */\n page?: number;\n /** Number of items per page */\n limit?: number;\n}\n\n/**\n * Usage history entry\n */\nexport interface UsageHistoryEntry {\n /** Unique ID of the usage log */\n id: string;\n /** Type of operation */\n operationType: string;\n /** AI provider used */\n provider: string;\n /** Credits consumed */\n creditsUsed: number;\n /** Whether the operation was successful */\n success: boolean;\n /** Error message if unsuccessful */\n errorMessage?: string;\n /** ID of the related resource */\n resourceId?: string;\n /** Type of the related resource */\n resourceType?: string;\n /** When the operation occurred (ISO 8601) */\n createdAt: string;\n}\n\n/**\n * Response from usage history endpoint\n */\nexport interface UsageHistoryResponse {\n /** List of usage entries */\n entries: UsageHistoryEntry[];\n /** Whether there are more results */\n hasMore: boolean;\n /** Total count (if available) */\n total?: number;\n}\n\n/**\n * Generic API response wrapper\n */\nexport interface ApiResponse<T> {\n success: boolean;\n data?: T;\n error?: {\n code: string;\n message: string;\n details?: unknown;\n };\n}\n\n/**\n * SDK error codes\n */\nexport const SDKErrorCode = {\n NETWORK_ERROR: \"NETWORK_ERROR\",\n AUTHENTICATION_ERROR: \"AUTHENTICATION_ERROR\",\n AUTHORIZATION_ERROR: \"AUTHORIZATION_ERROR\",\n INSUFFICIENT_CREDITS: \"INSUFFICIENT_CREDITS\",\n RESERVATION_NOT_FOUND: \"RESERVATION_NOT_FOUND\",\n RESERVATION_EXPIRED: \"RESERVATION_EXPIRED\",\n VALIDATION_ERROR: \"VALIDATION_ERROR\",\n SERVER_ERROR: \"SERVER_ERROR\",\n UNKNOWN_ERROR: \"UNKNOWN_ERROR\",\n} as const;\n\nexport type SDKErrorCodeType = (typeof SDKErrorCode)[keyof typeof SDKErrorCode];\n","/**\n * SDK error classes\n *\n * Provides typed error handling for SDK operations.\n */\n\nimport { SDKErrorCode, type SDKErrorCodeType } from \"./types.js\";\n\n/**\n * Base error class for SDK errors\n */\nexport class CreditsSDKError extends Error {\n public readonly code: SDKErrorCodeType;\n public readonly details?: Record<string, unknown>;\n public readonly statusCode?: number;\n\n constructor(\n message: string,\n code: SDKErrorCodeType,\n options?: {\n details?: Record<string, unknown>;\n statusCode?: number;\n cause?: Error;\n }\n ) {\n super(message, { cause: options?.cause });\n this.name = \"CreditsSDKError\";\n this.code = code;\n this.details = options?.details;\n this.statusCode = options?.statusCode;\n }\n\n /**\n * Check if this is a specific error type\n */\n is(code: SDKErrorCodeType): boolean {\n return this.code === code;\n }\n\n /**\n * Create a JSON representation for logging\n */\n toJSON(): Record<string, unknown> {\n return {\n name: this.name,\n message: this.message,\n code: this.code,\n details: this.details,\n statusCode: this.statusCode,\n };\n }\n}\n\n/**\n * Error thrown when a network request fails\n */\nexport class NetworkError extends CreditsSDKError {\n constructor(message: string, cause?: Error) {\n super(message, SDKErrorCode.NETWORK_ERROR, { cause });\n this.name = \"NetworkError\";\n }\n}\n\n/**\n * Error thrown when authentication fails\n */\nexport class AuthenticationError extends CreditsSDKError {\n constructor(message: string = \"Authentication required\") {\n super(message, SDKErrorCode.AUTHENTICATION_ERROR, { statusCode: 401 });\n this.name = \"AuthenticationError\";\n }\n}\n\n/**\n * Error thrown when authorization fails\n */\nexport class AuthorizationError extends CreditsSDKError {\n constructor(message: string = \"Access denied\") {\n super(message, SDKErrorCode.AUTHORIZATION_ERROR, { statusCode: 403 });\n this.name = \"AuthorizationError\";\n }\n}\n\n/**\n * Error thrown when credits are insufficient\n */\nexport class InsufficientCreditsError extends CreditsSDKError {\n public readonly available: number;\n public readonly required: number;\n\n constructor(available: number, required: number) {\n super(\n `Insufficient credits: available ${available}, required ${required}`,\n SDKErrorCode.INSUFFICIENT_CREDITS,\n {\n statusCode: 402,\n details: { available, required },\n }\n );\n this.name = \"InsufficientCreditsError\";\n this.available = available;\n this.required = required;\n }\n}\n\n/**\n * Error thrown when a reservation is not found\n */\nexport class ReservationNotFoundError extends CreditsSDKError {\n public readonly reservationId: string;\n\n constructor(reservationId: string) {\n super(\n `Reservation not found: ${reservationId}`,\n SDKErrorCode.RESERVATION_NOT_FOUND,\n {\n statusCode: 404,\n details: { reservationId },\n }\n );\n this.name = \"ReservationNotFoundError\";\n this.reservationId = reservationId;\n }\n}\n\n/**\n * Error thrown when a reservation has expired\n */\nexport class ReservationExpiredError extends CreditsSDKError {\n public readonly reservationId: string;\n\n constructor(reservationId: string) {\n super(\n `Reservation expired: ${reservationId}`,\n SDKErrorCode.RESERVATION_EXPIRED,\n {\n statusCode: 410,\n details: { reservationId },\n }\n );\n this.name = \"ReservationExpiredError\";\n this.reservationId = reservationId;\n }\n}\n\n/**\n * Error thrown for validation failures\n */\nexport class ValidationError extends CreditsSDKError {\n public readonly field?: string;\n\n constructor(message: string, field?: string) {\n super(message, SDKErrorCode.VALIDATION_ERROR, {\n statusCode: 400,\n details: field ? { field } : undefined,\n });\n this.name = \"ValidationError\";\n this.field = field;\n }\n}\n\n/**\n * Error thrown for server errors\n */\nexport class ServerError extends CreditsSDKError {\n constructor(message: string = \"Internal server error\") {\n super(message, SDKErrorCode.SERVER_ERROR, { statusCode: 500 });\n this.name = \"ServerError\";\n }\n}\n\n/**\n * Parse an API error response into the appropriate SDK error\n */\nexport function parseApiError(\n statusCode: number,\n body: { error?: { code?: string; message?: string; details?: unknown } }\n): CreditsSDKError {\n const errorCode = body.error?.code || \"\";\n const errorMessage = body.error?.message || \"An error occurred\";\n const details = body.error?.details as Record<string, unknown> | undefined;\n\n switch (statusCode) {\n case 401:\n return new AuthenticationError(errorMessage);\n case 403:\n return new AuthorizationError(errorMessage);\n case 402:\n // Parse insufficient credits details\n if (details?.available !== undefined && details?.required !== undefined) {\n return new InsufficientCreditsError(\n details.available as number,\n details.required as number\n );\n }\n return new CreditsSDKError(errorMessage, SDKErrorCode.INSUFFICIENT_CREDITS, {\n statusCode,\n details,\n });\n case 404:\n if (errorCode === \"RESERVATION_NOT_FOUND\" && details?.reservationId) {\n return new ReservationNotFoundError(details.reservationId as string);\n }\n return new CreditsSDKError(errorMessage, SDKErrorCode.RESERVATION_NOT_FOUND, {\n statusCode,\n details,\n });\n case 410:\n if (errorCode === \"RESERVATION_EXPIRED\" && details?.reservationId) {\n return new ReservationExpiredError(details.reservationId as string);\n }\n return new CreditsSDKError(errorMessage, SDKErrorCode.RESERVATION_EXPIRED, {\n statusCode,\n details,\n });\n case 400:\n case 422:\n return new ValidationError(errorMessage, details?.field as string | undefined);\n case 500:\n default:\n if (statusCode >= 500) {\n return new ServerError(errorMessage);\n }\n return new CreditsSDKError(errorMessage, SDKErrorCode.UNKNOWN_ERROR, {\n statusCode,\n details,\n });\n }\n}\n","/**\n * CreditsClient - REST API client for the credits system\n *\n * Provides a type-safe interface for consuming credits API endpoints\n * from external services or other applications.\n *\n * @example\n * ```typescript\n * const client = new CreditsClient({\n * baseUrl: process.env.CREDITS_API_URL,\n * getAuthToken: async () => session.accessToken,\n * });\n *\n * // Check if user has sufficient credits\n * const check = await client.checkAvailability(\"story_generation\");\n * if (!check.hasCredits) {\n * console.log(\"Insufficient credits\");\n * return;\n * }\n *\n * // Two-phase commit pattern\n * const reservation = await client.reserve(\"story_generation\");\n * try {\n * const result = await doExpensiveWork();\n * await client.commit(reservation.reservationId);\n * return result;\n * } catch (error) {\n * await client.release(reservation.reservationId);\n * throw error;\n * }\n * ```\n */\n\nimport type {\n CreditsClientConfig,\n UserCredits,\n CreditCheckResult,\n ReservationResult,\n UsageHistoryResponse,\n PaginationOptions,\n} from \"./types.js\";\nimport { parseApiError, NetworkError } from \"./errors.js\";\n\nexport class CreditsClient {\n private readonly baseUrl: string;\n private readonly getAuthToken?: () => Promise<string>;\n private readonly fetchFn: typeof fetch;\n\n constructor(config: CreditsClientConfig) {\n // Ensure baseUrl doesn't have trailing slash\n this.baseUrl = config.baseUrl.replace(/\\/$/, \"\");\n this.getAuthToken = config.getAuthToken;\n this.fetchFn = config.fetch ?? fetch;\n }\n\n /**\n * Make an authenticated request to the API\n */\n private async request<T>(\n path: string,\n options: {\n method?: \"GET\" | \"POST\" | \"PUT\" | \"DELETE\";\n body?: unknown;\n params?: Record<string, string | number | undefined>;\n } = {}\n ): Promise<T> {\n const { method = \"GET\", body, params } = options;\n\n // Build URL with query params\n let url = `${this.baseUrl}${path}`;\n if (params) {\n const searchParams = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n if (value !== undefined) {\n searchParams.set(key, String(value));\n }\n }\n const queryString = searchParams.toString();\n if (queryString) {\n url += `?${queryString}`;\n }\n }\n\n // Build headers\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n\n // Add auth token if available\n if (this.getAuthToken) {\n try {\n const token = await this.getAuthToken();\n if (token) {\n headers[\"Authorization\"] = `Bearer ${token}`;\n }\n } catch {\n // Auth token not available, proceed without it\n }\n }\n\n // Make the request\n let response: Response;\n try {\n response = await this.fetchFn(url, {\n method,\n headers,\n body: body ? JSON.stringify(body) : undefined,\n });\n } catch (error) {\n throw new NetworkError(\n error instanceof Error ? error.message : \"Network request failed\",\n error instanceof Error ? error : undefined\n );\n }\n\n // Parse response\n const responseBody = (await response.json()) as {\n data?: T;\n error?: { code?: string; message?: string; details?: unknown };\n };\n\n // Handle errors\n if (!response.ok) {\n throw parseApiError(response.status, responseBody);\n }\n\n // Return data\n return responseBody.data as T;\n }\n\n /**\n * Get the current user's credit balance\n *\n * @returns User's credit information\n * @throws AuthenticationError if not authenticated\n */\n async getBalance(): Promise<UserCredits> {\n return this.request<UserCredits>(\"/api/v1/credits/balance\");\n }\n\n /**\n * Check if the user has sufficient credits for an operation\n *\n * @param operationType - Type of operation to check\n * @param customCost - Optional custom cost to check against\n * @returns Credit availability status\n * @throws AuthenticationError if not authenticated\n */\n async checkAvailability(\n operationType: string,\n customCost?: number\n ): Promise<CreditCheckResult> {\n return this.request<CreditCheckResult>(\"/api/v1/credits/availability\", {\n params: {\n operationType,\n customCost: customCost?.toString(),\n },\n });\n }\n\n /**\n * Reserve credits for an operation\n *\n * This is the first step in the two-phase commit pattern.\n * Reserved credits are held for 5 minutes before expiring.\n *\n * @param operationType - Type of operation\n * @param options - Optional reservation options\n * @returns Reservation details including ID and expiry\n * @throws InsufficientCreditsError if not enough credits\n * @throws AuthenticationError if not authenticated\n */\n async reserve(\n operationType: string,\n options?: {\n customCost?: number;\n resourceId?: string;\n resourceType?: string;\n }\n ): Promise<ReservationResult> {\n return this.request<ReservationResult>(\"/api/v1/credits/reserve\", {\n method: \"POST\",\n body: {\n operationType,\n ...options,\n },\n });\n }\n\n /**\n * Commit a reservation (deduct credits)\n *\n * This is the second step in the two-phase commit pattern.\n * Call this after your operation succeeds.\n *\n * @param reservationId - ID of the reservation to commit\n * @throws ReservationNotFoundError if reservation doesn't exist\n * @throws ReservationExpiredError if reservation has expired\n * @throws AuthenticationError if not authenticated\n */\n async commit(reservationId: string): Promise<void> {\n await this.request<{ message: string }>(\"/api/v1/credits/commit\", {\n method: \"POST\",\n body: { reservationId },\n });\n }\n\n /**\n * Release a reservation (refund credits)\n *\n * Call this if your operation fails after reserving credits.\n *\n * @param reservationId - ID of the reservation to release\n * @throws ReservationNotFoundError if reservation doesn't exist\n * @throws AuthenticationError if not authenticated\n */\n async release(reservationId: string): Promise<void> {\n await this.request<{ message: string }>(\"/api/v1/credits/release\", {\n method: \"POST\",\n body: { reservationId },\n });\n }\n\n /**\n * Get usage history for the current user\n *\n * @param options - Optional pagination options\n * @returns Paginated list of usage entries\n * @throws AuthenticationError if not authenticated\n */\n async getHistory(options?: PaginationOptions): Promise<UsageHistoryResponse> {\n return this.request<UsageHistoryResponse>(\"/api/v1/credits/history\", {\n params: {\n page: options?.page,\n limit: options?.limit,\n },\n });\n }\n\n /**\n * Get the available credits (balance + bonus - reserved)\n *\n * Convenience method that calculates available credits from balance.\n *\n * @returns Number of available credits\n * @throws AuthenticationError if not authenticated\n */\n async getAvailableCredits(): Promise<number> {\n const balance = await this.getBalance();\n return balance.balance + balance.bonusCredits - balance.reserved;\n }\n\n /**\n * Execute an operation with automatic credit handling\n *\n * This is a convenience method that handles the full two-phase commit\n * pattern automatically.\n *\n * @param operationType - Type of operation\n * @param operation - The operation to execute\n * @param options - Optional reservation options\n * @returns Result of the operation\n * @throws InsufficientCreditsError if not enough credits\n * @throws AuthenticationError if not authenticated\n *\n * @example\n * ```typescript\n * const result = await client.withCredits(\n * \"story_generation\",\n * async () => {\n * return generateStory(data);\n * }\n * );\n * ```\n */\n async withCredits<T>(\n operationType: string,\n operation: () => Promise<T>,\n options?: {\n customCost?: number;\n resourceId?: string;\n resourceType?: string;\n }\n ): Promise<T> {\n const reservation = await this.reserve(operationType, options);\n\n try {\n const result = await operation();\n await this.commit(reservation.reservationId);\n return result;\n } catch (error) {\n // Always attempt to release on failure\n try {\n await this.release(reservation.reservationId);\n } catch {\n // Ignore release errors, original error is more important\n }\n throw error;\n }\n }\n}\n","/**\n * AdminCreditsClient - Admin REST API client for the credits system\n *\n * Provides administrative access to the credits system for managing\n * user credits, configurations, and subscriptions.\n *\n * @example\n * ```typescript\n * const adminClient = new AdminCreditsClient({\n * baseUrl: process.env.CREDITS_API_URL,\n * apiKey: process.env.CREDITS_ADMIN_API_KEY,\n * });\n *\n * // Add credits to a user\n * await adminClient.addCredits(\"user-123\", 100, \"Support credit grant\");\n *\n * // Update subscription tier\n * await adminClient.updateSubscription(\"user-123\", \"premium\", \"2027-01-01T00:00:00Z\");\n *\n * // Update operation costs\n * await adminClient.updateCosts({\n * story_generation: 8,\n * image_generation: 12,\n * });\n * ```\n */\n\nimport type { AdminCreditsClientConfig, UserCredits } from \"./types.js\";\nimport { parseApiError, NetworkError } from \"./errors.js\";\n\n/**\n * Credits configuration returned by the API\n */\nexport interface CreditsConfig {\n operationCosts: Record<string, number>;\n tierConfigs: Record<\n string,\n {\n monthlyLimit: number;\n features: string[];\n }\n >;\n}\n\n/**\n * Response from listing users\n */\nexport interface ListUsersResponse {\n users: Array<{\n userId: string;\n balance: number;\n bonusCredits?: number;\n tier: string;\n monthlyUsed?: number;\n }>;\n hasMore: boolean;\n total?: number;\n}\n\n/**\n * Options for listing users\n */\nexport interface ListUsersOptions {\n page?: number;\n limit?: number;\n tier?: string;\n search?: string;\n}\n\nexport class AdminCreditsClient {\n private readonly baseUrl: string;\n private readonly apiKey: string;\n private readonly fetchFn: typeof fetch;\n\n constructor(config: AdminCreditsClientConfig) {\n // Ensure baseUrl doesn't have trailing slash\n this.baseUrl = config.baseUrl.replace(/\\/$/, \"\");\n this.apiKey = config.apiKey;\n this.fetchFn = config.fetch ?? fetch;\n }\n\n /**\n * Make an authenticated admin request to the API\n */\n private async request<T>(\n path: string,\n options: {\n method?: \"GET\" | \"POST\" | \"PUT\" | \"DELETE\";\n body?: unknown;\n params?: Record<string, string | number | undefined>;\n } = {}\n ): Promise<T> {\n const { method = \"GET\", body, params } = options;\n\n // Build URL with query params\n let url = `${this.baseUrl}${path}`;\n if (params) {\n const searchParams = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n if (value !== undefined) {\n searchParams.set(key, String(value));\n }\n }\n const queryString = searchParams.toString();\n if (queryString) {\n url += `?${queryString}`;\n }\n }\n\n // Build headers with admin API key\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"x-api-key\": this.apiKey,\n };\n\n // Make the request\n let response: Response;\n try {\n response = await this.fetchFn(url, {\n method,\n headers,\n body: body ? JSON.stringify(body) : undefined,\n });\n } catch (error) {\n throw new NetworkError(\n error instanceof Error ? error.message : \"Network request failed\",\n error instanceof Error ? error : undefined\n );\n }\n\n // Parse response\n const responseBody = (await response.json()) as {\n data?: T;\n error?: { code?: string; message?: string; details?: unknown };\n };\n\n // Handle errors\n if (!response.ok) {\n throw parseApiError(response.status, responseBody);\n }\n\n // Return data\n return responseBody.data as T;\n }\n\n /**\n * Get a specific user's credits\n *\n * @param userId - ID of the user\n * @returns User's credit information\n * @throws AuthorizationError if API key is invalid\n */\n async getUserCredits(userId: string): Promise<UserCredits> {\n return this.request<UserCredits>(`/api/admin/credits/users/${userId}`);\n }\n\n /**\n * Add credits to a user's account\n *\n * Credits are added to bonusCredits, which don't expire on monthly reset.\n *\n * @param userId - ID of the user\n * @param amount - Amount of credits to add\n * @param description - Reason for adding credits\n * @throws AuthorizationError if API key is invalid\n */\n async addCredits(\n userId: string,\n amount: number,\n description: string\n ): Promise<void> {\n await this.request<{ message: string; newBalance: number }>(\n `/api/admin/credits/users/${userId}/add`,\n {\n method: \"POST\",\n body: { amount, description },\n }\n );\n }\n\n /**\n * Update a user's subscription tier\n *\n * @param userId - ID of the user\n * @param tier - New subscription tier\n * @param expiresAt - Optional expiration date (ISO 8601)\n * @throws AuthorizationError if API key is invalid\n */\n async updateSubscription(\n userId: string,\n tier: string,\n expiresAt?: string\n ): Promise<void> {\n const body: { tier: string; expiresAt?: string } = { tier };\n if (expiresAt) {\n body.expiresAt = expiresAt;\n }\n\n await this.request<{ message: string }>(\n `/api/admin/credits/users/${userId}/subscription`,\n {\n method: \"PUT\",\n body,\n }\n );\n }\n\n /**\n * Get the current credits configuration\n *\n * @returns Credits system configuration\n * @throws AuthorizationError if API key is invalid\n */\n async getConfig(): Promise<CreditsConfig> {\n return this.request<CreditsConfig>(\"/api/admin/credits/config\");\n }\n\n /**\n * Update operation costs\n *\n * @param costs - New operation costs (operation type -> cost)\n * @throws AuthorizationError if API key is invalid\n */\n async updateCosts(costs: Record<string, number>): Promise<void> {\n await this.request<{ message: string }>(\"/api/admin/credits/config/costs\", {\n method: \"PUT\",\n body: { operationCosts: costs },\n });\n }\n\n /**\n * List all users with their credits\n *\n * @param options - Pagination and filtering options\n * @returns Paginated list of users\n * @throws AuthorizationError if API key is invalid\n */\n async listUsers(options?: ListUsersOptions): Promise<ListUsersResponse> {\n return this.request<ListUsersResponse>(\"/api/admin/credits/users\", {\n params: {\n page: options?.page,\n limit: options?.limit,\n tier: options?.tier,\n search: options?.search,\n },\n });\n }\n\n /**\n * Deduct credits from a user's account\n *\n * @param userId - ID of the user\n * @param amount - Amount of credits to deduct\n * @param description - Reason for deducting credits\n * @throws AuthorizationError if API key is invalid\n */\n async deductCredits(\n userId: string,\n amount: number,\n description: string\n ): Promise<void> {\n await this.request<{ message: string; newBalance: number }>(\n `/api/admin/credits/users/${userId}/deduct`,\n {\n method: \"POST\",\n body: { amount, description },\n }\n );\n }\n\n /**\n * Get usage statistics for a user\n *\n * @param userId - ID of the user\n * @param options - Optional date range\n * @returns Usage statistics\n * @throws AuthorizationError if API key is invalid\n */\n async getUserStats(\n userId: string,\n options?: { startDate?: string; endDate?: string }\n ): Promise<{\n totalCreditsUsed: number;\n operationBreakdown: Record<string, number>;\n successRate: number;\n }> {\n return this.request<{\n totalCreditsUsed: number;\n operationBreakdown: Record<string, number>;\n successRate: number;\n }>(`/api/admin/credits/users/${userId}/stats`, {\n params: {\n startDate: options?.startDate,\n endDate: options?.endDate,\n },\n });\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/sdk/types.ts","../../src/sdk/errors.ts","../../src/core/amount.ts","../../src/sdk/client.ts","../../src/sdk/admin-client.ts"],"sourcesContent":["/**\n * SDK types for the credits system\n *\n * These types are used by the client SDKs for REST API consumption.\n */\n\n// Re-export core types for SDK consumers\nexport type {\n PortableUserCredits as UserCredits,\n PortableReservation as CreditReservation,\n CreditCheckResult,\n SubscriptionTier,\n} from \"../core/types.js\";\n\n/**\n * Configuration for CreditsClient\n */\nexport interface CreditsClientConfig {\n /** Base URL of the credits API (e.g., \"https://api.example.com\") */\n baseUrl: string;\n /** Function to get the auth token for session-based auth */\n getAuthToken?: () => Promise<string>;\n /** Optional custom fetch function for testing */\n fetch?: typeof fetch;\n}\n\n/**\n * Configuration for AdminCreditsClient\n */\nexport interface AdminCreditsClientConfig {\n /** Base URL of the credits API */\n baseUrl: string;\n /** Admin API key */\n apiKey: string;\n /** Optional custom fetch function for testing */\n fetch?: typeof fetch;\n}\n\n/**\n * Result of a credit reservation\n */\nexport interface ReservationResult {\n /** The reservation ID */\n reservationId: string;\n /** Amount of credits reserved */\n amount: number;\n /** When the reservation expires (ISO 8601) */\n expiresAt: string;\n}\n\n/**\n * Pagination options for API requests\n */\nexport interface PaginationOptions {\n /** Page number (1-indexed) */\n page?: number;\n /** Number of items per page */\n limit?: number;\n}\n\n/**\n * Usage history entry\n */\nexport interface UsageHistoryEntry {\n /** Unique ID of the usage log */\n id: string;\n /** Type of operation */\n operationType: string;\n /** AI provider used */\n provider: string;\n /** Credits consumed */\n creditsUsed: number;\n /** Whether the operation was successful */\n success: boolean;\n /** Error message if unsuccessful */\n errorMessage?: string;\n /** ID of the related resource */\n resourceId?: string;\n /** Type of the related resource */\n resourceType?: string;\n /** When the operation occurred (ISO 8601) */\n createdAt: string;\n}\n\n/**\n * Response from usage history endpoint\n */\nexport interface UsageHistoryResponse {\n /** List of usage entries */\n entries: UsageHistoryEntry[];\n /** Whether there are more results */\n hasMore: boolean;\n /** Total count (if available) */\n total?: number;\n}\n\n/**\n * Generic API response wrapper\n */\nexport interface ApiResponse<T> {\n success: boolean;\n data?: T;\n error?: {\n code: string;\n message: string;\n details?: unknown;\n };\n}\n\n/**\n * SDK error codes\n */\nexport const SDKErrorCode = {\n NETWORK_ERROR: \"NETWORK_ERROR\",\n AUTHENTICATION_ERROR: \"AUTHENTICATION_ERROR\",\n AUTHORIZATION_ERROR: \"AUTHORIZATION_ERROR\",\n INSUFFICIENT_CREDITS: \"INSUFFICIENT_CREDITS\",\n RESERVATION_NOT_FOUND: \"RESERVATION_NOT_FOUND\",\n RESERVATION_EXPIRED: \"RESERVATION_EXPIRED\",\n VALIDATION_ERROR: \"VALIDATION_ERROR\",\n SERVER_ERROR: \"SERVER_ERROR\",\n UNKNOWN_ERROR: \"UNKNOWN_ERROR\",\n} as const;\n\nexport type SDKErrorCodeType = (typeof SDKErrorCode)[keyof typeof SDKErrorCode];\n","/**\n * SDK error classes\n *\n * Provides typed error handling for SDK operations.\n */\n\nimport { SDKErrorCode, type SDKErrorCodeType } from \"./types.js\";\n\n/**\n * Base error class for SDK errors\n */\nexport class CreditsSDKError extends Error {\n public readonly code: SDKErrorCodeType;\n public readonly details?: Record<string, unknown>;\n public readonly statusCode?: number;\n\n constructor(\n message: string,\n code: SDKErrorCodeType,\n options?: {\n details?: Record<string, unknown>;\n statusCode?: number;\n cause?: Error;\n }\n ) {\n super(message, { cause: options?.cause });\n this.name = \"CreditsSDKError\";\n this.code = code;\n this.details = options?.details;\n this.statusCode = options?.statusCode;\n }\n\n /**\n * Check if this is a specific error type\n */\n is(code: SDKErrorCodeType): boolean {\n return this.code === code;\n }\n\n /**\n * Create a JSON representation for logging\n */\n toJSON(): Record<string, unknown> {\n return {\n name: this.name,\n message: this.message,\n code: this.code,\n details: this.details,\n statusCode: this.statusCode,\n };\n }\n}\n\n/**\n * Error thrown when a network request fails\n */\nexport class NetworkError extends CreditsSDKError {\n constructor(message: string, cause?: Error) {\n super(message, SDKErrorCode.NETWORK_ERROR, { cause });\n this.name = \"NetworkError\";\n }\n}\n\n/**\n * Error thrown when authentication fails\n */\nexport class AuthenticationError extends CreditsSDKError {\n constructor(message: string = \"Authentication required\") {\n super(message, SDKErrorCode.AUTHENTICATION_ERROR, { statusCode: 401 });\n this.name = \"AuthenticationError\";\n }\n}\n\n/**\n * Error thrown when authorization fails\n */\nexport class AuthorizationError extends CreditsSDKError {\n constructor(message: string = \"Access denied\") {\n super(message, SDKErrorCode.AUTHORIZATION_ERROR, { statusCode: 403 });\n this.name = \"AuthorizationError\";\n }\n}\n\n/**\n * Error thrown when credits are insufficient\n */\nexport class InsufficientCreditsError extends CreditsSDKError {\n public readonly available: number;\n public readonly required: number;\n\n constructor(available: number, required: number) {\n super(\n `Insufficient credits: available ${available}, required ${required}`,\n SDKErrorCode.INSUFFICIENT_CREDITS,\n {\n statusCode: 402,\n details: { available, required },\n }\n );\n this.name = \"InsufficientCreditsError\";\n this.available = available;\n this.required = required;\n }\n}\n\n/**\n * Error thrown when a reservation is not found\n */\nexport class ReservationNotFoundError extends CreditsSDKError {\n public readonly reservationId: string;\n\n constructor(reservationId: string) {\n super(\n `Reservation not found: ${reservationId}`,\n SDKErrorCode.RESERVATION_NOT_FOUND,\n {\n statusCode: 404,\n details: { reservationId },\n }\n );\n this.name = \"ReservationNotFoundError\";\n this.reservationId = reservationId;\n }\n}\n\n/**\n * Error thrown when a reservation has expired\n */\nexport class ReservationExpiredError extends CreditsSDKError {\n public readonly reservationId: string;\n\n constructor(reservationId: string) {\n super(\n `Reservation expired: ${reservationId}`,\n SDKErrorCode.RESERVATION_EXPIRED,\n {\n statusCode: 410,\n details: { reservationId },\n }\n );\n this.name = \"ReservationExpiredError\";\n this.reservationId = reservationId;\n }\n}\n\n/**\n * Error thrown for validation failures\n */\nexport class ValidationError extends CreditsSDKError {\n public readonly field?: string;\n\n constructor(message: string, field?: string) {\n super(message, SDKErrorCode.VALIDATION_ERROR, {\n statusCode: 400,\n details: field ? { field } : undefined,\n });\n this.name = \"ValidationError\";\n this.field = field;\n }\n}\n\n/**\n * Error thrown for server errors\n */\nexport class ServerError extends CreditsSDKError {\n constructor(message: string = \"Internal server error\") {\n super(message, SDKErrorCode.SERVER_ERROR, { statusCode: 500 });\n this.name = \"ServerError\";\n }\n}\n\n/**\n * Parse an API error response into the appropriate SDK error\n */\nexport function parseApiError(\n statusCode: number,\n body: { error?: { code?: string; message?: string; details?: unknown } }\n): CreditsSDKError {\n const errorCode = body.error?.code || \"\";\n const errorMessage = body.error?.message || \"An error occurred\";\n const details = body.error?.details as Record<string, unknown> | undefined;\n\n switch (statusCode) {\n case 401:\n return new AuthenticationError(errorMessage);\n case 403:\n return new AuthorizationError(errorMessage);\n case 402:\n // Parse insufficient credits details\n if (details?.available !== undefined && details?.required !== undefined) {\n return new InsufficientCreditsError(\n details.available as number,\n details.required as number\n );\n }\n return new CreditsSDKError(errorMessage, SDKErrorCode.INSUFFICIENT_CREDITS, {\n statusCode,\n details,\n });\n case 404:\n if (errorCode === \"RESERVATION_NOT_FOUND\" && details?.reservationId) {\n return new ReservationNotFoundError(details.reservationId as string);\n }\n return new CreditsSDKError(errorMessage, SDKErrorCode.RESERVATION_NOT_FOUND, {\n statusCode,\n details,\n });\n case 410:\n if (errorCode === \"RESERVATION_EXPIRED\" && details?.reservationId) {\n return new ReservationExpiredError(details.reservationId as string);\n }\n return new CreditsSDKError(errorMessage, SDKErrorCode.RESERVATION_EXPIRED, {\n statusCode,\n details,\n });\n case 400:\n case 422:\n return new ValidationError(errorMessage, details?.field as string | undefined);\n case 500:\n default:\n if (statusCode >= 500) {\n return new ServerError(errorMessage);\n }\n return new CreditsSDKError(errorMessage, SDKErrorCode.UNKNOWN_ERROR, {\n statusCode,\n details,\n });\n }\n}\n","/**\n * Validation and exact comparison for credit amounts.\n *\n * Every amount ends up in a `numeric(12, 2)` column, so the ledger can only\n * represent values on the cent grid, with at most ten integer digits. A value\n * outside that grid is not \"close enough\" — PostgreSQL silently rounds it on\n * write, and the row that comes back no longer equals what the caller asked\n * for. Rejecting it before any row is touched is the only way the ledger and\n * the caller can agree on what happened.\n *\n * These live in core rather than in an adapter so every repository — shipped\n * or third-party — validates identically.\n */\n\nimport { CreditError, CreditErrorCode } from \"./errors.js\";\n\n/** `numeric(12, 2)`: 12 significant digits, 2 of them after the point. */\nexport const CREDIT_AMOUNT_SCALE = 2;\n/** Largest representable amount: ten integer digits plus two decimals. */\nexport const CREDIT_AMOUNT_MAX = 9_999_999_999.99;\nconst MAX_CENTS = 999_999_999_999;\n\n/**\n * Convert an amount to whole cents, or `null` if it is not exactly on the grid.\n *\n * `Math.round(value * 100) === value * 100` is the tempting check and it is\n * wrong: `1.005 * 100` is `100.49999999999999` in binary float, so a genuinely\n * invalid value can pass and a valid one can fail. Rounding first and then\n * confirming the value round-trips (`value === cents / 100`) is exact for\n * everything in range, because a cent count below 2^53 divided by 100 is the\n * nearest double to that decimal, which is what the literal parsed to.\n */\nexport function toCents(value: number): number | null {\n if (typeof value !== \"number\" || !Number.isFinite(value)) return null;\n const cents = Math.round(value * 100);\n if (!Number.isSafeInteger(cents)) return null;\n return value === cents / 100 ? cents : null;\n}\n\n/**\n * Add amounts on the cent grid, exactly.\n *\n * Deriving a total with `+` is not safe in a ledger that validates against the\n * grid: `0.10 + 0.20` is `0.30000000000000004` and `0.30 - 0.10` is\n * `0.19999999999999998`, neither of which is a value `numeric(12, 2)` can hold.\n * Two perfectly legal columns would therefore produce a total the validator\n * rejects with `INVALID_AMOUNT`, blaming the caller for a rounding artefact.\n * Summing whole cents is exact for every value in range, and `cents / 100` is\n * the nearest double to that decimal — the same one the literal would parse to.\n *\n * Subtract by negating: `sumAmounts(total, -amount)`. Negation is exact.\n *\n * This is arithmetic, not validation. If any input is itself off the grid the\n * float sum is returned unchanged — deliberately, so a corrupt value is never\n * rounded into legitimacy here — but that is not a guarantee the result will be\n * rejected downstream: two off-grid inputs can cancel, and `sumAmounts(0.005,\n * -0.005)` is a perfectly representable `0`. Inputs read from storage must be\n * validated as inputs, with `assertValidStoredAmount` or\n * `assertValidStoredAmountRaw`, before they are summed. Checking only the\n * derived result is not enough and never was.\n */\nexport function sumAmounts(...values: number[]): number {\n let cents = 0;\n for (const value of values) {\n const valueCents = toCents(value);\n if (valueCents === null) return floatSum(values);\n cents += valueCents;\n }\n return Number.isSafeInteger(cents) ? cents / 100 : floatSum(values);\n}\n\nfunction floatSum(values: number[]): number {\n let total = 0;\n for (const value of values) total += value;\n return total;\n}\n\n/** Amounts that may be *moved*: strictly positive and on the cent grid. */\nexport function isValidCreditAmount(value: unknown): value is number {\n if (typeof value !== \"number\") return false;\n const cents = toCents(value);\n return cents !== null && cents > 0 && cents <= MAX_CENTS;\n}\n\n/**\n * Reject an unusable amount before any row is written.\n *\n * @param context - merged into the error details, e.g. `{ userId }`.\n */\nexport function assertValidCreditAmount(\n value: unknown,\n context?: Record<string, unknown>\n): asserts value is number {\n if (isValidCreditAmount(value)) return;\n throw new CreditError(\n creditAmountReason(value),\n CreditErrorCode.INVALID_AMOUNT,\n { amount: value, max: CREDIT_AMOUNT_MAX, scale: CREDIT_AMOUNT_SCALE, ...context }\n );\n}\n\n/** Say precisely which rule the value broke, so the caller can fix it. */\nfunction creditAmountReason(value: unknown): string {\n if (typeof value !== \"number\") {\n return `Credit amount must be a number (got ${typeof value})`;\n }\n if (!Number.isFinite(value)) {\n return `Credit amount must be finite (got ${String(value)})`;\n }\n if (value <= 0) {\n return `Credit amount must be greater than zero (got ${value})`;\n }\n if (value > CREDIT_AMOUNT_MAX) {\n return `Credit amount ${value} exceeds the maximum of ${CREDIT_AMOUNT_MAX}`;\n }\n return `Credit amount ${value} is not exact to ${CREDIT_AMOUNT_SCALE} decimal places`;\n}\n\n/**\n * Amounts the ledger can *record*: on the cent grid and in range, but free to\n * be zero or negative.\n *\n * Not every numeric column holds something spendable. A release journal entry\n * records `amount: 0` because no credits changed hands, and `balanceAfter` goes\n * negative for an account that was corrected or allowed to overdraw. Forcing\n * those through {@link isValidCreditAmount} would reject legitimate ledger\n * rows; skipping validation entirely would let `Infinity` or a third decimal\n * reach the column and be silently rounded or rejected by the driver.\n */\nexport function isRepresentableAmount(value: unknown): value is number {\n if (typeof value !== \"number\") return false;\n const cents = toCents(value);\n return cents !== null && Math.abs(cents) <= MAX_CENTS;\n}\n\n/**\n * Reject a ledger value the `numeric(12, 2)` column cannot hold exactly.\n *\n * @param field - the column being written, so the error names the culprit.\n */\nexport function assertRepresentableAmount(\n value: unknown,\n field: string,\n context?: Record<string, unknown>\n): asserts value is number {\n if (isRepresentableAmount(value)) return;\n throw new CreditError(\n `${field} must be a finite number exact to ${CREDIT_AMOUNT_SCALE} decimal places ` +\n `within ±${CREDIT_AMOUNT_MAX} (got ${String(value)})`,\n CreditErrorCode.INVALID_AMOUNT,\n { field, value, max: CREDIT_AMOUNT_MAX, scale: CREDIT_AMOUNT_SCALE, ...context }\n );\n}\n\n/**\n * Check a whole set of optional ledger fields at once.\n *\n * The balance writers each take a handful of independently optional numbers,\n * and validating them one `if` at a time is where one quietly gets missed.\n * Undefined entries are skipped — they mean \"leave this column alone\".\n */\nexport function assertRepresentableFields(\n fields: Record<string, unknown>,\n context?: Record<string, unknown>\n): void {\n for (const [field, value] of Object.entries(fields)) {\n if (value === undefined) continue;\n assertRepresentableAmount(value, field, context);\n }\n}\n\n/**\n * The lowest `balance` that keeps every outstanding hold committable.\n *\n * Commit's funding guard is `balance + bonusCredits >= amount`, and every live\n * hold is counted in `reserved`, so any write that lowers `balance` must keep\n * `balance + bonusCredits >= reserved` — otherwise a hold that was fully backed\n * when it was placed throws `INSUFFICIENT_CREDITS` at commit time and is\n * stranded. Monthly resets, tier downgrades and explicit tier writes all lower\n * `balance` to a configured target; this is the floor they may not cut through.\n *\n * The floor is `reserved - bonusCredits`, never below zero: bonus credits also\n * fund commits, so they count toward the backing.\n */\nexport function backedBalanceFloor(reserved: number, bonusCredits: number): number {\n const floor = Math.max(0, sumAmounts(reserved, -bonusCredits));\n // A floor beyond the representable range means the row itself is corrupt —\n // e.g. a large negative `bonusCredits` pushing `reserved - bonusCredits`\n // past the `numeric(12,2)` cap. Refuse loudly: writing it would succeed in\n // memory and be rejected by the SQL column, splitting the adapters.\n assertRepresentableAmount(floor, \"backedBalanceFloor\", { reserved, bonusCredits });\n return floor;\n}\n\n/**\n * Validate a limit derived from tier configuration.\n *\n * `Infinity` is this codebase's sentinel for an unlimited tier, so it is\n * accepted here; {@link storedMonthlyLimit} decides what actually reaches the\n * column. Every other value ends up in `numeric(12, 2)` and has to fit, so a\n * tier configured with `monthlyCredits: 1.005` is caught here rather than\n * silently rounded to `1.01` by PostgreSQL.\n */\nexport function assertRepresentableTierAmount(\n value: unknown,\n field: string,\n context?: Record<string, unknown>\n): void {\n if (value === Number.POSITIVE_INFINITY) return;\n assertRepresentableAmount(value, field, context);\n}\n\n/**\n * Validate a stored amount in the representation storage actually returned.\n *\n * Checking the mapped float is not enough, because the mapping is lossy in both\n * directions. A `numeric` column with no scale constraint can hold\n * `9999999999.9900001`; `Number()` rounds that to `9999999999.99`, which is a\n * perfectly valid amount, so a validator looking at the mapped value sees\n * nothing wrong and the transition proceeds — while PostgreSQL goes on doing\n * exact arithmetic with the digits JavaScript threw away. `NaN` and values past\n * double range are worse: the mapper turns them into `0`, which misreports what\n * the row holds.\n *\n * So this runs on the driver's own output — a string, for `numeric` — and uses\n * exact integer arithmetic. It returns the amount as a float only after\n * establishing that the float is a faithful representation of it.\n *\n * @returns the stored amount, exactly representable as a JS number.\n */\n/**\n * How an unlimited tier's monthly limit is stored.\n *\n * `getConfigMonthlyLimit` returns `Infinity` for an unlimited tier, and no\n * numeric column can hold that. Both adapters therefore store the ceiling of\n * the representable range instead: any real usage compares below it, so\n * \"unlimited\" keeps behaving like unlimited, and a reader that knows nothing\n * about the sentinel sees a very large allowance rather than a wrong one.\n *\n * This exists so the two adapters cannot drift. The SQL adapter previously\n * stored `0` here, which reads as *no* allowance — the exact opposite — while\n * the in-memory adapter kept `Infinity`.\n */\nexport function storedMonthlyLimit(limit: number): number {\n return Number.isFinite(limit) ? limit : CREDIT_AMOUNT_MAX;\n}\n\nexport function assertValidStoredAmountRaw(\n raw: unknown,\n context: Record<string, unknown>\n): number {\n const cents = numericToCents(raw);\n if (cents !== null && cents > 0n && cents <= BigInt(MAX_CENTS)) {\n return Number(cents) / 100;\n }\n throw new CreditError(\n `Stored credit amount ${String(raw)} is not a valid amount, so this ` +\n \"transition was refused before any state changed. The row must be \" +\n \"repaired before it can be committed, released or expired.\",\n CreditErrorCode.INVALID_AMOUNT,\n {\n amount: typeof raw === \"string\" ? raw : String(raw),\n reason: \"corrupt_stored_amount\",\n max: CREDIT_AMOUNT_MAX,\n scale: CREDIT_AMOUNT_SCALE,\n ...context,\n }\n );\n}\n\n/**\n * Reject a *persisted* amount that the ledger cannot honour.\n *\n * A reservation row is written once and read back on every transition, and\n * nothing stops a direct SQL write, a botched migration, or an older version of\n * this library from leaving an amount the invariants cannot reason about. A\n * negative one is the dangerous case: `reserved >= amount` is trivially true\n * for it, and `balance - (-10)` *adds* credits, so a transition that trusts the\n * stored value mints money. Every transition therefore re-validates what it\n * locked before it changes anything.\n *\n * Deliberately fails release and expire as well as commit. Returning a hold of\n * an unknown size is not obviously safer than spending one — it hands back\n * coverage that may never have been taken — so a corrupt row stops moving\n * entirely and waits for an operator, rather than being partially honoured.\n *\n * This is the float-typed form, used by the in-memory adapter where the stored\n * value *is* a JS number and no conversion has happened. A repository whose\n * storage returns some other representation must use\n * {@link assertValidStoredAmountRaw} instead, or the conversion will hide the\n * corruption from the check.\n */\nexport function assertValidStoredAmount(\n value: unknown,\n context: Record<string, unknown>\n): asserts value is number {\n if (isValidCreditAmount(value)) return;\n throw new CreditError(\n `Stored credit amount ${String(value)} is not a valid amount, so this ` +\n \"transition was refused before any state changed. The row must be \" +\n \"repaired before it can be committed, released or expired.\",\n CreditErrorCode.INVALID_AMOUNT,\n { amount: value, reason: \"corrupt_stored_amount\", max: CREDIT_AMOUNT_MAX, ...context }\n );\n}\n\n/**\n * Parse a value read back from a `numeric` column into exact cents.\n *\n * Drivers return `numeric` as a string precisely so no precision is lost in\n * transit; comparing the parsed floats would throw that away again. Returns\n * `null` for anything unparseable so callers can treat it as \"not equal\"\n * rather than crashing on malformed ledger data.\n */\nexport function numericToCents(value: unknown): bigint | null {\n if (typeof value === \"number\") {\n const cents = toCents(value);\n return cents === null ? null : BigInt(cents);\n }\n if (typeof value !== \"string\") return null;\n\n const match = /^([+-]?)(\\d+)(?:\\.(\\d*))?$/.exec(value.trim());\n if (!match) return null;\n\n const [, sign, whole, fraction = \"\"] = match;\n // More than two decimals is only acceptable when the extra digits are zeros;\n // anything else never came from a numeric(12,2) column.\n if (/[1-9]/.test(fraction.slice(CREDIT_AMOUNT_SCALE))) return null;\n\n const cents =\n BigInt(whole) * 100n + BigInt(fraction.slice(0, CREDIT_AMOUNT_SCALE).padEnd(CREDIT_AMOUNT_SCALE, \"0\"));\n return sign === \"-\" ? -cents : cents;\n}\n\n/**\n * Exact equality for two amounts that may be numbers or `numeric` strings.\n *\n * Unparseable input is *not* equal to anything, including itself — a\n * comparison against corrupt data must never come back \"matches\".\n */\nexport function sameAmount(a: unknown, b: unknown): boolean {\n const left = numericToCents(a);\n const right = numericToCents(b);\n return left !== null && right !== null && left === right;\n}\n","/**\n * CreditsClient - REST API client for the credits system\n *\n * Provides a type-safe interface for consuming credits API endpoints\n * from external services or other applications.\n *\n * @example\n * ```typescript\n * const client = new CreditsClient({\n * baseUrl: process.env.CREDITS_API_URL,\n * getAuthToken: async () => session.accessToken,\n * });\n *\n * // Check if user has sufficient credits\n * const check = await client.checkAvailability(\"story_generation\");\n * if (!check.hasCredits) {\n * console.log(\"Insufficient credits\");\n * return;\n * }\n *\n * // Two-phase commit pattern\n * const reservation = await client.reserve(\"story_generation\");\n * try {\n * const result = await doExpensiveWork();\n * await client.commit(reservation.reservationId);\n * return result;\n * } catch (error) {\n * await client.release(reservation.reservationId);\n * throw error;\n * }\n * ```\n */\n\nimport type {\n CreditsClientConfig,\n UserCredits,\n CreditCheckResult,\n ReservationResult,\n UsageHistoryResponse,\n PaginationOptions,\n} from \"./types.js\";\nimport { parseApiError, NetworkError } from \"./errors.js\";\nimport { sumAmounts } from \"../core/amount.js\";\n\nexport class CreditsClient {\n private readonly baseUrl: string;\n private readonly getAuthToken?: () => Promise<string>;\n private readonly fetchFn: typeof fetch;\n\n constructor(config: CreditsClientConfig) {\n // Ensure baseUrl doesn't have trailing slash\n this.baseUrl = config.baseUrl.replace(/\\/$/, \"\");\n this.getAuthToken = config.getAuthToken;\n this.fetchFn = config.fetch ?? fetch;\n }\n\n /**\n * Make an authenticated request to the API\n */\n private async request<T>(\n path: string,\n options: {\n method?: \"GET\" | \"POST\" | \"PUT\" | \"DELETE\";\n body?: unknown;\n params?: Record<string, string | number | undefined>;\n } = {}\n ): Promise<T> {\n const { method = \"GET\", body, params } = options;\n\n // Build URL with query params\n let url = `${this.baseUrl}${path}`;\n if (params) {\n const searchParams = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n if (value !== undefined) {\n searchParams.set(key, String(value));\n }\n }\n const queryString = searchParams.toString();\n if (queryString) {\n url += `?${queryString}`;\n }\n }\n\n // Build headers\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n\n // Add auth token if available\n if (this.getAuthToken) {\n try {\n const token = await this.getAuthToken();\n if (token) {\n headers[\"Authorization\"] = `Bearer ${token}`;\n }\n } catch {\n // Auth token not available, proceed without it\n }\n }\n\n // Make the request\n let response: Response;\n try {\n response = await this.fetchFn(url, {\n method,\n headers,\n body: body ? JSON.stringify(body) : undefined,\n });\n } catch (error) {\n throw new NetworkError(\n error instanceof Error ? error.message : \"Network request failed\",\n error instanceof Error ? error : undefined\n );\n }\n\n // Parse response\n const responseBody = (await response.json()) as {\n data?: T;\n error?: { code?: string; message?: string; details?: unknown };\n };\n\n // Handle errors\n if (!response.ok) {\n throw parseApiError(response.status, responseBody);\n }\n\n // Return data\n return responseBody.data as T;\n }\n\n /**\n * Get the current user's credit balance\n *\n * @returns User's credit information\n * @throws AuthenticationError if not authenticated\n */\n async getBalance(): Promise<UserCredits> {\n return this.request<UserCredits>(\"/api/v1/credits/balance\");\n }\n\n /**\n * Check if the user has sufficient credits for an operation\n *\n * @param operationType - Type of operation to check\n * @param customCost - Optional custom cost to check against\n * @returns Credit availability status\n * @throws AuthenticationError if not authenticated\n */\n async checkAvailability(\n operationType: string,\n customCost?: number\n ): Promise<CreditCheckResult> {\n return this.request<CreditCheckResult>(\"/api/v1/credits/availability\", {\n params: {\n operationType,\n customCost: customCost?.toString(),\n },\n });\n }\n\n /**\n * Reserve credits for an operation\n *\n * This is the first step in the two-phase commit pattern.\n * Reserved credits are held for 5 minutes before expiring.\n *\n * @param operationType - Type of operation\n * @param options - Optional reservation options\n * @returns Reservation details including ID and expiry\n * @throws InsufficientCreditsError if not enough credits\n * @throws AuthenticationError if not authenticated\n */\n async reserve(\n operationType: string,\n options?: {\n customCost?: number;\n resourceId?: string;\n resourceType?: string;\n }\n ): Promise<ReservationResult> {\n return this.request<ReservationResult>(\"/api/v1/credits/reserve\", {\n method: \"POST\",\n body: {\n operationType,\n ...options,\n },\n });\n }\n\n /**\n * Commit a reservation (deduct credits)\n *\n * This is the second step in the two-phase commit pattern.\n * Call this after your operation succeeds.\n *\n * @param reservationId - ID of the reservation to commit\n * @throws ReservationNotFoundError if reservation doesn't exist\n * @throws ReservationExpiredError if reservation has expired\n * @throws AuthenticationError if not authenticated\n */\n async commit(reservationId: string): Promise<void> {\n await this.request<{ message: string }>(\"/api/v1/credits/commit\", {\n method: \"POST\",\n body: { reservationId },\n });\n }\n\n /**\n * Release a reservation (refund credits)\n *\n * Call this if your operation fails after reserving credits.\n *\n * @param reservationId - ID of the reservation to release\n * @throws ReservationNotFoundError if reservation doesn't exist\n * @throws AuthenticationError if not authenticated\n */\n async release(reservationId: string): Promise<void> {\n await this.request<{ message: string }>(\"/api/v1/credits/release\", {\n method: \"POST\",\n body: { reservationId },\n });\n }\n\n /**\n * Get usage history for the current user\n *\n * @param options - Optional pagination options\n * @returns Paginated list of usage entries\n * @throws AuthenticationError if not authenticated\n */\n async getHistory(options?: PaginationOptions): Promise<UsageHistoryResponse> {\n return this.request<UsageHistoryResponse>(\"/api/v1/credits/history\", {\n params: {\n page: options?.page,\n limit: options?.limit,\n },\n });\n }\n\n /**\n * Get the available credits (balance + bonus - reserved)\n *\n * Convenience method that calculates available credits from balance.\n *\n * @returns Number of available credits\n * @throws AuthenticationError if not authenticated\n */\n async getAvailableCredits(): Promise<number> {\n const balance = await this.getBalance();\n return sumAmounts(balance.balance, balance.bonusCredits, -balance.reserved);\n }\n\n /**\n * Execute an operation with automatic credit handling\n *\n * This is a convenience method that handles the full two-phase commit\n * pattern automatically.\n *\n * @param operationType - Type of operation\n * @param operation - The operation to execute\n * @param options - Optional reservation options\n * @returns Result of the operation\n * @throws InsufficientCreditsError if not enough credits\n * @throws AuthenticationError if not authenticated\n *\n * @example\n * ```typescript\n * const result = await client.withCredits(\n * \"story_generation\",\n * async () => {\n * return generateStory(data);\n * }\n * );\n * ```\n */\n async withCredits<T>(\n operationType: string,\n operation: () => Promise<T>,\n options?: {\n customCost?: number;\n resourceId?: string;\n resourceType?: string;\n }\n ): Promise<T> {\n const reservation = await this.reserve(operationType, options);\n\n try {\n const result = await operation();\n await this.commit(reservation.reservationId);\n return result;\n } catch (error) {\n // Always attempt to release on failure\n try {\n await this.release(reservation.reservationId);\n } catch {\n // Ignore release errors, original error is more important\n }\n throw error;\n }\n }\n}\n","/**\n * AdminCreditsClient - Admin REST API client for the credits system\n *\n * Provides administrative access to the credits system for managing\n * user credits, configurations, and subscriptions.\n *\n * @example\n * ```typescript\n * const adminClient = new AdminCreditsClient({\n * baseUrl: process.env.CREDITS_API_URL,\n * apiKey: process.env.CREDITS_ADMIN_API_KEY,\n * });\n *\n * // Add credits to a user\n * await adminClient.addCredits(\"user-123\", 100, \"Support credit grant\");\n *\n * // Update subscription tier\n * await adminClient.updateSubscription(\"user-123\", \"premium\", \"2027-01-01T00:00:00Z\");\n *\n * // Update operation costs\n * await adminClient.updateCosts({\n * story_generation: 8,\n * image_generation: 12,\n * });\n * ```\n */\n\nimport type { AdminCreditsClientConfig, UserCredits } from \"./types.js\";\nimport { parseApiError, NetworkError } from \"./errors.js\";\n\n/**\n * Credits configuration returned by the API\n */\nexport interface CreditsConfig {\n operationCosts: Record<string, number>;\n tierConfigs: Record<\n string,\n {\n monthlyLimit: number;\n features: string[];\n }\n >;\n}\n\n/**\n * Response from listing users\n */\nexport interface ListUsersResponse {\n users: Array<{\n userId: string;\n balance: number;\n bonusCredits?: number;\n tier: string;\n monthlyUsed?: number;\n }>;\n hasMore: boolean;\n total?: number;\n}\n\n/**\n * Options for listing users\n */\nexport interface ListUsersOptions {\n page?: number;\n limit?: number;\n tier?: string;\n search?: string;\n}\n\nexport class AdminCreditsClient {\n private readonly baseUrl: string;\n private readonly apiKey: string;\n private readonly fetchFn: typeof fetch;\n\n constructor(config: AdminCreditsClientConfig) {\n // Ensure baseUrl doesn't have trailing slash\n this.baseUrl = config.baseUrl.replace(/\\/$/, \"\");\n this.apiKey = config.apiKey;\n this.fetchFn = config.fetch ?? fetch;\n }\n\n /**\n * Make an authenticated admin request to the API\n */\n private async request<T>(\n path: string,\n options: {\n method?: \"GET\" | \"POST\" | \"PUT\" | \"DELETE\";\n body?: unknown;\n params?: Record<string, string | number | undefined>;\n } = {}\n ): Promise<T> {\n const { method = \"GET\", body, params } = options;\n\n // Build URL with query params\n let url = `${this.baseUrl}${path}`;\n if (params) {\n const searchParams = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n if (value !== undefined) {\n searchParams.set(key, String(value));\n }\n }\n const queryString = searchParams.toString();\n if (queryString) {\n url += `?${queryString}`;\n }\n }\n\n // Build headers with admin API key\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"x-api-key\": this.apiKey,\n };\n\n // Make the request\n let response: Response;\n try {\n response = await this.fetchFn(url, {\n method,\n headers,\n body: body ? JSON.stringify(body) : undefined,\n });\n } catch (error) {\n throw new NetworkError(\n error instanceof Error ? error.message : \"Network request failed\",\n error instanceof Error ? error : undefined\n );\n }\n\n // Parse response\n const responseBody = (await response.json()) as {\n data?: T;\n error?: { code?: string; message?: string; details?: unknown };\n };\n\n // Handle errors\n if (!response.ok) {\n throw parseApiError(response.status, responseBody);\n }\n\n // Return data\n return responseBody.data as T;\n }\n\n /**\n * Get a specific user's credits\n *\n * @param userId - ID of the user\n * @returns User's credit information\n * @throws AuthorizationError if API key is invalid\n */\n async getUserCredits(userId: string): Promise<UserCredits> {\n return this.request<UserCredits>(`/api/admin/credits/users/${userId}`);\n }\n\n /**\n * Add credits to a user's account\n *\n * Credits are added to bonusCredits, which don't expire on monthly reset.\n *\n * @param userId - ID of the user\n * @param amount - Amount of credits to add\n * @param description - Reason for adding credits\n * @throws AuthorizationError if API key is invalid\n */\n async addCredits(\n userId: string,\n amount: number,\n description: string\n ): Promise<void> {\n await this.request<{ message: string; newBalance: number }>(\n `/api/admin/credits/users/${userId}/add`,\n {\n method: \"POST\",\n body: { amount, description },\n }\n );\n }\n\n /**\n * Update a user's subscription tier\n *\n * @param userId - ID of the user\n * @param tier - New subscription tier\n * @param expiresAt - Optional expiration date (ISO 8601)\n * @throws AuthorizationError if API key is invalid\n */\n async updateSubscription(\n userId: string,\n tier: string,\n expiresAt?: string\n ): Promise<void> {\n const body: { tier: string; expiresAt?: string } = { tier };\n if (expiresAt) {\n body.expiresAt = expiresAt;\n }\n\n await this.request<{ message: string }>(\n `/api/admin/credits/users/${userId}/subscription`,\n {\n method: \"PUT\",\n body,\n }\n );\n }\n\n /**\n * Get the current credits configuration\n *\n * @returns Credits system configuration\n * @throws AuthorizationError if API key is invalid\n */\n async getConfig(): Promise<CreditsConfig> {\n return this.request<CreditsConfig>(\"/api/admin/credits/config\");\n }\n\n /**\n * Update operation costs\n *\n * @param costs - New operation costs (operation type -> cost)\n * @throws AuthorizationError if API key is invalid\n */\n async updateCosts(costs: Record<string, number>): Promise<void> {\n await this.request<{ message: string }>(\"/api/admin/credits/config/costs\", {\n method: \"PUT\",\n body: { operationCosts: costs },\n });\n }\n\n /**\n * List all users with their credits\n *\n * @param options - Pagination and filtering options\n * @returns Paginated list of users\n * @throws AuthorizationError if API key is invalid\n */\n async listUsers(options?: ListUsersOptions): Promise<ListUsersResponse> {\n return this.request<ListUsersResponse>(\"/api/admin/credits/users\", {\n params: {\n page: options?.page,\n limit: options?.limit,\n tier: options?.tier,\n search: options?.search,\n },\n });\n }\n\n /**\n * Deduct credits from a user's account\n *\n * @param userId - ID of the user\n * @param amount - Amount of credits to deduct\n * @param description - Reason for deducting credits\n * @throws AuthorizationError if API key is invalid\n */\n async deductCredits(\n userId: string,\n amount: number,\n description: string\n ): Promise<void> {\n await this.request<{ message: string; newBalance: number }>(\n `/api/admin/credits/users/${userId}/deduct`,\n {\n method: \"POST\",\n body: { amount, description },\n }\n );\n }\n\n /**\n * Get usage statistics for a user\n *\n * @param userId - ID of the user\n * @param options - Optional date range\n * @returns Usage statistics\n * @throws AuthorizationError if API key is invalid\n */\n async getUserStats(\n userId: string,\n options?: { startDate?: string; endDate?: string }\n ): Promise<{\n totalCreditsUsed: number;\n operationBreakdown: Record<string, number>;\n successRate: number;\n }> {\n return this.request<{\n totalCreditsUsed: number;\n operationBreakdown: Record<string, number>;\n successRate: number;\n }>(`/api/admin/credits/users/${userId}/stats`, {\n params: {\n startDate: options?.startDate,\n endDate: options?.endDate,\n },\n });\n }\n}\n"],"mappings":"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","names":[]}