@nehorai/credits 1.2.0 → 1.4.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 (139) hide show
  1. package/LICENSE +21 -0
  2. package/dist/adapters/index.cjs +463 -0
  3. package/dist/adapters/index.cjs.map +1 -0
  4. package/dist/adapters/index.d.cts +66 -0
  5. package/dist/adapters/index.d.ts +66 -3
  6. package/dist/adapters/index.js +435 -1
  7. package/dist/adapters/index.js.map +1 -1
  8. package/dist/auth/index.cjs +96 -0
  9. package/dist/auth/index.cjs.map +1 -0
  10. package/dist/auth/{api-key.d.ts → index.d.cts} +9 -6
  11. package/dist/auth/index.d.ts +37 -5
  12. package/dist/auth/index.js +65 -7
  13. package/dist/auth/index.js.map +1 -1
  14. package/dist/core/index.cjs +522 -0
  15. package/dist/core/index.cjs.map +1 -0
  16. package/dist/core/index.d.cts +119 -0
  17. package/dist/core/index.d.ts +115 -25
  18. package/dist/core/index.js +475 -27
  19. package/dist/core/index.js.map +1 -1
  20. package/dist/{core/deferred.d.ts → deferred-C7rrRfkm.d.cts} +7 -6
  21. package/dist/{core/deferred.js → deferred-C7rrRfkm.d.ts} +23 -29
  22. package/dist/index.cjs +2514 -0
  23. package/dist/index.cjs.map +1 -0
  24. package/dist/index.d.cts +437 -0
  25. package/dist/index.d.ts +429 -50
  26. package/dist/index.js +2407 -63
  27. package/dist/index.js.map +1 -1
  28. package/dist/repository/index.cjs +819 -0
  29. package/dist/repository/index.cjs.map +1 -0
  30. package/dist/repository/{memory/index.d.ts → index.d.cts} +35 -5
  31. package/dist/repository/index.d.ts +97 -7
  32. package/dist/repository/index.js +786 -10
  33. package/dist/repository/index.js.map +1 -1
  34. package/dist/sdk/index.cjs +608 -0
  35. package/dist/sdk/index.cjs.map +1 -0
  36. package/dist/sdk/index.d.cts +495 -0
  37. package/dist/sdk/index.d.ts +491 -9
  38. package/dist/sdk/index.js +568 -12
  39. package/dist/sdk/index.js.map +1 -1
  40. package/dist/service/index.cjs +675 -0
  41. package/dist/service/index.cjs.map +1 -0
  42. package/dist/service/{credits-service.d.ts → index.d.cts} +25 -8
  43. package/dist/service/index.d.ts +156 -3
  44. package/dist/service/index.js +646 -1
  45. package/dist/service/index.js.map +1 -1
  46. package/dist/{core/types.d.ts → types-BBxIWHK2.d.cts} +25 -24
  47. package/dist/types-BBxIWHK2.d.ts +277 -0
  48. package/dist/types-D1vdeBcA.d.cts +305 -0
  49. package/dist/{auth/types.d.ts → types-OZU3I5OM.d.cts} +5 -4
  50. package/dist/types-OZU3I5OM.d.ts +32 -0
  51. package/dist/{repository/types.d.ts → types-aJsyv8df.d.ts} +35 -13
  52. package/package.json +21 -19
  53. package/dist/adapters/generic.d.ts +0 -18
  54. package/dist/adapters/generic.d.ts.map +0 -1
  55. package/dist/adapters/generic.js +0 -151
  56. package/dist/adapters/generic.js.map +0 -1
  57. package/dist/adapters/index.d.ts.map +0 -1
  58. package/dist/adapters/types.d.ts +0 -45
  59. package/dist/adapters/types.d.ts.map +0 -1
  60. package/dist/adapters/types.js +0 -8
  61. package/dist/adapters/types.js.map +0 -1
  62. package/dist/auth/api-key.d.ts.map +0 -1
  63. package/dist/auth/api-key.js +0 -94
  64. package/dist/auth/api-key.js.map +0 -1
  65. package/dist/auth/index.d.ts.map +0 -1
  66. package/dist/auth/types.d.ts.map +0 -1
  67. package/dist/auth/types.js +0 -2
  68. package/dist/auth/types.js.map +0 -1
  69. package/dist/config/costs.d.ts +0 -56
  70. package/dist/config/costs.d.ts.map +0 -1
  71. package/dist/config/costs.js +0 -71
  72. package/dist/config/costs.js.map +0 -1
  73. package/dist/config/index.d.ts +0 -184
  74. package/dist/config/index.d.ts.map +0 -1
  75. package/dist/config/index.js +0 -342
  76. package/dist/config/index.js.map +0 -1
  77. package/dist/config/provider.d.ts +0 -12
  78. package/dist/config/provider.d.ts.map +0 -1
  79. package/dist/config/provider.js +0 -14
  80. package/dist/config/provider.js.map +0 -1
  81. package/dist/core/deferred.d.ts.map +0 -1
  82. package/dist/core/deferred.js.map +0 -1
  83. package/dist/core/errors.d.ts +0 -78
  84. package/dist/core/errors.d.ts.map +0 -1
  85. package/dist/core/errors.js +0 -110
  86. package/dist/core/errors.js.map +0 -1
  87. package/dist/core/index.d.ts.map +0 -1
  88. package/dist/core/operations.d.ts +0 -36
  89. package/dist/core/operations.d.ts.map +0 -1
  90. package/dist/core/operations.js +0 -88
  91. package/dist/core/operations.js.map +0 -1
  92. package/dist/core/types.d.ts.map +0 -1
  93. package/dist/core/types.js +0 -82
  94. package/dist/core/types.js.map +0 -1
  95. package/dist/index.d.ts.map +0 -1
  96. package/dist/notifications/index.d.ts +0 -76
  97. package/dist/notifications/index.d.ts.map +0 -1
  98. package/dist/notifications/index.js +0 -174
  99. package/dist/notifications/index.js.map +0 -1
  100. package/dist/repository/index.d.ts.map +0 -1
  101. package/dist/repository/memory/index.d.ts.map +0 -1
  102. package/dist/repository/memory/index.js +0 -518
  103. package/dist/repository/memory/index.js.map +0 -1
  104. package/dist/repository/types.d.ts.map +0 -1
  105. package/dist/repository/types.js +0 -39
  106. package/dist/repository/types.js.map +0 -1
  107. package/dist/repository/utils.d.ts +0 -22
  108. package/dist/repository/utils.d.ts.map +0 -1
  109. package/dist/repository/utils.js +0 -40
  110. package/dist/repository/utils.js.map +0 -1
  111. package/dist/sdk/admin-client.d.ts +0 -146
  112. package/dist/sdk/admin-client.d.ts.map +0 -1
  113. package/dist/sdk/admin-client.js +0 -196
  114. package/dist/sdk/admin-client.js.map +0 -1
  115. package/dist/sdk/client.d.ts +0 -144
  116. package/dist/sdk/client.d.ts.map +0 -1
  117. package/dist/sdk/client.js +0 -247
  118. package/dist/sdk/client.js.map +0 -1
  119. package/dist/sdk/errors.d.ts +0 -91
  120. package/dist/sdk/errors.d.ts.map +0 -1
  121. package/dist/sdk/errors.js +0 -185
  122. package/dist/sdk/errors.js.map +0 -1
  123. package/dist/sdk/index.d.ts.map +0 -1
  124. package/dist/sdk/types.d.ts +0 -110
  125. package/dist/sdk/types.d.ts.map +0 -1
  126. package/dist/sdk/types.js +0 -20
  127. package/dist/sdk/types.js.map +0 -1
  128. package/dist/service/credits-service.d.ts.map +0 -1
  129. package/dist/service/credits-service.js +0 -368
  130. package/dist/service/credits-service.js.map +0 -1
  131. package/dist/service/index.d.ts.map +0 -1
  132. package/dist/utils/error-utils.d.ts +0 -48
  133. package/dist/utils/error-utils.d.ts.map +0 -1
  134. package/dist/utils/error-utils.js +0 -57
  135. package/dist/utils/error-utils.js.map +0 -1
  136. package/dist/utils/index.d.ts +0 -54
  137. package/dist/utils/index.d.ts.map +0 -1
  138. package/dist/utils/index.js +0 -72
  139. package/dist/utils/index.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,64 +1,2408 @@
1
- /**
2
- * @nehorai/credits - Framework-agnostic credits system
3
- *
4
- * Provides a complete credits/billing system with:
5
- * - Two-phase commit for safe credit deduction
6
- * - Journal-based audit trail
7
- * - Subscription tier management
8
- * - Notification hooks
9
- * - REST API SDK
10
- *
11
- * @example
12
- * ```typescript
13
- * import {
14
- * CreditsService,
15
- * createInMemoryCreditRepository,
16
- * CreditError,
17
- * } from '@nehorai/credits';
18
- *
19
- * const repository = createInMemoryCreditRepository();
20
- * const service = new CreditsService(repository);
21
- *
22
- * // Check credits
23
- * const check = await service.checkCredits(userId, 10);
24
- * if (!check.hasCredits) {
25
- * throw new Error(`Need ${check.shortfall} more credits`);
26
- * }
27
- *
28
- * // Reserve and execute
29
- * const reservation = await service.reserveCredits(userId, 10, 'story_generation');
30
- * try {
31
- * await doWork();
32
- * await service.commitCredits(userId, reservation.id);
33
- * } catch (error) {
34
- * await service.releaseCredits(userId, reservation.id);
35
- * throw error;
36
- * }
37
- * ```
38
- */
39
- // ==================== Core ====================
40
- export * from "./core/index.js";
41
- // ==================== Config ====================
42
- export { getConfig, initializeConfig, loadConfigFromEnv, getValidOperationTypes, isValidOperationType, getConfigOperationCost, getConfigTierConfig, getConfigMonthlyLimit, isFeatureEnabled, resetConfig, getOperationLabel, getOperationLabels, } from "./config/index.js";
43
- export { registerConfigProvider, getConfigProvider, clearConfigProvider, } from "./config/provider.js";
44
- export { getOperationCosts, getOperationCost, getTierConfig, getMonthlyLimit, getDefaultFreeCredits, getReservationExpiryMs, getOperationCostsWithLabels, } from "./config/costs.js";
45
- export { toClientUserCredits } from "./repository/types.js";
46
- export { generateId, getNextMonthlyReset } from "./repository/utils.js";
47
- export { InMemoryCreditRepository, createInMemoryCreditRepository, } from "./repository/memory/index.js";
48
- // ==================== Auth ====================
49
- export * from "./auth/index.js";
50
- // ==================== Service ====================
51
- export * from "./service/index.js";
52
- // ==================== Adapters ====================
53
- export * from "./adapters/index.js";
54
- // ==================== Notifications ====================
55
- export { registerNotificationHandler, clearNotificationHandlers, clearCooldownState, checkAndNotifyLowBalance, notifySubscriptionExpiring, notifySubscriptionExpired, ConsoleNotificationHandler, } from "./notifications/index.js";
56
- // ==================== SDK ====================
57
- // Export SDK (excluding types that conflict with core)
58
- export { CreditsClient } from "./sdk/client.js";
59
- export { AdminCreditsClient } from "./sdk/admin-client.js";
60
- export { SDKErrorCode } from "./sdk/types.js";
61
- export { CreditsSDKError, NetworkError, AuthenticationError, AuthorizationError, InsufficientCreditsError as SDKInsufficientCreditsError, ReservationNotFoundError as SDKReservationNotFoundError, ReservationExpiredError as SDKReservationExpiredError, ValidationError, ServerError, parseApiError, } from "./sdk/errors.js";
62
- // ==================== Utils ====================
63
- export { getTotalCredits, generateRequestId, parseCreditError, isCreditErrorMessage, isPreviewMode, createDummyReservation, } from "./utils/index.js";
1
+ // src/core/types.ts
2
+ function calculateAvailableCredits(balance, bonusCredits, reserved) {
3
+ return balance + bonusCredits - reserved;
4
+ }
5
+ function toPortableTimestamp(value) {
6
+ if (!value) {
7
+ return (/* @__PURE__ */ new Date()).toISOString();
8
+ }
9
+ if (typeof value === "string") {
10
+ return value;
11
+ }
12
+ if (value instanceof Date) {
13
+ return value.toISOString();
14
+ }
15
+ if (typeof value === "object" && value !== null && "toDate" in value && typeof value.toDate === "function") {
16
+ return value.toDate().toISOString();
17
+ }
18
+ return (/* @__PURE__ */ new Date()).toISOString();
19
+ }
20
+ function toDate(value) {
21
+ if (value instanceof Date) {
22
+ return value;
23
+ }
24
+ if (typeof value === "string") {
25
+ return new Date(value);
26
+ }
27
+ if (typeof value === "object" && value !== null && "toDate" in value && typeof value.toDate === "function") {
28
+ return value.toDate();
29
+ }
30
+ return /* @__PURE__ */ new Date();
31
+ }
32
+
33
+ // src/core/errors.ts
34
+ var CreditErrorCode = {
35
+ INSUFFICIENT_CREDITS: "INSUFFICIENT_CREDITS",
36
+ RESERVATION_NOT_FOUND: "RESERVATION_NOT_FOUND",
37
+ RESERVATION_EXPIRED: "RESERVATION_EXPIRED",
38
+ RESERVATION_ALREADY_PROCESSED: "RESERVATION_ALREADY_PROCESSED",
39
+ USER_NOT_FOUND: "USER_NOT_FOUND",
40
+ INVALID_OPERATION_TYPE: "INVALID_OPERATION_TYPE",
41
+ CONFIGURATION_ERROR: "CONFIGURATION_ERROR",
42
+ DATABASE_ERROR: "DATABASE_ERROR"
43
+ };
44
+ var CreditError = class _CreditError extends Error {
45
+ code;
46
+ details;
47
+ constructor(message, code, details) {
48
+ super(message);
49
+ this.name = "CreditError";
50
+ this.code = code;
51
+ this.details = details;
52
+ if (Error.captureStackTrace) {
53
+ Error.captureStackTrace(this, _CreditError);
54
+ }
55
+ }
56
+ /**
57
+ * Convert to JSON-serializable object
58
+ */
59
+ toJSON() {
60
+ return {
61
+ name: this.name,
62
+ code: this.code,
63
+ message: this.message,
64
+ ...this.details && { details: this.details }
65
+ };
66
+ }
67
+ };
68
+ function isCreditError(error) {
69
+ return error instanceof CreditError;
70
+ }
71
+ function isInsufficientCreditsError(error) {
72
+ if (isCreditError(error)) {
73
+ return error.code === CreditErrorCode.INSUFFICIENT_CREDITS;
74
+ }
75
+ if (error instanceof Error) {
76
+ return error.message.toLowerCase().includes("insufficient credits");
77
+ }
78
+ return false;
79
+ }
80
+ function createInsufficientCreditsError(required, available) {
81
+ return new CreditError(
82
+ `Insufficient credits: available ${available}, required ${required}`,
83
+ CreditErrorCode.INSUFFICIENT_CREDITS,
84
+ { required, available, shortfall: required - available }
85
+ );
86
+ }
87
+ function createReservationNotFoundError(reservationId) {
88
+ return new CreditError(
89
+ `Reservation ${reservationId} not found`,
90
+ CreditErrorCode.RESERVATION_NOT_FOUND,
91
+ { reservationId }
92
+ );
93
+ }
94
+ function createReservationExpiredError(reservationId) {
95
+ return new CreditError(
96
+ `Reservation ${reservationId} has expired`,
97
+ CreditErrorCode.RESERVATION_EXPIRED,
98
+ { reservationId }
99
+ );
100
+ }
101
+ function createReservationAlreadyProcessedError(reservationId, status) {
102
+ return new CreditError(
103
+ `Reservation ${reservationId} has already been ${status}`,
104
+ CreditErrorCode.RESERVATION_ALREADY_PROCESSED,
105
+ { reservationId, status }
106
+ );
107
+ }
108
+ function createUserNotFoundError(userId) {
109
+ return new CreditError(
110
+ `User ${userId} not found`,
111
+ CreditErrorCode.USER_NOT_FOUND,
112
+ { userId }
113
+ );
114
+ }
115
+ function createInvalidOperationTypeError(operationType, validTypes) {
116
+ return new CreditError(
117
+ `Invalid operation type: ${operationType}. Valid types: ${validTypes.join(", ")}`,
118
+ CreditErrorCode.INVALID_OPERATION_TYPE,
119
+ { operationType, validTypes }
120
+ );
121
+ }
122
+
123
+ // src/core/deferred.ts
124
+ var genericDeferred = {
125
+ defer(fn) {
126
+ const scheduler = typeof setImmediate !== "undefined" ? setImmediate : setTimeout;
127
+ scheduler(() => {
128
+ fn().catch((error) => {
129
+ console.error("[Credits] Deferred task error:", error);
130
+ });
131
+ });
132
+ }
133
+ };
134
+ var synchronousDeferred = {
135
+ defer(fn) {
136
+ fn().catch((error) => {
137
+ console.error("[Credits] Deferred task error:", error);
138
+ });
139
+ }
140
+ };
141
+ function createDeferredExecutor(schedule) {
142
+ return {
143
+ defer: schedule
144
+ };
145
+ }
146
+ var noopDeferred = {
147
+ defer(_fn) {
148
+ }
149
+ };
150
+
151
+ // src/config/index.ts
152
+ import { z } from "zod";
153
+
154
+ // src/config/provider.ts
155
+ var configProvider = null;
156
+ function registerConfigProvider(provider) {
157
+ configProvider = provider;
158
+ }
159
+ function getConfigProvider() {
160
+ return configProvider;
161
+ }
162
+ function clearConfigProvider() {
163
+ configProvider = null;
164
+ }
165
+
166
+ // src/config/index.ts
167
+ var tierConfigSchema = z.object({
168
+ tier: z.enum(["free", "basic", "premium", "unlimited"]),
169
+ monthlyCredits: z.number().min(0),
170
+ priceUsd: z.number().min(0),
171
+ features: z.array(z.string())
172
+ });
173
+ var creditSystemConfigSchema = z.object({
174
+ /**
175
+ * Operation costs in credits
176
+ * Keys are operation type names (any non-empty string)
177
+ * Values are positive numbers representing credit cost
178
+ */
179
+ operationCosts: z.record(
180
+ z.string().min(1),
181
+ z.number().positive()
182
+ ),
183
+ /**
184
+ * Display labels for operation types (English canonical labels).
185
+ * Falls back to auto-generating from snake_case keys if not provided.
186
+ */
187
+ operationLabels: z.record(
188
+ z.string().min(1),
189
+ z.string().min(1)
190
+ ).optional().default({}),
191
+ /**
192
+ * Tier configurations
193
+ */
194
+ tierConfigs: z.record(
195
+ z.enum(["free", "basic", "premium", "unlimited"]),
196
+ tierConfigSchema
197
+ ),
198
+ /**
199
+ * Reservation expiry time in milliseconds
200
+ */
201
+ reservationExpiryMs: z.number().positive().default(5 * 60 * 1e3),
202
+ /**
203
+ * Default credits for new users (free tier)
204
+ */
205
+ defaultFreeCredits: z.number().positive().default(25),
206
+ /**
207
+ * Grace period in days after subscription expires before downgrade
208
+ */
209
+ subscriptionGracePeriodDays: z.number().min(0).default(3),
210
+ /**
211
+ * Low balance notification threshold
212
+ */
213
+ lowBalanceThreshold: z.number().min(0).default(10),
214
+ /**
215
+ * Cooldown in hours between low balance notifications
216
+ */
217
+ lowBalanceNotificationCooldownHours: z.number().positive().default(24),
218
+ /**
219
+ * Feature flags for optional features
220
+ */
221
+ features: z.object({
222
+ journalEntries: z.boolean().default(true),
223
+ notifications: z.boolean().default(true),
224
+ subscriptionExpiry: z.boolean().default(true),
225
+ usageHistory: z.boolean().default(true)
226
+ }).default({
227
+ journalEntries: true,
228
+ notifications: true,
229
+ subscriptionExpiry: true,
230
+ usageHistory: true
231
+ })
232
+ });
233
+ var DEFAULT_CONFIG = {
234
+ operationCosts: {
235
+ story_generation: 5,
236
+ conversation: 2,
237
+ image_generation: 10,
238
+ template_generation: 10
239
+ },
240
+ operationLabels: {
241
+ story_generation: "Story generation",
242
+ conversation: "Conversation",
243
+ image_generation: "Image generation",
244
+ template_generation: "Template generation"
245
+ },
246
+ tierConfigs: {
247
+ free: {
248
+ tier: "free",
249
+ monthlyCredits: 25,
250
+ priceUsd: 0,
251
+ features: [
252
+ "25 credits per month",
253
+ "Basic story generation",
254
+ "Standard image quality"
255
+ ]
256
+ },
257
+ basic: {
258
+ tier: "basic",
259
+ monthlyCredits: 200,
260
+ priceUsd: 9.99,
261
+ features: [
262
+ "200 credits per month",
263
+ "All story features",
264
+ "High quality images",
265
+ "Priority support"
266
+ ]
267
+ },
268
+ premium: {
269
+ tier: "premium",
270
+ monthlyCredits: 500,
271
+ priceUsd: 19.99,
272
+ features: [
273
+ "500 credits per month",
274
+ "All features",
275
+ "Highest quality images",
276
+ "Early access to new features",
277
+ "Priority support"
278
+ ]
279
+ },
280
+ unlimited: {
281
+ tier: "unlimited",
282
+ monthlyCredits: 0,
283
+ // 0 = unlimited
284
+ priceUsd: 49.99,
285
+ features: [
286
+ "Unlimited credits",
287
+ "All features",
288
+ "Highest quality images",
289
+ "Early access to new features",
290
+ "Dedicated support"
291
+ ]
292
+ }
293
+ },
294
+ reservationExpiryMs: 5 * 60 * 1e3,
295
+ // 5 minutes
296
+ defaultFreeCredits: 25,
297
+ subscriptionGracePeriodDays: 3,
298
+ lowBalanceThreshold: 10,
299
+ lowBalanceNotificationCooldownHours: 24,
300
+ features: {
301
+ journalEntries: true,
302
+ notifications: true,
303
+ subscriptionExpiry: true,
304
+ usageHistory: true
305
+ }
306
+ };
307
+ var currentConfig = DEFAULT_CONFIG;
308
+ function loadConfigFromEnv() {
309
+ const envConfig = {};
310
+ const operationCostsEnv = process.env.CREDITS_OPERATION_COSTS;
311
+ if (operationCostsEnv) {
312
+ try {
313
+ const parsed = JSON.parse(operationCostsEnv);
314
+ if (typeof parsed === "object" && parsed !== null) {
315
+ envConfig.operationCosts = {
316
+ ...DEFAULT_CONFIG.operationCosts,
317
+ ...parsed
318
+ };
319
+ }
320
+ } catch {
321
+ console.warn("[Credits Config] Failed to parse CREDITS_OPERATION_COSTS env var as JSON");
322
+ }
323
+ }
324
+ const reservationExpiry = process.env.CREDITS_RESERVATION_EXPIRY_MS;
325
+ if (reservationExpiry) {
326
+ envConfig.reservationExpiryMs = parseInt(reservationExpiry, 10);
327
+ }
328
+ const defaultCredits = process.env.CREDITS_DEFAULT_FREE;
329
+ if (defaultCredits) {
330
+ envConfig.defaultFreeCredits = parseInt(defaultCredits, 10);
331
+ }
332
+ const gracePeriod = process.env.CREDITS_GRACE_PERIOD_DAYS;
333
+ if (gracePeriod) {
334
+ envConfig.subscriptionGracePeriodDays = parseInt(gracePeriod, 10);
335
+ }
336
+ const lowBalanceThreshold = process.env.CREDITS_LOW_BALANCE_THRESHOLD;
337
+ if (lowBalanceThreshold) {
338
+ envConfig.lowBalanceThreshold = parseInt(lowBalanceThreshold, 10);
339
+ }
340
+ const featuresEnv = process.env.CREDITS_FEATURES;
341
+ if (featuresEnv) {
342
+ try {
343
+ const features = JSON.parse(featuresEnv);
344
+ envConfig.features = {
345
+ ...DEFAULT_CONFIG.features,
346
+ ...features
347
+ };
348
+ } catch {
349
+ console.warn("[Credits Config] Failed to parse CREDITS_FEATURES env var");
350
+ }
351
+ }
352
+ return envConfig;
353
+ }
354
+ function initializeConfig(overrides) {
355
+ const envConfig = loadConfigFromEnv();
356
+ const mergedConfig = {
357
+ ...DEFAULT_CONFIG,
358
+ ...envConfig,
359
+ ...overrides,
360
+ operationCosts: {
361
+ ...DEFAULT_CONFIG.operationCosts,
362
+ ...envConfig.operationCosts,
363
+ ...overrides?.operationCosts
364
+ },
365
+ operationLabels: {
366
+ ...DEFAULT_CONFIG.operationLabels,
367
+ ...envConfig.operationLabels,
368
+ ...overrides?.operationLabels
369
+ },
370
+ tierConfigs: {
371
+ ...DEFAULT_CONFIG.tierConfigs,
372
+ ...envConfig.tierConfigs,
373
+ ...overrides?.tierConfigs
374
+ },
375
+ features: {
376
+ ...DEFAULT_CONFIG.features,
377
+ ...envConfig.features,
378
+ ...overrides?.features
379
+ }
380
+ };
381
+ const result = creditSystemConfigSchema.safeParse(mergedConfig);
382
+ if (!result.success) {
383
+ console.error("[Credits Config] Invalid configuration:", result.error.issues);
384
+ throw new Error("Invalid credit system configuration");
385
+ }
386
+ currentConfig = result.data;
387
+ return currentConfig;
388
+ }
389
+ function getConfig() {
390
+ const provider = getConfigProvider();
391
+ if (provider) {
392
+ return provider.getConfig();
393
+ }
394
+ return currentConfig;
395
+ }
396
+ function getValidOperationTypes() {
397
+ return Object.keys(getConfig().operationCosts);
398
+ }
399
+ function isValidOperationType(type) {
400
+ return type in getConfig().operationCosts;
401
+ }
402
+ function getConfigOperationCost(operationType) {
403
+ const cost = getConfig().operationCosts[operationType];
404
+ if (cost === void 0) {
405
+ throw new Error(
406
+ `Unknown operation type: ${operationType}. Valid types: ${getValidOperationTypes().join(", ")}`
407
+ );
408
+ }
409
+ return cost;
410
+ }
411
+ function getConfigTierConfig(tier) {
412
+ return getConfig().tierConfigs[tier];
413
+ }
414
+ function getConfigMonthlyLimit(tier) {
415
+ const config = getConfig().tierConfigs[tier];
416
+ return config.monthlyCredits === 0 ? Infinity : config.monthlyCredits;
417
+ }
418
+ function isFeatureEnabled(feature) {
419
+ return getConfig().features[feature];
420
+ }
421
+ function snakeCaseToTitleCase(str) {
422
+ return str.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
423
+ }
424
+ function getOperationLabel(operationType) {
425
+ const labels = getConfig().operationLabels;
426
+ if (labels && operationType in labels) {
427
+ return labels[operationType];
428
+ }
429
+ return snakeCaseToTitleCase(operationType);
430
+ }
431
+ function getOperationLabels() {
432
+ const config = getConfig();
433
+ const result = {};
434
+ for (const key of Object.keys(config.operationCosts)) {
435
+ result[key] = config.operationLabels?.[key] ?? snakeCaseToTitleCase(key);
436
+ }
437
+ return result;
438
+ }
439
+ function resetConfig() {
440
+ currentConfig = DEFAULT_CONFIG;
441
+ }
442
+ initializeConfig();
443
+
444
+ // src/core/operations.ts
445
+ async function commitReservationWithJournal(repository, userId, reservationId) {
446
+ const reservation = await repository.getReservation(userId, reservationId);
447
+ if (!reservation) {
448
+ throw new Error(`Reservation ${reservationId} not found`);
449
+ }
450
+ await repository.commitReservationAtomic(userId, reservationId);
451
+ const credits = await repository.getUserCredits(userId);
452
+ if (credits) {
453
+ await repository.createJournalEntry({
454
+ userId,
455
+ entryType: "debit",
456
+ amount: reservation.amount,
457
+ balanceAfter: credits.balance,
458
+ source: "operation_commit",
459
+ referenceId: reservationId,
460
+ referenceType: "reservation",
461
+ description: `Committed ${reservation.amount} credits for ${getOperationLabel(reservation.operationType)}`,
462
+ metadata: {
463
+ operationType: reservation.operationType
464
+ }
465
+ });
466
+ }
467
+ }
468
+ async function releaseReservationWithJournal(repository, userId, reservationId) {
469
+ const reservation = await repository.getReservation(userId, reservationId);
470
+ await repository.releaseReservationAtomic(userId, reservationId);
471
+ if (reservation?.status === "reserved") {
472
+ const credits = await repository.getUserCredits(userId);
473
+ if (credits) {
474
+ await repository.createJournalEntry({
475
+ userId,
476
+ entryType: "credit",
477
+ amount: 0,
478
+ // No actual credits returned (they were reserved, not spent)
479
+ balanceAfter: credits.balance,
480
+ source: "operation_release",
481
+ referenceId: reservationId,
482
+ referenceType: "reservation",
483
+ description: `Released ${reservation.amount} reserved credits for ${getOperationLabel(reservation.operationType)}`,
484
+ metadata: {
485
+ operationType: reservation.operationType,
486
+ amount: reservation.amount
487
+ }
488
+ });
489
+ }
490
+ }
491
+ }
492
+ async function reserveCreditsForOperation(repository, userId, amount, operationType, expiryMs = 5 * 60 * 1e3) {
493
+ const expiresAt = new Date(Date.now() + expiryMs);
494
+ return repository.reserveCreditsAtomic(userId, amount, operationType, expiresAt);
495
+ }
496
+
497
+ // src/config/costs.ts
498
+ function getOperationCosts() {
499
+ return { ...getConfig().operationCosts };
500
+ }
501
+ function getOperationCost(type) {
502
+ return getConfigOperationCost(type);
503
+ }
504
+ function getTierConfig(tier) {
505
+ return getConfigTierConfig(tier);
506
+ }
507
+ function getMonthlyLimit(tier) {
508
+ return getConfigMonthlyLimit(tier);
509
+ }
510
+ function getDefaultFreeCredits() {
511
+ return getConfig().defaultFreeCredits;
512
+ }
513
+ function getReservationExpiryMs() {
514
+ return getConfig().reservationExpiryMs;
515
+ }
516
+ function getOperationCostsWithLabels() {
517
+ const config = getConfig();
518
+ const result = {};
519
+ for (const [key, cost] of Object.entries(config.operationCosts)) {
520
+ result[key] = {
521
+ key,
522
+ cost,
523
+ label: getOperationLabel(key)
524
+ };
525
+ }
526
+ return result;
527
+ }
528
+
529
+ // src/repository/types.ts
530
+ function toClientUserCredits(credits) {
531
+ return {
532
+ userId: credits.userId,
533
+ balance: credits.balance,
534
+ bonusCredits: credits.bonusCredits ?? 0,
535
+ reserved: credits.reserved,
536
+ tier: credits.tier,
537
+ monthlyLimit: credits.monthlyLimit,
538
+ monthlyUsed: credits.monthlyUsed,
539
+ monthlyResetAt: toISOString(credits.monthlyResetAt),
540
+ subscriptionExpiresAt: credits.subscriptionExpiresAt ? toISOString(credits.subscriptionExpiresAt) : null,
541
+ createdAt: toISOString(credits.createdAt),
542
+ updatedAt: toISOString(credits.updatedAt)
543
+ };
544
+ }
545
+ function toISOString(value) {
546
+ if (!value) return (/* @__PURE__ */ new Date()).toISOString();
547
+ if (typeof value === "string") return value;
548
+ if (value instanceof Date) return value.toISOString();
549
+ if (typeof value === "object" && "toDate" in value && typeof value.toDate === "function") {
550
+ return value.toDate().toISOString();
551
+ }
552
+ return (/* @__PURE__ */ new Date()).toISOString();
553
+ }
554
+
555
+ // src/repository/utils.ts
556
+ function generateId() {
557
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
558
+ }
559
+ function toDate2(value) {
560
+ if (value instanceof Date) return value;
561
+ if (typeof value === "string") return new Date(value);
562
+ if (typeof value === "object" && value !== null && "toDate" in value) {
563
+ return value.toDate();
564
+ }
565
+ return /* @__PURE__ */ new Date();
566
+ }
567
+ function getNextMonthlyReset(from = /* @__PURE__ */ new Date()) {
568
+ const next = new Date(from);
569
+ next.setMonth(next.getMonth() + 1);
570
+ next.setDate(1);
571
+ next.setHours(0, 0, 0, 0);
572
+ return next;
573
+ }
574
+
575
+ // src/repository/memory/index.ts
576
+ var InMemoryCreditRepository = class {
577
+ users = /* @__PURE__ */ new Map();
578
+ reservations = /* @__PURE__ */ new Map();
579
+ transactions = /* @__PURE__ */ new Map();
580
+ usageLogs = [];
581
+ journalEntries = /* @__PURE__ */ new Map();
582
+ // ==================== User Credits ====================
583
+ async getUserCredits(userId) {
584
+ return this.users.get(userId) ?? null;
585
+ }
586
+ async initializeUserCredits(userId, tier, initialBalance) {
587
+ const now = (/* @__PURE__ */ new Date()).toISOString();
588
+ const credits = {
589
+ userId,
590
+ balance: initialBalance,
591
+ bonusCredits: 0,
592
+ reserved: 0,
593
+ tier,
594
+ monthlyLimit: getConfigMonthlyLimit(tier),
595
+ monthlyUsed: 0,
596
+ monthlyResetAt: getNextMonthlyReset().toISOString(),
597
+ subscriptionExpiresAt: null,
598
+ createdAt: now,
599
+ updatedAt: now
600
+ };
601
+ this.users.set(userId, credits);
602
+ return credits;
603
+ }
604
+ async updateUserCredits(userId, updates) {
605
+ const credits = this.users.get(userId);
606
+ if (!credits) {
607
+ throw new Error(`User ${userId} not found`);
608
+ }
609
+ const now = (/* @__PURE__ */ new Date()).toISOString();
610
+ if (updates.balance !== void 0) credits.balance = updates.balance;
611
+ if (updates.bonusCredits !== void 0) credits.bonusCredits = updates.bonusCredits;
612
+ if (updates.reserved !== void 0) credits.reserved = updates.reserved;
613
+ if (updates.tier !== void 0) credits.tier = updates.tier;
614
+ if (updates.monthlyLimit !== void 0) credits.monthlyLimit = updates.monthlyLimit;
615
+ if (updates.monthlyUsed !== void 0) credits.monthlyUsed = updates.monthlyUsed;
616
+ if (updates.monthlyResetAt !== void 0) {
617
+ credits.monthlyResetAt = updates.monthlyResetAt instanceof Date ? updates.monthlyResetAt.toISOString() : updates.monthlyResetAt;
618
+ }
619
+ if (updates.subscriptionExpiresAt !== void 0) {
620
+ if (updates.subscriptionExpiresAt === null) {
621
+ credits.subscriptionExpiresAt = null;
622
+ } else {
623
+ credits.subscriptionExpiresAt = updates.subscriptionExpiresAt instanceof Date ? updates.subscriptionExpiresAt.toISOString() : updates.subscriptionExpiresAt;
624
+ }
625
+ }
626
+ if (updates.balanceIncrement !== void 0) {
627
+ credits.balance += updates.balanceIncrement;
628
+ }
629
+ if (updates.bonusCreditsIncrement !== void 0) {
630
+ credits.bonusCredits += updates.bonusCreditsIncrement;
631
+ }
632
+ if (updates.reservedIncrement !== void 0) {
633
+ credits.reserved += updates.reservedIncrement;
634
+ }
635
+ if (updates.monthlyUsedIncrement !== void 0) {
636
+ credits.monthlyUsed += updates.monthlyUsedIncrement;
637
+ }
638
+ credits.updatedAt = now;
639
+ this.users.set(userId, credits);
640
+ }
641
+ async updateUserTier(userId, input) {
642
+ const credits = this.users.get(userId);
643
+ if (!credits) {
644
+ throw new Error(`User ${userId} not found`);
645
+ }
646
+ credits.tier = input.tier;
647
+ credits.monthlyLimit = input.monthlyLimit;
648
+ if (input.balance !== void 0) credits.balance = input.balance;
649
+ if (input.monthlyUsed !== void 0) credits.monthlyUsed = input.monthlyUsed;
650
+ if (input.subscriptionExpiresAt !== void 0) {
651
+ if (input.subscriptionExpiresAt === null) {
652
+ credits.subscriptionExpiresAt = null;
653
+ } else {
654
+ credits.subscriptionExpiresAt = input.subscriptionExpiresAt instanceof Date ? input.subscriptionExpiresAt.toISOString() : input.subscriptionExpiresAt;
655
+ }
656
+ }
657
+ credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
658
+ this.users.set(userId, credits);
659
+ }
660
+ // ==================== Reservations ====================
661
+ async createReservation(input) {
662
+ const now = (/* @__PURE__ */ new Date()).toISOString();
663
+ const reservation = {
664
+ id: generateId(),
665
+ userId: input.userId,
666
+ amount: input.amount,
667
+ operationType: input.operationType,
668
+ status: "reserved",
669
+ createdAt: now,
670
+ expiresAt: input.expiresAt.toISOString()
671
+ };
672
+ if (!this.reservations.has(input.userId)) {
673
+ this.reservations.set(input.userId, /* @__PURE__ */ new Map());
674
+ }
675
+ this.reservations.get(input.userId).set(reservation.id, reservation);
676
+ return reservation;
677
+ }
678
+ async getReservation(userId, reservationId) {
679
+ const userReservations = this.reservations.get(userId);
680
+ if (!userReservations) return null;
681
+ return userReservations.get(reservationId) ?? null;
682
+ }
683
+ async updateReservationStatus(userId, reservationId, status, completedAt) {
684
+ const userReservations = this.reservations.get(userId);
685
+ if (!userReservations) {
686
+ throw new Error(`No reservations found for user ${userId}`);
687
+ }
688
+ const reservation = userReservations.get(reservationId);
689
+ if (!reservation) {
690
+ throw new Error(`Reservation ${reservationId} not found`);
691
+ }
692
+ reservation.status = status;
693
+ if (completedAt) reservation.completedAt = completedAt.toISOString();
694
+ userReservations.set(reservationId, reservation);
695
+ }
696
+ // ==================== Atomic Operations ====================
697
+ async reserveCreditsAtomic(userId, amount, operationType, expiresAt) {
698
+ const credits = this.users.get(userId);
699
+ if (!credits) {
700
+ throw new Error(`User ${userId} not found`);
701
+ }
702
+ const available = credits.balance + credits.bonusCredits - credits.reserved;
703
+ if (available < amount) {
704
+ throw new Error(
705
+ `Insufficient credits. Available: ${available}, Required: ${amount}`
706
+ );
707
+ }
708
+ const reservation = await this.createReservation({
709
+ userId,
710
+ amount,
711
+ operationType,
712
+ expiresAt
713
+ });
714
+ credits.reserved += amount;
715
+ credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
716
+ this.users.set(userId, credits);
717
+ return reservation;
718
+ }
719
+ async commitReservationAtomic(userId, reservationId) {
720
+ const credits = this.users.get(userId);
721
+ if (!credits) {
722
+ throw new Error(`User ${userId} not found`);
723
+ }
724
+ const reservation = await this.getReservation(userId, reservationId);
725
+ if (!reservation) {
726
+ throw new Error(`Reservation ${reservationId} not found`);
727
+ }
728
+ if (reservation.status === "committed") {
729
+ return;
730
+ }
731
+ if (reservation.status !== "reserved") {
732
+ throw new Error(
733
+ `Cannot commit reservation in ${reservation.status} state`
734
+ );
735
+ }
736
+ const amount = reservation.amount;
737
+ credits.balance -= amount;
738
+ credits.reserved -= amount;
739
+ credits.monthlyUsed += amount;
740
+ credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
741
+ this.users.set(userId, credits);
742
+ await this.updateReservationStatus(userId, reservationId, "committed", /* @__PURE__ */ new Date());
743
+ }
744
+ async releaseReservationAtomic(userId, reservationId) {
745
+ const credits = this.users.get(userId);
746
+ if (!credits) {
747
+ throw new Error(`User ${userId} not found`);
748
+ }
749
+ const reservation = await this.getReservation(userId, reservationId);
750
+ if (!reservation) {
751
+ throw new Error(`Reservation ${reservationId} not found`);
752
+ }
753
+ if (reservation.status !== "reserved") {
754
+ return;
755
+ }
756
+ credits.reserved -= reservation.amount;
757
+ credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
758
+ this.users.set(userId, credits);
759
+ await this.updateReservationStatus(userId, reservationId, "released", /* @__PURE__ */ new Date());
760
+ }
761
+ async addCreditsAtomic(userId, amount, description, paymentRef) {
762
+ const credits = this.users.get(userId);
763
+ if (!credits) {
764
+ throw new Error(`User ${userId} not found`);
765
+ }
766
+ const previousBalance = credits.bonusCredits;
767
+ credits.bonusCredits += amount;
768
+ credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
769
+ this.users.set(userId, credits);
770
+ await this.createTransaction({
771
+ userId,
772
+ type: "purchase",
773
+ amount,
774
+ description,
775
+ paymentRef,
776
+ previousBalance,
777
+ newBalance: credits.bonusCredits
778
+ });
779
+ }
780
+ async deductCreditsAtomic(userId, amount) {
781
+ if (amount <= 0) {
782
+ throw new Error(`deductCreditsAtomic amount must be positive (got ${amount})`);
783
+ }
784
+ const credits = this.users.get(userId);
785
+ if (!credits) {
786
+ throw new Error(`User credits not found for userId: ${userId}`);
787
+ }
788
+ const available = credits.balance + credits.bonusCredits - credits.reserved;
789
+ if (available < amount) {
790
+ throw new Error(
791
+ `Insufficient credits. Available: ${available}, requested: ${amount}`
792
+ );
793
+ }
794
+ const balanceDeduction = Math.min(credits.balance, amount);
795
+ const bonusDeduction = amount - balanceDeduction;
796
+ const previousBalance = credits.balance + credits.bonusCredits;
797
+ credits.balance -= balanceDeduction;
798
+ credits.bonusCredits -= bonusDeduction;
799
+ credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
800
+ this.users.set(userId, credits);
801
+ return { previousBalance, newBalance: previousBalance - amount };
802
+ }
803
+ // ==================== Transactions ====================
804
+ async createTransaction(input) {
805
+ const transaction = {
806
+ id: generateId(),
807
+ userId: input.userId,
808
+ type: input.type,
809
+ amount: input.amount,
810
+ description: input.description,
811
+ paymentRef: input.paymentRef,
812
+ previousBalance: input.previousBalance,
813
+ newBalance: input.newBalance,
814
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
815
+ };
816
+ if (!this.transactions.has(input.userId)) {
817
+ this.transactions.set(input.userId, []);
818
+ }
819
+ this.transactions.get(input.userId).push(transaction);
820
+ return transaction;
821
+ }
822
+ async getTransactions(userId, limit = 50, offset = 0) {
823
+ const userTransactions = this.transactions.get(userId) ?? [];
824
+ const sorted = [...userTransactions].sort((a, b) => {
825
+ const aDate = toDate2(a.createdAt).getTime();
826
+ const bDate = toDate2(b.createdAt).getTime();
827
+ return bDate - aDate;
828
+ });
829
+ return sorted.slice(offset, offset + limit);
830
+ }
831
+ // ==================== Usage Logs ====================
832
+ async logUsage(input) {
833
+ const log = {
834
+ id: generateId(),
835
+ userId: input.userId,
836
+ operationType: input.operationType,
837
+ provider: input.provider,
838
+ creditsUsed: input.creditsUsed,
839
+ success: input.success,
840
+ errorMessage: input.errorMessage,
841
+ resourceId: input.resourceId,
842
+ resourceType: input.resourceType,
843
+ requestId: input.requestId,
844
+ metadata: input.metadata,
845
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
846
+ };
847
+ this.usageLogs.push(log);
848
+ return log;
849
+ }
850
+ async getUsageLogs(query) {
851
+ let results = [...this.usageLogs];
852
+ if (query.userId) {
853
+ results = results.filter((log) => log.userId === query.userId);
854
+ }
855
+ if (query.operationType) {
856
+ results = results.filter((log) => log.operationType === query.operationType);
857
+ }
858
+ if (query.success !== void 0) {
859
+ results = results.filter((log) => log.success === query.success);
860
+ }
861
+ if (query.startDate) {
862
+ const startTime = query.startDate.getTime();
863
+ results = results.filter(
864
+ (log) => toDate2(log.createdAt).getTime() >= startTime
865
+ );
866
+ }
867
+ if (query.endDate) {
868
+ const endTime = query.endDate.getTime();
869
+ results = results.filter(
870
+ (log) => toDate2(log.createdAt).getTime() <= endTime
871
+ );
872
+ }
873
+ results.sort((a, b) => {
874
+ return toDate2(b.createdAt).getTime() - toDate2(a.createdAt).getTime();
875
+ });
876
+ const offset = query.offset ?? 0;
877
+ const limit = query.limit ?? 50;
878
+ return results.slice(offset, offset + limit);
879
+ }
880
+ async getUsageLogsCount(query) {
881
+ const results = await this.getUsageLogs({ ...query, limit: Infinity, offset: 0 });
882
+ return results.length;
883
+ }
884
+ // ==================== Journal Entries ====================
885
+ async createJournalEntry(input) {
886
+ const entry = {
887
+ id: generateId(),
888
+ userId: input.userId,
889
+ entryType: input.entryType,
890
+ amount: input.amount,
891
+ balanceAfter: input.balanceAfter,
892
+ source: input.source,
893
+ referenceId: input.referenceId,
894
+ referenceType: input.referenceType,
895
+ description: input.description,
896
+ metadata: input.metadata,
897
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
898
+ };
899
+ if (!this.journalEntries.has(input.userId)) {
900
+ this.journalEntries.set(input.userId, []);
901
+ }
902
+ this.journalEntries.get(input.userId).push(entry);
903
+ return entry;
904
+ }
905
+ async getJournalEntries(query) {
906
+ let results = this.journalEntries.get(query.userId) ?? [];
907
+ if (query.source) {
908
+ results = results.filter((entry) => entry.source === query.source);
909
+ }
910
+ if (query.referenceType) {
911
+ results = results.filter((entry) => entry.referenceType === query.referenceType);
912
+ }
913
+ if (query.startDate) {
914
+ const startTime = query.startDate.getTime();
915
+ results = results.filter(
916
+ (entry) => toDate2(entry.createdAt).getTime() >= startTime
917
+ );
918
+ }
919
+ if (query.endDate) {
920
+ const endTime = query.endDate.getTime();
921
+ results = results.filter(
922
+ (entry) => toDate2(entry.createdAt).getTime() <= endTime
923
+ );
924
+ }
925
+ results = [...results].sort((a, b) => {
926
+ return toDate2(b.createdAt).getTime() - toDate2(a.createdAt).getTime();
927
+ });
928
+ const offset = query.offset ?? 0;
929
+ const limit = query.limit ?? 50;
930
+ return results.slice(offset, offset + limit);
931
+ }
932
+ async getJournalEntriesCount(query) {
933
+ const results = await this.getJournalEntries({
934
+ ...query,
935
+ limit: Infinity,
936
+ offset: 0
937
+ });
938
+ return results.length;
939
+ }
940
+ // ==================== Cleanup Operations ====================
941
+ async findAndExpireReservations(_batchSize = 100, _maxIterations = 100) {
942
+ const now = /* @__PURE__ */ new Date();
943
+ let expiredCount = 0;
944
+ let creditsReleased = 0;
945
+ const errors = [];
946
+ for (const [userId, userReservations] of this.reservations) {
947
+ for (const [reservationId, reservation] of userReservations) {
948
+ if (reservation.status === "reserved" && toDate2(reservation.expiresAt).getTime() < now.getTime()) {
949
+ try {
950
+ await this.releaseReservationAtomic(userId, reservationId);
951
+ reservation.status = "expired";
952
+ userReservations.set(reservationId, reservation);
953
+ expiredCount++;
954
+ creditsReleased += reservation.amount;
955
+ } catch (error) {
956
+ errors.push(
957
+ `Failed to expire reservation ${reservationId}: ${error}`
958
+ );
959
+ }
960
+ }
961
+ }
962
+ }
963
+ return { expiredCount, creditsReleased, errors };
964
+ }
965
+ // ==================== Atomic Monthly Reset ====================
966
+ async atomicMonthlyReset(userId, tier, expectedResetAt) {
967
+ const credits = this.users.get(userId);
968
+ if (!credits) {
969
+ throw new Error(`User ${userId} not found`);
970
+ }
971
+ const currentResetAt = toDate2(credits.monthlyResetAt).getTime();
972
+ const expected = toDate2(expectedResetAt).getTime();
973
+ if (currentResetAt !== expected) {
974
+ return { wasReset: false, credits };
975
+ }
976
+ const newBalance = getConfigMonthlyLimit(tier);
977
+ const nextReset = getNextMonthlyReset();
978
+ credits.balance = newBalance === Infinity ? credits.balance : newBalance;
979
+ credits.monthlyUsed = 0;
980
+ credits.monthlyResetAt = nextReset.toISOString();
981
+ credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
982
+ this.users.set(userId, credits);
983
+ return { wasReset: true, credits };
984
+ }
985
+ // ==================== Subscription Expiry ====================
986
+ async checkAndHandleSubscriptionExpiry(userId, gracePeriodDays = 3) {
987
+ const credits = this.users.get(userId);
988
+ if (!credits) {
989
+ throw new Error(`User ${userId} not found`);
990
+ }
991
+ if (credits.tier === "free" || !credits.subscriptionExpiresAt) {
992
+ return {
993
+ wasDowngraded: false,
994
+ inGracePeriod: false,
995
+ graceDaysRemaining: 0,
996
+ credits
997
+ };
998
+ }
999
+ const now = /* @__PURE__ */ new Date();
1000
+ const expiresAt = toDate2(credits.subscriptionExpiresAt);
1001
+ const daysSinceExpiry = (now.getTime() - expiresAt.getTime()) / (1e3 * 60 * 60 * 24);
1002
+ if (daysSinceExpiry <= 0) {
1003
+ return {
1004
+ wasDowngraded: false,
1005
+ inGracePeriod: false,
1006
+ graceDaysRemaining: 0,
1007
+ credits
1008
+ };
1009
+ }
1010
+ if (daysSinceExpiry <= gracePeriodDays) {
1011
+ return {
1012
+ wasDowngraded: false,
1013
+ inGracePeriod: true,
1014
+ graceDaysRemaining: Math.ceil(gracePeriodDays - daysSinceExpiry),
1015
+ credits
1016
+ };
1017
+ }
1018
+ const config = getConfig();
1019
+ const freeTierConfig = config.tierConfigs.free;
1020
+ credits.tier = "free";
1021
+ credits.monthlyLimit = freeTierConfig.monthlyCredits;
1022
+ credits.balance = Math.min(credits.balance, freeTierConfig.monthlyCredits);
1023
+ credits.subscriptionExpiresAt = null;
1024
+ credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1025
+ this.users.set(userId, credits);
1026
+ return {
1027
+ wasDowngraded: true,
1028
+ inGracePeriod: false,
1029
+ graceDaysRemaining: 0,
1030
+ credits
1031
+ };
1032
+ }
1033
+ // ==================== Testing Utilities ====================
1034
+ /**
1035
+ * Clear all data (useful for testing)
1036
+ */
1037
+ clear() {
1038
+ this.users.clear();
1039
+ this.reservations.clear();
1040
+ this.transactions.clear();
1041
+ this.usageLogs = [];
1042
+ this.journalEntries.clear();
1043
+ }
1044
+ /**
1045
+ * Get all users (useful for testing/debugging)
1046
+ */
1047
+ getAllUsers() {
1048
+ return Array.from(this.users.values());
1049
+ }
1050
+ /**
1051
+ * Get all reservations for a user (useful for testing)
1052
+ */
1053
+ getAllReservations(userId) {
1054
+ const userReservations = this.reservations.get(userId);
1055
+ if (!userReservations) return [];
1056
+ return Array.from(userReservations.values());
1057
+ }
1058
+ };
1059
+ function createInMemoryCreditRepository() {
1060
+ return new InMemoryCreditRepository();
1061
+ }
1062
+
1063
+ // src/auth/api-key.ts
1064
+ function timingSafeEqual(a, b) {
1065
+ if (!a || !b) {
1066
+ return false;
1067
+ }
1068
+ const maxLength = Math.max(a.length, b.length);
1069
+ const paddedA = a.padEnd(maxLength);
1070
+ const paddedB = b.padEnd(maxLength);
1071
+ let result = 0;
1072
+ for (let i = 0; i < maxLength; i++) {
1073
+ result |= paddedA.charCodeAt(i) ^ paddedB.charCodeAt(i);
1074
+ }
1075
+ return result === 0 && a.length === b.length;
1076
+ }
1077
+ function verifyBearerToken(header, secret) {
1078
+ if (!header || !secret) {
1079
+ return false;
1080
+ }
1081
+ const token = header.replace(/^Bearer\s+/i, "");
1082
+ return timingSafeEqual(token, secret);
1083
+ }
1084
+ var ApiKeyCreditsProvider = class {
1085
+ apiKey;
1086
+ isValidated = false;
1087
+ constructor(authHeader, apiKey) {
1088
+ this.apiKey = apiKey ?? process.env.CREDITS_ADMIN_API_KEY;
1089
+ if (authHeader && this.apiKey) {
1090
+ this.isValidated = verifyBearerToken(authHeader, this.apiKey);
1091
+ }
1092
+ }
1093
+ async getCurrentUser() {
1094
+ if (!this.isValidated) {
1095
+ return null;
1096
+ }
1097
+ return {
1098
+ id: "api-admin",
1099
+ email: "api@admin.internal",
1100
+ name: "API Admin"
1101
+ };
1102
+ }
1103
+ async verifyAdminAccess() {
1104
+ return this.isValidated;
1105
+ }
1106
+ };
1107
+ function verifyAdminApiKey(authHeader, apiKey) {
1108
+ if (!authHeader) {
1109
+ return false;
1110
+ }
1111
+ const expectedKey = apiKey ?? process.env.CREDITS_ADMIN_API_KEY;
1112
+ if (!expectedKey) {
1113
+ console.warn("CREDITS_ADMIN_API_KEY not configured");
1114
+ return false;
1115
+ }
1116
+ return verifyBearerToken(authHeader, expectedKey);
1117
+ }
1118
+ function createApiKeyProvider(request, apiKey) {
1119
+ const authHeader = request.headers.get("Authorization");
1120
+ return new ApiKeyCreditsProvider(authHeader, apiKey);
1121
+ }
1122
+
1123
+ // src/service/credits-service.ts
1124
+ function isPastMonthlyReset(resetAt) {
1125
+ if (!resetAt) return false;
1126
+ const resetDate = toDate(resetAt);
1127
+ return /* @__PURE__ */ new Date() >= resetDate;
1128
+ }
1129
+ var CreditsService = class {
1130
+ constructor(repository) {
1131
+ this.repository = repository;
1132
+ }
1133
+ lowBalanceCallback;
1134
+ subscriptionExpiredCallback;
1135
+ /**
1136
+ * Set callback for low balance notifications
1137
+ */
1138
+ setLowBalanceCallback(callback) {
1139
+ this.lowBalanceCallback = callback;
1140
+ }
1141
+ /**
1142
+ * Set callback for subscription expired notifications
1143
+ */
1144
+ setSubscriptionExpiredCallback(callback) {
1145
+ this.subscriptionExpiredCallback = callback;
1146
+ }
1147
+ /**
1148
+ * Get user credits, performing monthly reset and subscription expiry checks if needed
1149
+ *
1150
+ * This method uses atomic operations to prevent race conditions:
1151
+ * 1. Checks subscription expiry with grace period
1152
+ * 2. Atomically performs monthly reset if needed (with optimistic locking)
1153
+ *
1154
+ * @param userId - User ID
1155
+ * @returns User credits or null if not found
1156
+ */
1157
+ async getUserCredits(userId) {
1158
+ let data = await this.repository.getUserCredits(userId);
1159
+ if (!data) {
1160
+ return null;
1161
+ }
1162
+ if (data.tier !== "free" && data.subscriptionExpiresAt) {
1163
+ const expiryResult = await this.repository.checkAndHandleSubscriptionExpiry(
1164
+ userId,
1165
+ getConfig().subscriptionGracePeriodDays
1166
+ );
1167
+ if (expiryResult.wasDowngraded) {
1168
+ await this.repository.createJournalEntry({
1169
+ userId,
1170
+ entryType: "debit",
1171
+ amount: 0,
1172
+ // No credits deducted, just tier change
1173
+ balanceAfter: expiryResult.credits.balance,
1174
+ source: "subscription_downgrade",
1175
+ referenceId: `downgrade-${Date.now()}`,
1176
+ referenceType: "subscription",
1177
+ description: `Subscription expired. Downgraded from ${data.tier} to free tier.`,
1178
+ metadata: {
1179
+ previousTier: data.tier,
1180
+ previousBalance: data.balance,
1181
+ newBalance: expiryResult.credits.balance
1182
+ }
1183
+ });
1184
+ if (this.subscriptionExpiredCallback) {
1185
+ this.subscriptionExpiredCallback(userId, true).catch((error) => {
1186
+ console.error("[Credits] Failed to send subscription expired notification:", error);
1187
+ });
1188
+ }
1189
+ }
1190
+ data = expiryResult.credits;
1191
+ }
1192
+ if (isPastMonthlyReset(data.monthlyResetAt)) {
1193
+ const expectedResetAt = toDate(data.monthlyResetAt);
1194
+ const resetResult = await this.repository.atomicMonthlyReset(
1195
+ userId,
1196
+ data.tier,
1197
+ expectedResetAt
1198
+ );
1199
+ if (resetResult.wasReset) {
1200
+ const balanceChange = resetResult.credits.balance - data.balance;
1201
+ if (balanceChange !== 0) {
1202
+ await this.repository.createJournalEntry({
1203
+ userId,
1204
+ entryType: balanceChange > 0 ? "credit" : "debit",
1205
+ amount: Math.abs(balanceChange),
1206
+ balanceAfter: resetResult.credits.balance,
1207
+ source: "monthly_reset",
1208
+ referenceId: `reset-${Date.now()}`,
1209
+ referenceType: "reset",
1210
+ description: `Monthly credit reset for ${data.tier} tier.`,
1211
+ metadata: {
1212
+ tier: data.tier,
1213
+ previousBalance: data.balance,
1214
+ newBalance: resetResult.credits.balance
1215
+ }
1216
+ });
1217
+ }
1218
+ }
1219
+ data = resetResult.credits;
1220
+ }
1221
+ return toClientUserCredits(data);
1222
+ }
1223
+ /**
1224
+ * Initialize credits for a new user with free tier
1225
+ * @param userId - User ID
1226
+ * @returns Initialized user credits
1227
+ */
1228
+ async initializeUserCredits(userId) {
1229
+ const credits = await this.repository.initializeUserCredits(
1230
+ userId,
1231
+ "free",
1232
+ getConfig().defaultFreeCredits
1233
+ );
1234
+ return toClientUserCredits(credits);
1235
+ }
1236
+ /**
1237
+ * Get or create user credits
1238
+ * Initializes with free tier if not exists
1239
+ * @param userId - User ID
1240
+ * @returns User credits
1241
+ */
1242
+ async getOrCreateUserCredits(userId) {
1243
+ const existing = await this.getUserCredits(userId);
1244
+ if (existing) {
1245
+ return existing;
1246
+ }
1247
+ return this.initializeUserCredits(userId);
1248
+ }
1249
+ /**
1250
+ * Check if user has sufficient credits for an operation
1251
+ * @param userId - User ID
1252
+ * @param requiredCredits - Credits required
1253
+ * @returns Credit check result
1254
+ */
1255
+ async checkCredits(userId, requiredCredits) {
1256
+ const credits = await this.getOrCreateUserCredits(userId);
1257
+ const totalBalance = credits.balance + credits.bonusCredits;
1258
+ const available = totalBalance - credits.reserved;
1259
+ const hasCredits = available >= requiredCredits;
1260
+ return {
1261
+ hasCredits,
1262
+ balance: totalBalance,
1263
+ required: requiredCredits,
1264
+ shortfall: hasCredits ? 0 : requiredCredits - available
1265
+ };
1266
+ }
1267
+ /**
1268
+ * Reserve credits for an operation (phase 1 of two-phase commit)
1269
+ * Creates a reservation and locks the credits
1270
+ * @param userId - User ID
1271
+ * @param amount - Credits to reserve
1272
+ * @param operationType - Operation type for tracking
1273
+ * @param options - Optional settings (e.g. a custom `ttlMs` for long-running async jobs)
1274
+ * @returns Reservation object
1275
+ * @throws Error if insufficient credits
1276
+ */
1277
+ async reserveCredits(userId, amount, operationType, options) {
1278
+ const ttlMs = options?.ttlMs ?? getConfig().reservationExpiryMs;
1279
+ const expiresAt = new Date(Date.now() + ttlMs);
1280
+ return this.repository.reserveCreditsAtomic(userId, amount, operationType, expiresAt);
1281
+ }
1282
+ /**
1283
+ * Commit a reservation (phase 2 of two-phase commit - success)
1284
+ * Deducts credits and marks reservation as committed
1285
+ * Also triggers low balance notifications if balance drops below threshold
1286
+ */
1287
+ async commitCredits(userId, reservationId) {
1288
+ const reservation = await this.repository.getReservation(userId, reservationId);
1289
+ if (!reservation) {
1290
+ throw new Error(`Reservation ${reservationId} not found`);
1291
+ }
1292
+ if (reservation.status === "committed") {
1293
+ return;
1294
+ }
1295
+ await this.repository.commitReservationAtomic(userId, reservationId);
1296
+ const credits = await this.repository.getUserCredits(userId);
1297
+ if (credits) {
1298
+ await this.repository.createJournalEntry({
1299
+ userId,
1300
+ entryType: "debit",
1301
+ amount: reservation.amount,
1302
+ balanceAfter: credits.balance,
1303
+ source: "operation_commit",
1304
+ referenceId: reservationId,
1305
+ referenceType: "reservation",
1306
+ description: `Committed ${reservation.amount} credits for ${getOperationLabel(reservation.operationType)}`,
1307
+ metadata: {
1308
+ operationType: reservation.operationType
1309
+ }
1310
+ });
1311
+ if (this.lowBalanceCallback) {
1312
+ this.lowBalanceCallback(userId, credits.balance).catch((error) => {
1313
+ console.error("[Credits] Failed to send low balance notification:", error);
1314
+ });
1315
+ }
1316
+ }
1317
+ }
1318
+ /**
1319
+ * Release a reservation (phase 2 of two-phase commit - failure)
1320
+ * Returns reserved credits and marks reservation as released
1321
+ */
1322
+ async releaseCredits(userId, reservationId) {
1323
+ const reservation = await this.repository.getReservation(userId, reservationId);
1324
+ await this.repository.releaseReservationAtomic(userId, reservationId);
1325
+ if (reservation?.status === "reserved") {
1326
+ const credits = await this.repository.getUserCredits(userId);
1327
+ if (credits) {
1328
+ await this.repository.createJournalEntry({
1329
+ userId,
1330
+ entryType: "credit",
1331
+ amount: 0,
1332
+ // No actual credits returned (they were reserved, not spent)
1333
+ balanceAfter: credits.balance,
1334
+ source: "operation_release",
1335
+ referenceId: reservationId,
1336
+ referenceType: "reservation",
1337
+ description: `Released ${reservation.amount} reserved credits for ${getOperationLabel(reservation.operationType)}`,
1338
+ metadata: {
1339
+ operationType: reservation.operationType,
1340
+ amount: reservation.amount
1341
+ }
1342
+ });
1343
+ }
1344
+ }
1345
+ }
1346
+ /**
1347
+ * Log usage for audit trail
1348
+ * @param log - Usage log data
1349
+ */
1350
+ async logUsage(log) {
1351
+ await this.repository.logUsage(log);
1352
+ }
1353
+ /**
1354
+ * Add credits to user account (for purchases, bonuses, etc.)
1355
+ * @param userId - User ID
1356
+ * @param amount - Credits to add
1357
+ * @param description - Transaction description
1358
+ * @param paymentRef - Optional payment reference
1359
+ */
1360
+ async addCredits(userId, amount, description, paymentRef) {
1361
+ return this.repository.addCreditsAtomic(userId, amount, description, paymentRef);
1362
+ }
1363
+ /**
1364
+ * Update user subscription tier
1365
+ * @param userId - User ID
1366
+ * @param tier - New subscription tier
1367
+ * @param expiresAt - Subscription expiry date (optional)
1368
+ */
1369
+ async updateTier(userId, tier, expiresAt) {
1370
+ const monthlyLimit = getConfigMonthlyLimit(tier);
1371
+ await this.repository.updateUserTier(userId, {
1372
+ tier,
1373
+ monthlyLimit: monthlyLimit === Infinity ? 0 : monthlyLimit,
1374
+ // Reset balance to new tier limit if upgrading
1375
+ balance: tier !== "free" ? monthlyLimit === Infinity ? 999999 : monthlyLimit : void 0,
1376
+ monthlyUsed: tier !== "free" ? 0 : void 0,
1377
+ subscriptionExpiresAt: expiresAt ? expiresAt.toISOString() : null
1378
+ });
1379
+ }
1380
+ /**
1381
+ * Get usage logs with optional filtering
1382
+ * @param userId - Optional user ID filter
1383
+ * @param limit - Max results
1384
+ * @param offset - Skip results
1385
+ * @returns List of usage logs
1386
+ */
1387
+ async getUsageLogs(userId, limit = 50, offset = 0) {
1388
+ return this.repository.getUsageLogs({ userId, limit, offset });
1389
+ }
1390
+ /**
1391
+ * Get user-friendly usage history
1392
+ * Combines journal entries into a user-facing format
1393
+ *
1394
+ * @param userId - User ID
1395
+ * @param limit - Max results per page
1396
+ * @param offset - Skip results for pagination
1397
+ * @returns Paginated usage history response
1398
+ */
1399
+ async getUsageHistory(userId, limit = 20, offset = 0) {
1400
+ const [entries, total] = await Promise.all([
1401
+ this.repository.getJournalEntries({ userId, limit, offset }),
1402
+ this.repository.getJournalEntriesCount({ userId })
1403
+ ]);
1404
+ const historyEntries = entries.map((entry) => {
1405
+ const type = this.mapSourceToHistoryType(entry.source);
1406
+ const creditsChange = entry.entryType === "credit" ? entry.amount : -entry.amount;
1407
+ return {
1408
+ id: entry.id,
1409
+ type,
1410
+ creditsChange,
1411
+ balanceAfter: entry.balanceAfter,
1412
+ description: entry.description,
1413
+ createdAt: typeof entry.createdAt === "string" ? entry.createdAt : toDate(entry.createdAt).toISOString()
1414
+ };
1415
+ });
1416
+ return {
1417
+ entries: historyEntries,
1418
+ pagination: {
1419
+ total,
1420
+ limit,
1421
+ offset,
1422
+ hasMore: offset + entries.length < total
1423
+ }
1424
+ };
1425
+ }
1426
+ /**
1427
+ * Map journal entry source to user-friendly history type
1428
+ */
1429
+ mapSourceToHistoryType(source) {
1430
+ switch (source) {
1431
+ case "operation_commit":
1432
+ case "reservation_expired":
1433
+ return "usage";
1434
+ case "purchase":
1435
+ return "purchase";
1436
+ case "subscription_grant":
1437
+ case "subscription_upgrade":
1438
+ case "bonus":
1439
+ return "bonus";
1440
+ case "monthly_reset":
1441
+ return "reset";
1442
+ case "refund":
1443
+ case "operation_release":
1444
+ return "refund";
1445
+ case "admin_adjustment":
1446
+ case "subscription_downgrade":
1447
+ case "expiry":
1448
+ default:
1449
+ return "adjustment";
1450
+ }
1451
+ }
1452
+ /**
1453
+ * Get journal entries directly (for admin or debugging)
1454
+ * @param query - Journal entry query parameters
1455
+ * @returns List of journal entries
1456
+ */
1457
+ async getJournalEntries(query) {
1458
+ return this.repository.getJournalEntries(query);
1459
+ }
1460
+ /**
1461
+ * Get the underlying repository (for advanced use cases)
1462
+ */
1463
+ getRepository() {
1464
+ return this.repository;
1465
+ }
1466
+ };
1467
+ function createCreditsService(repository) {
1468
+ return new CreditsService(repository);
1469
+ }
1470
+
1471
+ // src/adapters/generic.ts
1472
+ function generateRequestId() {
1473
+ const timestamp = Date.now();
1474
+ const randomPart = Math.random().toString(36).substring(2, 14);
1475
+ return `req_${timestamp}_${randomPart}`;
1476
+ }
1477
+ function createGenericAdapter(config) {
1478
+ const { repository, authProvider, deferred, operationCosts } = config;
1479
+ function getOperationCost2(operationType) {
1480
+ const cost = operationCosts[operationType];
1481
+ if (cost === void 0) {
1482
+ throw new Error(
1483
+ `Unknown operation type: "${operationType}". Configured types: ${Object.keys(operationCosts).join(", ")}`
1484
+ );
1485
+ }
1486
+ return cost;
1487
+ }
1488
+ function logUsage(params) {
1489
+ deferred.defer(async () => {
1490
+ await repository.logUsage({
1491
+ userId: params.userId,
1492
+ operationType: params.operationType,
1493
+ provider: "gemini",
1494
+ creditsUsed: params.creditsUsed,
1495
+ success: params.success,
1496
+ errorMessage: params.errorMessage,
1497
+ resourceId: params.resourceId,
1498
+ resourceType: params.resourceType,
1499
+ requestId: params.requestId
1500
+ });
1501
+ });
1502
+ }
1503
+ return {
1504
+ withCredits(options, handler) {
1505
+ return async (data) => {
1506
+ const requestId = generateRequestId();
1507
+ const user = await authProvider.getCurrentUser();
1508
+ if (!user?.id) {
1509
+ return { success: false, error: "Authentication required" };
1510
+ }
1511
+ if (isPreviewMode(data)) {
1512
+ const dummyReservation = createDummyReservation(user.id, options.operationType);
1513
+ return handler(user, data, dummyReservation);
1514
+ }
1515
+ let reservation;
1516
+ try {
1517
+ const cost = options.customCost ?? getOperationCost2(options.operationType);
1518
+ reservation = await reserveCreditsForOperation(
1519
+ repository,
1520
+ user.id,
1521
+ cost,
1522
+ options.operationType
1523
+ );
1524
+ } catch (error) {
1525
+ const message = error instanceof Error ? error.message : "Failed to reserve credits";
1526
+ logUsage({
1527
+ userId: user.id,
1528
+ operationType: options.operationType,
1529
+ creditsUsed: 0,
1530
+ success: false,
1531
+ errorMessage: message,
1532
+ resourceId: options.resourceId,
1533
+ resourceType: options.resourceType,
1534
+ requestId
1535
+ });
1536
+ return { success: false, error: message };
1537
+ }
1538
+ try {
1539
+ const result = await handler(user, data, reservation);
1540
+ if (result.success) {
1541
+ await commitReservationWithJournal(repository, user.id, reservation.id);
1542
+ logUsage({
1543
+ userId: user.id,
1544
+ operationType: options.operationType,
1545
+ creditsUsed: reservation.amount,
1546
+ success: true,
1547
+ resourceId: options.resourceId,
1548
+ resourceType: options.resourceType,
1549
+ requestId
1550
+ });
1551
+ } else {
1552
+ await releaseReservationWithJournal(repository, user.id, reservation.id);
1553
+ logUsage({
1554
+ userId: user.id,
1555
+ operationType: options.operationType,
1556
+ creditsUsed: 0,
1557
+ success: false,
1558
+ errorMessage: result.error,
1559
+ resourceId: options.resourceId,
1560
+ resourceType: options.resourceType,
1561
+ requestId
1562
+ });
1563
+ }
1564
+ return result;
1565
+ } catch (error) {
1566
+ const message = error instanceof Error ? error.message : "An unexpected error occurred";
1567
+ await releaseReservationWithJournal(repository, user.id, reservation.id);
1568
+ logUsage({
1569
+ userId: user.id,
1570
+ operationType: options.operationType,
1571
+ creditsUsed: 0,
1572
+ success: false,
1573
+ errorMessage: message,
1574
+ resourceId: options.resourceId,
1575
+ resourceType: options.resourceType,
1576
+ requestId
1577
+ });
1578
+ console.error("Action error:", error);
1579
+ return { success: false, error: message };
1580
+ }
1581
+ };
1582
+ }
1583
+ };
1584
+ }
1585
+ function isPreviewMode(data) {
1586
+ return typeof data === "object" && data !== null && "preview" in data && data.preview === true;
1587
+ }
1588
+ function createDummyReservation(userId, operationType) {
1589
+ return {
1590
+ id: "preview-mode",
1591
+ userId,
1592
+ amount: 0,
1593
+ operationType,
1594
+ status: "released",
1595
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1596
+ expiresAt: (/* @__PURE__ */ new Date()).toISOString()
1597
+ };
1598
+ }
1599
+
1600
+ // src/notifications/index.ts
1601
+ var cooldownState = /* @__PURE__ */ new Map();
1602
+ function isNotificationAllowed(userId, eventType) {
1603
+ const state = cooldownState.get(userId);
1604
+ if (!state) return true;
1605
+ const now = Date.now();
1606
+ let lastNotification;
1607
+ switch (eventType) {
1608
+ case "lowBalance":
1609
+ lastNotification = state.lastLowBalanceNotification;
1610
+ break;
1611
+ case "depleted":
1612
+ lastNotification = state.lastDepletedNotification;
1613
+ break;
1614
+ case "expiring":
1615
+ lastNotification = state.lastExpiringNotification;
1616
+ break;
1617
+ }
1618
+ if (!lastNotification) return true;
1619
+ return now - lastNotification.getTime() > getConfig().lowBalanceNotificationCooldownHours * 60 * 60 * 1e3;
1620
+ }
1621
+ function recordNotification(userId, eventType) {
1622
+ const state = cooldownState.get(userId) || {};
1623
+ const now = /* @__PURE__ */ new Date();
1624
+ switch (eventType) {
1625
+ case "lowBalance":
1626
+ state.lastLowBalanceNotification = now;
1627
+ break;
1628
+ case "depleted":
1629
+ state.lastDepletedNotification = now;
1630
+ break;
1631
+ case "expiring":
1632
+ state.lastExpiringNotification = now;
1633
+ break;
1634
+ }
1635
+ cooldownState.set(userId, state);
1636
+ }
1637
+ var notificationHandlers = [];
1638
+ function registerNotificationHandler(handler) {
1639
+ notificationHandlers.push(handler);
1640
+ }
1641
+ function clearNotificationHandlers() {
1642
+ notificationHandlers.length = 0;
1643
+ }
1644
+ function clearCooldownState() {
1645
+ cooldownState.clear();
1646
+ }
1647
+ async function dispatchNotification(event) {
1648
+ if (notificationHandlers.length === 0) {
1649
+ console.debug(`[Credits] No notification handlers registered for: ${event.type}`);
1650
+ return;
1651
+ }
1652
+ await Promise.allSettled(
1653
+ notificationHandlers.map(
1654
+ (handler) => handler.handleNotification(event).catch((error) => {
1655
+ console.error(`[Credits] Notification handler error:`, error);
1656
+ })
1657
+ )
1658
+ );
1659
+ }
1660
+ async function checkAndNotifyLowBalance(userId, newBalance, threshold = getConfig().lowBalanceThreshold) {
1661
+ if (newBalance <= 0 && isNotificationAllowed(userId, "depleted")) {
1662
+ recordNotification(userId, "depleted");
1663
+ await dispatchNotification({
1664
+ type: "balance_depleted",
1665
+ userId
1666
+ });
1667
+ return;
1668
+ }
1669
+ if (newBalance > 0 && newBalance <= threshold && isNotificationAllowed(userId, "lowBalance")) {
1670
+ recordNotification(userId, "lowBalance");
1671
+ await dispatchNotification({
1672
+ type: "low_balance",
1673
+ userId,
1674
+ balance: newBalance,
1675
+ threshold
1676
+ });
1677
+ }
1678
+ }
1679
+ async function notifySubscriptionExpiring(userId, expiresAt, daysRemaining) {
1680
+ if (!isNotificationAllowed(userId, "expiring")) {
1681
+ return;
1682
+ }
1683
+ recordNotification(userId, "expiring");
1684
+ await dispatchNotification({
1685
+ type: "subscription_expiring",
1686
+ userId,
1687
+ expiresAt,
1688
+ daysRemaining
1689
+ });
1690
+ }
1691
+ async function notifySubscriptionExpired(userId, wasDowngraded) {
1692
+ await dispatchNotification({
1693
+ type: "subscription_expired",
1694
+ userId,
1695
+ wasDowngraded
1696
+ });
1697
+ }
1698
+ var ConsoleNotificationHandler = class {
1699
+ async handleNotification(event) {
1700
+ switch (event.type) {
1701
+ case "low_balance":
1702
+ console.log(
1703
+ `[Credits Notification] Low balance alert for user ${event.userId}: ${event.balance} credits remaining (threshold: ${event.threshold})`
1704
+ );
1705
+ break;
1706
+ case "balance_depleted":
1707
+ console.log(
1708
+ `[Credits Notification] Balance depleted for user ${event.userId}`
1709
+ );
1710
+ break;
1711
+ case "subscription_expiring":
1712
+ console.log(
1713
+ `[Credits Notification] Subscription expiring for user ${event.userId}: ${event.daysRemaining} days remaining`
1714
+ );
1715
+ break;
1716
+ case "subscription_expired":
1717
+ console.log(
1718
+ `[Credits Notification] Subscription expired for user ${event.userId}: downgraded=${event.wasDowngraded}`
1719
+ );
1720
+ break;
1721
+ }
1722
+ }
1723
+ };
1724
+
1725
+ // src/sdk/types.ts
1726
+ var SDKErrorCode = {
1727
+ NETWORK_ERROR: "NETWORK_ERROR",
1728
+ AUTHENTICATION_ERROR: "AUTHENTICATION_ERROR",
1729
+ AUTHORIZATION_ERROR: "AUTHORIZATION_ERROR",
1730
+ INSUFFICIENT_CREDITS: "INSUFFICIENT_CREDITS",
1731
+ RESERVATION_NOT_FOUND: "RESERVATION_NOT_FOUND",
1732
+ RESERVATION_EXPIRED: "RESERVATION_EXPIRED",
1733
+ VALIDATION_ERROR: "VALIDATION_ERROR",
1734
+ SERVER_ERROR: "SERVER_ERROR",
1735
+ UNKNOWN_ERROR: "UNKNOWN_ERROR"
1736
+ };
1737
+
1738
+ // src/sdk/errors.ts
1739
+ var CreditsSDKError = class extends Error {
1740
+ code;
1741
+ details;
1742
+ statusCode;
1743
+ constructor(message, code, options) {
1744
+ super(message, { cause: options?.cause });
1745
+ this.name = "CreditsSDKError";
1746
+ this.code = code;
1747
+ this.details = options?.details;
1748
+ this.statusCode = options?.statusCode;
1749
+ }
1750
+ /**
1751
+ * Check if this is a specific error type
1752
+ */
1753
+ is(code) {
1754
+ return this.code === code;
1755
+ }
1756
+ /**
1757
+ * Create a JSON representation for logging
1758
+ */
1759
+ toJSON() {
1760
+ return {
1761
+ name: this.name,
1762
+ message: this.message,
1763
+ code: this.code,
1764
+ details: this.details,
1765
+ statusCode: this.statusCode
1766
+ };
1767
+ }
1768
+ };
1769
+ var NetworkError = class extends CreditsSDKError {
1770
+ constructor(message, cause) {
1771
+ super(message, SDKErrorCode.NETWORK_ERROR, { cause });
1772
+ this.name = "NetworkError";
1773
+ }
1774
+ };
1775
+ var AuthenticationError = class extends CreditsSDKError {
1776
+ constructor(message = "Authentication required") {
1777
+ super(message, SDKErrorCode.AUTHENTICATION_ERROR, { statusCode: 401 });
1778
+ this.name = "AuthenticationError";
1779
+ }
1780
+ };
1781
+ var AuthorizationError = class extends CreditsSDKError {
1782
+ constructor(message = "Access denied") {
1783
+ super(message, SDKErrorCode.AUTHORIZATION_ERROR, { statusCode: 403 });
1784
+ this.name = "AuthorizationError";
1785
+ }
1786
+ };
1787
+ var InsufficientCreditsError = class extends CreditsSDKError {
1788
+ available;
1789
+ required;
1790
+ constructor(available, required) {
1791
+ super(
1792
+ `Insufficient credits: available ${available}, required ${required}`,
1793
+ SDKErrorCode.INSUFFICIENT_CREDITS,
1794
+ {
1795
+ statusCode: 402,
1796
+ details: { available, required }
1797
+ }
1798
+ );
1799
+ this.name = "InsufficientCreditsError";
1800
+ this.available = available;
1801
+ this.required = required;
1802
+ }
1803
+ };
1804
+ var ReservationNotFoundError = class extends CreditsSDKError {
1805
+ reservationId;
1806
+ constructor(reservationId) {
1807
+ super(
1808
+ `Reservation not found: ${reservationId}`,
1809
+ SDKErrorCode.RESERVATION_NOT_FOUND,
1810
+ {
1811
+ statusCode: 404,
1812
+ details: { reservationId }
1813
+ }
1814
+ );
1815
+ this.name = "ReservationNotFoundError";
1816
+ this.reservationId = reservationId;
1817
+ }
1818
+ };
1819
+ var ReservationExpiredError = class extends CreditsSDKError {
1820
+ reservationId;
1821
+ constructor(reservationId) {
1822
+ super(
1823
+ `Reservation expired: ${reservationId}`,
1824
+ SDKErrorCode.RESERVATION_EXPIRED,
1825
+ {
1826
+ statusCode: 410,
1827
+ details: { reservationId }
1828
+ }
1829
+ );
1830
+ this.name = "ReservationExpiredError";
1831
+ this.reservationId = reservationId;
1832
+ }
1833
+ };
1834
+ var ValidationError = class extends CreditsSDKError {
1835
+ field;
1836
+ constructor(message, field) {
1837
+ super(message, SDKErrorCode.VALIDATION_ERROR, {
1838
+ statusCode: 400,
1839
+ details: field ? { field } : void 0
1840
+ });
1841
+ this.name = "ValidationError";
1842
+ this.field = field;
1843
+ }
1844
+ };
1845
+ var ServerError = class extends CreditsSDKError {
1846
+ constructor(message = "Internal server error") {
1847
+ super(message, SDKErrorCode.SERVER_ERROR, { statusCode: 500 });
1848
+ this.name = "ServerError";
1849
+ }
1850
+ };
1851
+ function parseApiError(statusCode, body) {
1852
+ const errorCode = body.error?.code || "";
1853
+ const errorMessage = body.error?.message || "An error occurred";
1854
+ const details = body.error?.details;
1855
+ switch (statusCode) {
1856
+ case 401:
1857
+ return new AuthenticationError(errorMessage);
1858
+ case 403:
1859
+ return new AuthorizationError(errorMessage);
1860
+ case 402:
1861
+ if (details?.available !== void 0 && details?.required !== void 0) {
1862
+ return new InsufficientCreditsError(
1863
+ details.available,
1864
+ details.required
1865
+ );
1866
+ }
1867
+ return new CreditsSDKError(errorMessage, SDKErrorCode.INSUFFICIENT_CREDITS, {
1868
+ statusCode,
1869
+ details
1870
+ });
1871
+ case 404:
1872
+ if (errorCode === "RESERVATION_NOT_FOUND" && details?.reservationId) {
1873
+ return new ReservationNotFoundError(details.reservationId);
1874
+ }
1875
+ return new CreditsSDKError(errorMessage, SDKErrorCode.RESERVATION_NOT_FOUND, {
1876
+ statusCode,
1877
+ details
1878
+ });
1879
+ case 410:
1880
+ if (errorCode === "RESERVATION_EXPIRED" && details?.reservationId) {
1881
+ return new ReservationExpiredError(details.reservationId);
1882
+ }
1883
+ return new CreditsSDKError(errorMessage, SDKErrorCode.RESERVATION_EXPIRED, {
1884
+ statusCode,
1885
+ details
1886
+ });
1887
+ case 400:
1888
+ case 422:
1889
+ return new ValidationError(errorMessage, details?.field);
1890
+ case 500:
1891
+ default:
1892
+ if (statusCode >= 500) {
1893
+ return new ServerError(errorMessage);
1894
+ }
1895
+ return new CreditsSDKError(errorMessage, SDKErrorCode.UNKNOWN_ERROR, {
1896
+ statusCode,
1897
+ details
1898
+ });
1899
+ }
1900
+ }
1901
+
1902
+ // src/sdk/client.ts
1903
+ var CreditsClient = class {
1904
+ baseUrl;
1905
+ getAuthToken;
1906
+ fetchFn;
1907
+ constructor(config) {
1908
+ this.baseUrl = config.baseUrl.replace(/\/$/, "");
1909
+ this.getAuthToken = config.getAuthToken;
1910
+ this.fetchFn = config.fetch ?? fetch;
1911
+ }
1912
+ /**
1913
+ * Make an authenticated request to the API
1914
+ */
1915
+ async request(path, options = {}) {
1916
+ const { method = "GET", body, params } = options;
1917
+ let url = `${this.baseUrl}${path}`;
1918
+ if (params) {
1919
+ const searchParams = new URLSearchParams();
1920
+ for (const [key, value] of Object.entries(params)) {
1921
+ if (value !== void 0) {
1922
+ searchParams.set(key, String(value));
1923
+ }
1924
+ }
1925
+ const queryString = searchParams.toString();
1926
+ if (queryString) {
1927
+ url += `?${queryString}`;
1928
+ }
1929
+ }
1930
+ const headers = {
1931
+ "Content-Type": "application/json"
1932
+ };
1933
+ if (this.getAuthToken) {
1934
+ try {
1935
+ const token = await this.getAuthToken();
1936
+ if (token) {
1937
+ headers["Authorization"] = `Bearer ${token}`;
1938
+ }
1939
+ } catch {
1940
+ }
1941
+ }
1942
+ let response;
1943
+ try {
1944
+ response = await this.fetchFn(url, {
1945
+ method,
1946
+ headers,
1947
+ body: body ? JSON.stringify(body) : void 0
1948
+ });
1949
+ } catch (error) {
1950
+ throw new NetworkError(
1951
+ error instanceof Error ? error.message : "Network request failed",
1952
+ error instanceof Error ? error : void 0
1953
+ );
1954
+ }
1955
+ const responseBody = await response.json();
1956
+ if (!response.ok) {
1957
+ throw parseApiError(response.status, responseBody);
1958
+ }
1959
+ return responseBody.data;
1960
+ }
1961
+ /**
1962
+ * Get the current user's credit balance
1963
+ *
1964
+ * @returns User's credit information
1965
+ * @throws AuthenticationError if not authenticated
1966
+ */
1967
+ async getBalance() {
1968
+ return this.request("/api/v1/credits/balance");
1969
+ }
1970
+ /**
1971
+ * Check if the user has sufficient credits for an operation
1972
+ *
1973
+ * @param operationType - Type of operation to check
1974
+ * @param customCost - Optional custom cost to check against
1975
+ * @returns Credit availability status
1976
+ * @throws AuthenticationError if not authenticated
1977
+ */
1978
+ async checkAvailability(operationType, customCost) {
1979
+ return this.request("/api/v1/credits/availability", {
1980
+ params: {
1981
+ operationType,
1982
+ customCost: customCost?.toString()
1983
+ }
1984
+ });
1985
+ }
1986
+ /**
1987
+ * Reserve credits for an operation
1988
+ *
1989
+ * This is the first step in the two-phase commit pattern.
1990
+ * Reserved credits are held for 5 minutes before expiring.
1991
+ *
1992
+ * @param operationType - Type of operation
1993
+ * @param options - Optional reservation options
1994
+ * @returns Reservation details including ID and expiry
1995
+ * @throws InsufficientCreditsError if not enough credits
1996
+ * @throws AuthenticationError if not authenticated
1997
+ */
1998
+ async reserve(operationType, options) {
1999
+ return this.request("/api/v1/credits/reserve", {
2000
+ method: "POST",
2001
+ body: {
2002
+ operationType,
2003
+ ...options
2004
+ }
2005
+ });
2006
+ }
2007
+ /**
2008
+ * Commit a reservation (deduct credits)
2009
+ *
2010
+ * This is the second step in the two-phase commit pattern.
2011
+ * Call this after your operation succeeds.
2012
+ *
2013
+ * @param reservationId - ID of the reservation to commit
2014
+ * @throws ReservationNotFoundError if reservation doesn't exist
2015
+ * @throws ReservationExpiredError if reservation has expired
2016
+ * @throws AuthenticationError if not authenticated
2017
+ */
2018
+ async commit(reservationId) {
2019
+ await this.request("/api/v1/credits/commit", {
2020
+ method: "POST",
2021
+ body: { reservationId }
2022
+ });
2023
+ }
2024
+ /**
2025
+ * Release a reservation (refund credits)
2026
+ *
2027
+ * Call this if your operation fails after reserving credits.
2028
+ *
2029
+ * @param reservationId - ID of the reservation to release
2030
+ * @throws ReservationNotFoundError if reservation doesn't exist
2031
+ * @throws AuthenticationError if not authenticated
2032
+ */
2033
+ async release(reservationId) {
2034
+ await this.request("/api/v1/credits/release", {
2035
+ method: "POST",
2036
+ body: { reservationId }
2037
+ });
2038
+ }
2039
+ /**
2040
+ * Get usage history for the current user
2041
+ *
2042
+ * @param options - Optional pagination options
2043
+ * @returns Paginated list of usage entries
2044
+ * @throws AuthenticationError if not authenticated
2045
+ */
2046
+ async getHistory(options) {
2047
+ return this.request("/api/v1/credits/history", {
2048
+ params: {
2049
+ page: options?.page,
2050
+ limit: options?.limit
2051
+ }
2052
+ });
2053
+ }
2054
+ /**
2055
+ * Get the available credits (balance + bonus - reserved)
2056
+ *
2057
+ * Convenience method that calculates available credits from balance.
2058
+ *
2059
+ * @returns Number of available credits
2060
+ * @throws AuthenticationError if not authenticated
2061
+ */
2062
+ async getAvailableCredits() {
2063
+ const balance = await this.getBalance();
2064
+ return balance.balance + balance.bonusCredits - balance.reserved;
2065
+ }
2066
+ /**
2067
+ * Execute an operation with automatic credit handling
2068
+ *
2069
+ * This is a convenience method that handles the full two-phase commit
2070
+ * pattern automatically.
2071
+ *
2072
+ * @param operationType - Type of operation
2073
+ * @param operation - The operation to execute
2074
+ * @param options - Optional reservation options
2075
+ * @returns Result of the operation
2076
+ * @throws InsufficientCreditsError if not enough credits
2077
+ * @throws AuthenticationError if not authenticated
2078
+ *
2079
+ * @example
2080
+ * ```typescript
2081
+ * const result = await client.withCredits(
2082
+ * "story_generation",
2083
+ * async () => {
2084
+ * return generateStory(data);
2085
+ * }
2086
+ * );
2087
+ * ```
2088
+ */
2089
+ async withCredits(operationType, operation, options) {
2090
+ const reservation = await this.reserve(operationType, options);
2091
+ try {
2092
+ const result = await operation();
2093
+ await this.commit(reservation.reservationId);
2094
+ return result;
2095
+ } catch (error) {
2096
+ try {
2097
+ await this.release(reservation.reservationId);
2098
+ } catch {
2099
+ }
2100
+ throw error;
2101
+ }
2102
+ }
2103
+ };
2104
+
2105
+ // src/sdk/admin-client.ts
2106
+ var AdminCreditsClient = class {
2107
+ baseUrl;
2108
+ apiKey;
2109
+ fetchFn;
2110
+ constructor(config) {
2111
+ this.baseUrl = config.baseUrl.replace(/\/$/, "");
2112
+ this.apiKey = config.apiKey;
2113
+ this.fetchFn = config.fetch ?? fetch;
2114
+ }
2115
+ /**
2116
+ * Make an authenticated admin request to the API
2117
+ */
2118
+ async request(path, options = {}) {
2119
+ const { method = "GET", body, params } = options;
2120
+ let url = `${this.baseUrl}${path}`;
2121
+ if (params) {
2122
+ const searchParams = new URLSearchParams();
2123
+ for (const [key, value] of Object.entries(params)) {
2124
+ if (value !== void 0) {
2125
+ searchParams.set(key, String(value));
2126
+ }
2127
+ }
2128
+ const queryString = searchParams.toString();
2129
+ if (queryString) {
2130
+ url += `?${queryString}`;
2131
+ }
2132
+ }
2133
+ const headers = {
2134
+ "Content-Type": "application/json",
2135
+ "x-api-key": this.apiKey
2136
+ };
2137
+ let response;
2138
+ try {
2139
+ response = await this.fetchFn(url, {
2140
+ method,
2141
+ headers,
2142
+ body: body ? JSON.stringify(body) : void 0
2143
+ });
2144
+ } catch (error) {
2145
+ throw new NetworkError(
2146
+ error instanceof Error ? error.message : "Network request failed",
2147
+ error instanceof Error ? error : void 0
2148
+ );
2149
+ }
2150
+ const responseBody = await response.json();
2151
+ if (!response.ok) {
2152
+ throw parseApiError(response.status, responseBody);
2153
+ }
2154
+ return responseBody.data;
2155
+ }
2156
+ /**
2157
+ * Get a specific user's credits
2158
+ *
2159
+ * @param userId - ID of the user
2160
+ * @returns User's credit information
2161
+ * @throws AuthorizationError if API key is invalid
2162
+ */
2163
+ async getUserCredits(userId) {
2164
+ return this.request(`/api/admin/credits/users/${userId}`);
2165
+ }
2166
+ /**
2167
+ * Add credits to a user's account
2168
+ *
2169
+ * Credits are added to bonusCredits, which don't expire on monthly reset.
2170
+ *
2171
+ * @param userId - ID of the user
2172
+ * @param amount - Amount of credits to add
2173
+ * @param description - Reason for adding credits
2174
+ * @throws AuthorizationError if API key is invalid
2175
+ */
2176
+ async addCredits(userId, amount, description) {
2177
+ await this.request(
2178
+ `/api/admin/credits/users/${userId}/add`,
2179
+ {
2180
+ method: "POST",
2181
+ body: { amount, description }
2182
+ }
2183
+ );
2184
+ }
2185
+ /**
2186
+ * Update a user's subscription tier
2187
+ *
2188
+ * @param userId - ID of the user
2189
+ * @param tier - New subscription tier
2190
+ * @param expiresAt - Optional expiration date (ISO 8601)
2191
+ * @throws AuthorizationError if API key is invalid
2192
+ */
2193
+ async updateSubscription(userId, tier, expiresAt) {
2194
+ const body = { tier };
2195
+ if (expiresAt) {
2196
+ body.expiresAt = expiresAt;
2197
+ }
2198
+ await this.request(
2199
+ `/api/admin/credits/users/${userId}/subscription`,
2200
+ {
2201
+ method: "PUT",
2202
+ body
2203
+ }
2204
+ );
2205
+ }
2206
+ /**
2207
+ * Get the current credits configuration
2208
+ *
2209
+ * @returns Credits system configuration
2210
+ * @throws AuthorizationError if API key is invalid
2211
+ */
2212
+ async getConfig() {
2213
+ return this.request("/api/admin/credits/config");
2214
+ }
2215
+ /**
2216
+ * Update operation costs
2217
+ *
2218
+ * @param costs - New operation costs (operation type -> cost)
2219
+ * @throws AuthorizationError if API key is invalid
2220
+ */
2221
+ async updateCosts(costs) {
2222
+ await this.request("/api/admin/credits/config/costs", {
2223
+ method: "PUT",
2224
+ body: { operationCosts: costs }
2225
+ });
2226
+ }
2227
+ /**
2228
+ * List all users with their credits
2229
+ *
2230
+ * @param options - Pagination and filtering options
2231
+ * @returns Paginated list of users
2232
+ * @throws AuthorizationError if API key is invalid
2233
+ */
2234
+ async listUsers(options) {
2235
+ return this.request("/api/admin/credits/users", {
2236
+ params: {
2237
+ page: options?.page,
2238
+ limit: options?.limit,
2239
+ tier: options?.tier,
2240
+ search: options?.search
2241
+ }
2242
+ });
2243
+ }
2244
+ /**
2245
+ * Deduct credits from a user's account
2246
+ *
2247
+ * @param userId - ID of the user
2248
+ * @param amount - Amount of credits to deduct
2249
+ * @param description - Reason for deducting credits
2250
+ * @throws AuthorizationError if API key is invalid
2251
+ */
2252
+ async deductCredits(userId, amount, description) {
2253
+ await this.request(
2254
+ `/api/admin/credits/users/${userId}/deduct`,
2255
+ {
2256
+ method: "POST",
2257
+ body: { amount, description }
2258
+ }
2259
+ );
2260
+ }
2261
+ /**
2262
+ * Get usage statistics for a user
2263
+ *
2264
+ * @param userId - ID of the user
2265
+ * @param options - Optional date range
2266
+ * @returns Usage statistics
2267
+ * @throws AuthorizationError if API key is invalid
2268
+ */
2269
+ async getUserStats(userId, options) {
2270
+ return this.request(`/api/admin/credits/users/${userId}/stats`, {
2271
+ params: {
2272
+ startDate: options?.startDate,
2273
+ endDate: options?.endDate
2274
+ }
2275
+ });
2276
+ }
2277
+ };
2278
+
2279
+ // src/utils/error-utils.ts
2280
+ function parseCreditError(error) {
2281
+ const match = error.match(/Available:\s*(\d+)\s*,\s*Required:\s*(\d+)/i);
2282
+ if (!match) {
2283
+ return null;
2284
+ }
2285
+ const available = parseInt(match[1], 10);
2286
+ const required = parseInt(match[2], 10);
2287
+ if (isNaN(available) || isNaN(required)) {
2288
+ return null;
2289
+ }
2290
+ return {
2291
+ available,
2292
+ required,
2293
+ shortfall: required - available,
2294
+ rawMessage: error
2295
+ };
2296
+ }
2297
+ function isCreditErrorMessage(error) {
2298
+ const lowerError = error.toLowerCase();
2299
+ return lowerError.includes("insufficient credits") || lowerError.includes("not enough credits");
2300
+ }
2301
+
2302
+ // src/utils/index.ts
2303
+ function getTotalCredits(credits) {
2304
+ return credits.balance + credits.bonusCredits;
2305
+ }
2306
+ function generateRequestId2() {
2307
+ const timestamp = Date.now();
2308
+ const randomPart = Math.random().toString(36).substring(2, 14);
2309
+ return `req_${timestamp}_${randomPart}`;
2310
+ }
2311
+ function isPreviewMode2(data) {
2312
+ return typeof data === "object" && data !== null && "preview" in data && data.preview === true;
2313
+ }
2314
+ function createDummyReservation2(userId, operationType) {
2315
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2316
+ return {
2317
+ id: "preview-mode",
2318
+ userId,
2319
+ amount: 0,
2320
+ operationType,
2321
+ status: "released",
2322
+ createdAt: now,
2323
+ expiresAt: now
2324
+ };
2325
+ }
2326
+ export {
2327
+ AdminCreditsClient,
2328
+ ApiKeyCreditsProvider,
2329
+ AuthenticationError,
2330
+ AuthorizationError,
2331
+ ConsoleNotificationHandler,
2332
+ CreditError,
2333
+ CreditErrorCode,
2334
+ CreditsClient,
2335
+ CreditsSDKError,
2336
+ CreditsService,
2337
+ InMemoryCreditRepository,
2338
+ NetworkError,
2339
+ SDKErrorCode,
2340
+ InsufficientCreditsError as SDKInsufficientCreditsError,
2341
+ ReservationExpiredError as SDKReservationExpiredError,
2342
+ ReservationNotFoundError as SDKReservationNotFoundError,
2343
+ ServerError,
2344
+ ValidationError,
2345
+ calculateAvailableCredits,
2346
+ checkAndNotifyLowBalance,
2347
+ clearConfigProvider,
2348
+ clearCooldownState,
2349
+ clearNotificationHandlers,
2350
+ commitReservationWithJournal,
2351
+ createApiKeyProvider,
2352
+ createCreditsService,
2353
+ createDeferredExecutor,
2354
+ createDummyReservation2 as createDummyReservation,
2355
+ createGenericAdapter,
2356
+ createInMemoryCreditRepository,
2357
+ createInsufficientCreditsError,
2358
+ createInvalidOperationTypeError,
2359
+ createReservationAlreadyProcessedError,
2360
+ createReservationExpiredError,
2361
+ createReservationNotFoundError,
2362
+ createUserNotFoundError,
2363
+ generateId,
2364
+ generateRequestId2 as generateRequestId,
2365
+ genericDeferred,
2366
+ getConfig,
2367
+ getConfigMonthlyLimit,
2368
+ getConfigOperationCost,
2369
+ getConfigProvider,
2370
+ getConfigTierConfig,
2371
+ getDefaultFreeCredits,
2372
+ getMonthlyLimit,
2373
+ getNextMonthlyReset,
2374
+ getOperationCost,
2375
+ getOperationCosts,
2376
+ getOperationCostsWithLabels,
2377
+ getOperationLabel,
2378
+ getOperationLabels,
2379
+ getReservationExpiryMs,
2380
+ getTierConfig,
2381
+ getTotalCredits,
2382
+ getValidOperationTypes,
2383
+ initializeConfig,
2384
+ isCreditError,
2385
+ isCreditErrorMessage,
2386
+ isFeatureEnabled,
2387
+ isInsufficientCreditsError,
2388
+ isPreviewMode2 as isPreviewMode,
2389
+ isValidOperationType,
2390
+ loadConfigFromEnv,
2391
+ noopDeferred,
2392
+ notifySubscriptionExpired,
2393
+ notifySubscriptionExpiring,
2394
+ parseApiError,
2395
+ parseCreditError,
2396
+ registerConfigProvider,
2397
+ registerNotificationHandler,
2398
+ releaseReservationWithJournal,
2399
+ reserveCreditsForOperation,
2400
+ resetConfig,
2401
+ synchronousDeferred,
2402
+ toClientUserCredits,
2403
+ toDate,
2404
+ toPortableTimestamp,
2405
+ verifyAdminApiKey,
2406
+ verifyBearerToken
2407
+ };
64
2408
  //# sourceMappingURL=index.js.map