@nehorai/credits 1.2.0 → 1.3.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 +2506 -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 +2399 -63
  27. package/dist/index.js.map +1 -1
  28. package/dist/repository/index.cjs +816 -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 +783 -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 +670 -0
  41. package/dist/service/index.cjs.map +1 -0
  42. package/dist/service/{credits-service.d.ts → index.d.cts} +9 -7
  43. package/dist/service/index.d.ts +141 -3
  44. package/dist/service/index.js +641 -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,2400 @@
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 !== "reserved") {
729
+ throw new Error(
730
+ `Cannot commit reservation in ${reservation.status} state`
731
+ );
732
+ }
733
+ const amount = reservation.amount;
734
+ credits.balance -= amount;
735
+ credits.reserved -= amount;
736
+ credits.monthlyUsed += amount;
737
+ credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
738
+ this.users.set(userId, credits);
739
+ await this.updateReservationStatus(userId, reservationId, "committed", /* @__PURE__ */ new Date());
740
+ }
741
+ async releaseReservationAtomic(userId, reservationId) {
742
+ const credits = this.users.get(userId);
743
+ if (!credits) {
744
+ throw new Error(`User ${userId} not found`);
745
+ }
746
+ const reservation = await this.getReservation(userId, reservationId);
747
+ if (!reservation) {
748
+ throw new Error(`Reservation ${reservationId} not found`);
749
+ }
750
+ if (reservation.status !== "reserved") {
751
+ return;
752
+ }
753
+ credits.reserved -= reservation.amount;
754
+ credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
755
+ this.users.set(userId, credits);
756
+ await this.updateReservationStatus(userId, reservationId, "released", /* @__PURE__ */ new Date());
757
+ }
758
+ async addCreditsAtomic(userId, amount, description, paymentRef) {
759
+ const credits = this.users.get(userId);
760
+ if (!credits) {
761
+ throw new Error(`User ${userId} not found`);
762
+ }
763
+ const previousBalance = credits.bonusCredits;
764
+ credits.bonusCredits += amount;
765
+ credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
766
+ this.users.set(userId, credits);
767
+ await this.createTransaction({
768
+ userId,
769
+ type: "purchase",
770
+ amount,
771
+ description,
772
+ paymentRef,
773
+ previousBalance,
774
+ newBalance: credits.bonusCredits
775
+ });
776
+ }
777
+ async deductCreditsAtomic(userId, amount) {
778
+ if (amount <= 0) {
779
+ throw new Error(`deductCreditsAtomic amount must be positive (got ${amount})`);
780
+ }
781
+ const credits = this.users.get(userId);
782
+ if (!credits) {
783
+ throw new Error(`User credits not found for userId: ${userId}`);
784
+ }
785
+ const available = credits.balance + credits.bonusCredits - credits.reserved;
786
+ if (available < amount) {
787
+ throw new Error(
788
+ `Insufficient credits. Available: ${available}, requested: ${amount}`
789
+ );
790
+ }
791
+ const balanceDeduction = Math.min(credits.balance, amount);
792
+ const bonusDeduction = amount - balanceDeduction;
793
+ const previousBalance = credits.balance + credits.bonusCredits;
794
+ credits.balance -= balanceDeduction;
795
+ credits.bonusCredits -= bonusDeduction;
796
+ credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
797
+ this.users.set(userId, credits);
798
+ return { previousBalance, newBalance: previousBalance - amount };
799
+ }
800
+ // ==================== Transactions ====================
801
+ async createTransaction(input) {
802
+ const transaction = {
803
+ id: generateId(),
804
+ userId: input.userId,
805
+ type: input.type,
806
+ amount: input.amount,
807
+ description: input.description,
808
+ paymentRef: input.paymentRef,
809
+ previousBalance: input.previousBalance,
810
+ newBalance: input.newBalance,
811
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
812
+ };
813
+ if (!this.transactions.has(input.userId)) {
814
+ this.transactions.set(input.userId, []);
815
+ }
816
+ this.transactions.get(input.userId).push(transaction);
817
+ return transaction;
818
+ }
819
+ async getTransactions(userId, limit = 50, offset = 0) {
820
+ const userTransactions = this.transactions.get(userId) ?? [];
821
+ const sorted = [...userTransactions].sort((a, b) => {
822
+ const aDate = toDate2(a.createdAt).getTime();
823
+ const bDate = toDate2(b.createdAt).getTime();
824
+ return bDate - aDate;
825
+ });
826
+ return sorted.slice(offset, offset + limit);
827
+ }
828
+ // ==================== Usage Logs ====================
829
+ async logUsage(input) {
830
+ const log = {
831
+ id: generateId(),
832
+ userId: input.userId,
833
+ operationType: input.operationType,
834
+ provider: input.provider,
835
+ creditsUsed: input.creditsUsed,
836
+ success: input.success,
837
+ errorMessage: input.errorMessage,
838
+ resourceId: input.resourceId,
839
+ resourceType: input.resourceType,
840
+ requestId: input.requestId,
841
+ metadata: input.metadata,
842
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
843
+ };
844
+ this.usageLogs.push(log);
845
+ return log;
846
+ }
847
+ async getUsageLogs(query) {
848
+ let results = [...this.usageLogs];
849
+ if (query.userId) {
850
+ results = results.filter((log) => log.userId === query.userId);
851
+ }
852
+ if (query.operationType) {
853
+ results = results.filter((log) => log.operationType === query.operationType);
854
+ }
855
+ if (query.success !== void 0) {
856
+ results = results.filter((log) => log.success === query.success);
857
+ }
858
+ if (query.startDate) {
859
+ const startTime = query.startDate.getTime();
860
+ results = results.filter(
861
+ (log) => toDate2(log.createdAt).getTime() >= startTime
862
+ );
863
+ }
864
+ if (query.endDate) {
865
+ const endTime = query.endDate.getTime();
866
+ results = results.filter(
867
+ (log) => toDate2(log.createdAt).getTime() <= endTime
868
+ );
869
+ }
870
+ results.sort((a, b) => {
871
+ return toDate2(b.createdAt).getTime() - toDate2(a.createdAt).getTime();
872
+ });
873
+ const offset = query.offset ?? 0;
874
+ const limit = query.limit ?? 50;
875
+ return results.slice(offset, offset + limit);
876
+ }
877
+ async getUsageLogsCount(query) {
878
+ const results = await this.getUsageLogs({ ...query, limit: Infinity, offset: 0 });
879
+ return results.length;
880
+ }
881
+ // ==================== Journal Entries ====================
882
+ async createJournalEntry(input) {
883
+ const entry = {
884
+ id: generateId(),
885
+ userId: input.userId,
886
+ entryType: input.entryType,
887
+ amount: input.amount,
888
+ balanceAfter: input.balanceAfter,
889
+ source: input.source,
890
+ referenceId: input.referenceId,
891
+ referenceType: input.referenceType,
892
+ description: input.description,
893
+ metadata: input.metadata,
894
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
895
+ };
896
+ if (!this.journalEntries.has(input.userId)) {
897
+ this.journalEntries.set(input.userId, []);
898
+ }
899
+ this.journalEntries.get(input.userId).push(entry);
900
+ return entry;
901
+ }
902
+ async getJournalEntries(query) {
903
+ let results = this.journalEntries.get(query.userId) ?? [];
904
+ if (query.source) {
905
+ results = results.filter((entry) => entry.source === query.source);
906
+ }
907
+ if (query.referenceType) {
908
+ results = results.filter((entry) => entry.referenceType === query.referenceType);
909
+ }
910
+ if (query.startDate) {
911
+ const startTime = query.startDate.getTime();
912
+ results = results.filter(
913
+ (entry) => toDate2(entry.createdAt).getTime() >= startTime
914
+ );
915
+ }
916
+ if (query.endDate) {
917
+ const endTime = query.endDate.getTime();
918
+ results = results.filter(
919
+ (entry) => toDate2(entry.createdAt).getTime() <= endTime
920
+ );
921
+ }
922
+ results = [...results].sort((a, b) => {
923
+ return toDate2(b.createdAt).getTime() - toDate2(a.createdAt).getTime();
924
+ });
925
+ const offset = query.offset ?? 0;
926
+ const limit = query.limit ?? 50;
927
+ return results.slice(offset, offset + limit);
928
+ }
929
+ async getJournalEntriesCount(query) {
930
+ const results = await this.getJournalEntries({
931
+ ...query,
932
+ limit: Infinity,
933
+ offset: 0
934
+ });
935
+ return results.length;
936
+ }
937
+ // ==================== Cleanup Operations ====================
938
+ async findAndExpireReservations(_batchSize = 100, _maxIterations = 100) {
939
+ const now = /* @__PURE__ */ new Date();
940
+ let expiredCount = 0;
941
+ let creditsReleased = 0;
942
+ const errors = [];
943
+ for (const [userId, userReservations] of this.reservations) {
944
+ for (const [reservationId, reservation] of userReservations) {
945
+ if (reservation.status === "reserved" && toDate2(reservation.expiresAt).getTime() < now.getTime()) {
946
+ try {
947
+ await this.releaseReservationAtomic(userId, reservationId);
948
+ reservation.status = "expired";
949
+ userReservations.set(reservationId, reservation);
950
+ expiredCount++;
951
+ creditsReleased += reservation.amount;
952
+ } catch (error) {
953
+ errors.push(
954
+ `Failed to expire reservation ${reservationId}: ${error}`
955
+ );
956
+ }
957
+ }
958
+ }
959
+ }
960
+ return { expiredCount, creditsReleased, errors };
961
+ }
962
+ // ==================== Atomic Monthly Reset ====================
963
+ async atomicMonthlyReset(userId, tier, expectedResetAt) {
964
+ const credits = this.users.get(userId);
965
+ if (!credits) {
966
+ throw new Error(`User ${userId} not found`);
967
+ }
968
+ const currentResetAt = toDate2(credits.monthlyResetAt).getTime();
969
+ const expected = toDate2(expectedResetAt).getTime();
970
+ if (currentResetAt !== expected) {
971
+ return { wasReset: false, credits };
972
+ }
973
+ const newBalance = getConfigMonthlyLimit(tier);
974
+ const nextReset = getNextMonthlyReset();
975
+ credits.balance = newBalance === Infinity ? credits.balance : newBalance;
976
+ credits.monthlyUsed = 0;
977
+ credits.monthlyResetAt = nextReset.toISOString();
978
+ credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
979
+ this.users.set(userId, credits);
980
+ return { wasReset: true, credits };
981
+ }
982
+ // ==================== Subscription Expiry ====================
983
+ async checkAndHandleSubscriptionExpiry(userId, gracePeriodDays = 3) {
984
+ const credits = this.users.get(userId);
985
+ if (!credits) {
986
+ throw new Error(`User ${userId} not found`);
987
+ }
988
+ if (credits.tier === "free" || !credits.subscriptionExpiresAt) {
989
+ return {
990
+ wasDowngraded: false,
991
+ inGracePeriod: false,
992
+ graceDaysRemaining: 0,
993
+ credits
994
+ };
995
+ }
996
+ const now = /* @__PURE__ */ new Date();
997
+ const expiresAt = toDate2(credits.subscriptionExpiresAt);
998
+ const daysSinceExpiry = (now.getTime() - expiresAt.getTime()) / (1e3 * 60 * 60 * 24);
999
+ if (daysSinceExpiry <= 0) {
1000
+ return {
1001
+ wasDowngraded: false,
1002
+ inGracePeriod: false,
1003
+ graceDaysRemaining: 0,
1004
+ credits
1005
+ };
1006
+ }
1007
+ if (daysSinceExpiry <= gracePeriodDays) {
1008
+ return {
1009
+ wasDowngraded: false,
1010
+ inGracePeriod: true,
1011
+ graceDaysRemaining: Math.ceil(gracePeriodDays - daysSinceExpiry),
1012
+ credits
1013
+ };
1014
+ }
1015
+ const config = getConfig();
1016
+ const freeTierConfig = config.tierConfigs.free;
1017
+ credits.tier = "free";
1018
+ credits.monthlyLimit = freeTierConfig.monthlyCredits;
1019
+ credits.balance = Math.min(credits.balance, freeTierConfig.monthlyCredits);
1020
+ credits.subscriptionExpiresAt = null;
1021
+ credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1022
+ this.users.set(userId, credits);
1023
+ return {
1024
+ wasDowngraded: true,
1025
+ inGracePeriod: false,
1026
+ graceDaysRemaining: 0,
1027
+ credits
1028
+ };
1029
+ }
1030
+ // ==================== Testing Utilities ====================
1031
+ /**
1032
+ * Clear all data (useful for testing)
1033
+ */
1034
+ clear() {
1035
+ this.users.clear();
1036
+ this.reservations.clear();
1037
+ this.transactions.clear();
1038
+ this.usageLogs = [];
1039
+ this.journalEntries.clear();
1040
+ }
1041
+ /**
1042
+ * Get all users (useful for testing/debugging)
1043
+ */
1044
+ getAllUsers() {
1045
+ return Array.from(this.users.values());
1046
+ }
1047
+ /**
1048
+ * Get all reservations for a user (useful for testing)
1049
+ */
1050
+ getAllReservations(userId) {
1051
+ const userReservations = this.reservations.get(userId);
1052
+ if (!userReservations) return [];
1053
+ return Array.from(userReservations.values());
1054
+ }
1055
+ };
1056
+ function createInMemoryCreditRepository() {
1057
+ return new InMemoryCreditRepository();
1058
+ }
1059
+
1060
+ // src/auth/api-key.ts
1061
+ function timingSafeEqual(a, b) {
1062
+ if (!a || !b) {
1063
+ return false;
1064
+ }
1065
+ const maxLength = Math.max(a.length, b.length);
1066
+ const paddedA = a.padEnd(maxLength);
1067
+ const paddedB = b.padEnd(maxLength);
1068
+ let result = 0;
1069
+ for (let i = 0; i < maxLength; i++) {
1070
+ result |= paddedA.charCodeAt(i) ^ paddedB.charCodeAt(i);
1071
+ }
1072
+ return result === 0 && a.length === b.length;
1073
+ }
1074
+ function verifyBearerToken(header, secret) {
1075
+ if (!header || !secret) {
1076
+ return false;
1077
+ }
1078
+ const token = header.replace(/^Bearer\s+/i, "");
1079
+ return timingSafeEqual(token, secret);
1080
+ }
1081
+ var ApiKeyCreditsProvider = class {
1082
+ apiKey;
1083
+ isValidated = false;
1084
+ constructor(authHeader, apiKey) {
1085
+ this.apiKey = apiKey ?? process.env.CREDITS_ADMIN_API_KEY;
1086
+ if (authHeader && this.apiKey) {
1087
+ this.isValidated = verifyBearerToken(authHeader, this.apiKey);
1088
+ }
1089
+ }
1090
+ async getCurrentUser() {
1091
+ if (!this.isValidated) {
1092
+ return null;
1093
+ }
1094
+ return {
1095
+ id: "api-admin",
1096
+ email: "api@admin.internal",
1097
+ name: "API Admin"
1098
+ };
1099
+ }
1100
+ async verifyAdminAccess() {
1101
+ return this.isValidated;
1102
+ }
1103
+ };
1104
+ function verifyAdminApiKey(authHeader, apiKey) {
1105
+ if (!authHeader) {
1106
+ return false;
1107
+ }
1108
+ const expectedKey = apiKey ?? process.env.CREDITS_ADMIN_API_KEY;
1109
+ if (!expectedKey) {
1110
+ console.warn("CREDITS_ADMIN_API_KEY not configured");
1111
+ return false;
1112
+ }
1113
+ return verifyBearerToken(authHeader, expectedKey);
1114
+ }
1115
+ function createApiKeyProvider(request, apiKey) {
1116
+ const authHeader = request.headers.get("Authorization");
1117
+ return new ApiKeyCreditsProvider(authHeader, apiKey);
1118
+ }
1119
+
1120
+ // src/service/credits-service.ts
1121
+ function isPastMonthlyReset(resetAt) {
1122
+ if (!resetAt) return false;
1123
+ const resetDate = toDate(resetAt);
1124
+ return /* @__PURE__ */ new Date() >= resetDate;
1125
+ }
1126
+ var CreditsService = class {
1127
+ constructor(repository) {
1128
+ this.repository = repository;
1129
+ }
1130
+ lowBalanceCallback;
1131
+ subscriptionExpiredCallback;
1132
+ /**
1133
+ * Set callback for low balance notifications
1134
+ */
1135
+ setLowBalanceCallback(callback) {
1136
+ this.lowBalanceCallback = callback;
1137
+ }
1138
+ /**
1139
+ * Set callback for subscription expired notifications
1140
+ */
1141
+ setSubscriptionExpiredCallback(callback) {
1142
+ this.subscriptionExpiredCallback = callback;
1143
+ }
1144
+ /**
1145
+ * Get user credits, performing monthly reset and subscription expiry checks if needed
1146
+ *
1147
+ * This method uses atomic operations to prevent race conditions:
1148
+ * 1. Checks subscription expiry with grace period
1149
+ * 2. Atomically performs monthly reset if needed (with optimistic locking)
1150
+ *
1151
+ * @param userId - User ID
1152
+ * @returns User credits or null if not found
1153
+ */
1154
+ async getUserCredits(userId) {
1155
+ let data = await this.repository.getUserCredits(userId);
1156
+ if (!data) {
1157
+ return null;
1158
+ }
1159
+ if (data.tier !== "free" && data.subscriptionExpiresAt) {
1160
+ const expiryResult = await this.repository.checkAndHandleSubscriptionExpiry(
1161
+ userId,
1162
+ getConfig().subscriptionGracePeriodDays
1163
+ );
1164
+ if (expiryResult.wasDowngraded) {
1165
+ await this.repository.createJournalEntry({
1166
+ userId,
1167
+ entryType: "debit",
1168
+ amount: 0,
1169
+ // No credits deducted, just tier change
1170
+ balanceAfter: expiryResult.credits.balance,
1171
+ source: "subscription_downgrade",
1172
+ referenceId: `downgrade-${Date.now()}`,
1173
+ referenceType: "subscription",
1174
+ description: `Subscription expired. Downgraded from ${data.tier} to free tier.`,
1175
+ metadata: {
1176
+ previousTier: data.tier,
1177
+ previousBalance: data.balance,
1178
+ newBalance: expiryResult.credits.balance
1179
+ }
1180
+ });
1181
+ if (this.subscriptionExpiredCallback) {
1182
+ this.subscriptionExpiredCallback(userId, true).catch((error) => {
1183
+ console.error("[Credits] Failed to send subscription expired notification:", error);
1184
+ });
1185
+ }
1186
+ }
1187
+ data = expiryResult.credits;
1188
+ }
1189
+ if (isPastMonthlyReset(data.monthlyResetAt)) {
1190
+ const expectedResetAt = toDate(data.monthlyResetAt);
1191
+ const resetResult = await this.repository.atomicMonthlyReset(
1192
+ userId,
1193
+ data.tier,
1194
+ expectedResetAt
1195
+ );
1196
+ if (resetResult.wasReset) {
1197
+ const balanceChange = resetResult.credits.balance - data.balance;
1198
+ if (balanceChange !== 0) {
1199
+ await this.repository.createJournalEntry({
1200
+ userId,
1201
+ entryType: balanceChange > 0 ? "credit" : "debit",
1202
+ amount: Math.abs(balanceChange),
1203
+ balanceAfter: resetResult.credits.balance,
1204
+ source: "monthly_reset",
1205
+ referenceId: `reset-${Date.now()}`,
1206
+ referenceType: "reset",
1207
+ description: `Monthly credit reset for ${data.tier} tier.`,
1208
+ metadata: {
1209
+ tier: data.tier,
1210
+ previousBalance: data.balance,
1211
+ newBalance: resetResult.credits.balance
1212
+ }
1213
+ });
1214
+ }
1215
+ }
1216
+ data = resetResult.credits;
1217
+ }
1218
+ return toClientUserCredits(data);
1219
+ }
1220
+ /**
1221
+ * Initialize credits for a new user with free tier
1222
+ * @param userId - User ID
1223
+ * @returns Initialized user credits
1224
+ */
1225
+ async initializeUserCredits(userId) {
1226
+ const credits = await this.repository.initializeUserCredits(
1227
+ userId,
1228
+ "free",
1229
+ getConfig().defaultFreeCredits
1230
+ );
1231
+ return toClientUserCredits(credits);
1232
+ }
1233
+ /**
1234
+ * Get or create user credits
1235
+ * Initializes with free tier if not exists
1236
+ * @param userId - User ID
1237
+ * @returns User credits
1238
+ */
1239
+ async getOrCreateUserCredits(userId) {
1240
+ const existing = await this.getUserCredits(userId);
1241
+ if (existing) {
1242
+ return existing;
1243
+ }
1244
+ return this.initializeUserCredits(userId);
1245
+ }
1246
+ /**
1247
+ * Check if user has sufficient credits for an operation
1248
+ * @param userId - User ID
1249
+ * @param requiredCredits - Credits required
1250
+ * @returns Credit check result
1251
+ */
1252
+ async checkCredits(userId, requiredCredits) {
1253
+ const credits = await this.getOrCreateUserCredits(userId);
1254
+ const totalBalance = credits.balance + credits.bonusCredits;
1255
+ const available = totalBalance - credits.reserved;
1256
+ const hasCredits = available >= requiredCredits;
1257
+ return {
1258
+ hasCredits,
1259
+ balance: totalBalance,
1260
+ required: requiredCredits,
1261
+ shortfall: hasCredits ? 0 : requiredCredits - available
1262
+ };
1263
+ }
1264
+ /**
1265
+ * Reserve credits for an operation (phase 1 of two-phase commit)
1266
+ * Creates a reservation and locks the credits
1267
+ * @param userId - User ID
1268
+ * @param amount - Credits to reserve
1269
+ * @param operationType - Operation type for tracking
1270
+ * @returns Reservation object
1271
+ * @throws Error if insufficient credits
1272
+ */
1273
+ async reserveCredits(userId, amount, operationType) {
1274
+ const expiresAt = new Date(Date.now() + getConfig().reservationExpiryMs);
1275
+ return this.repository.reserveCreditsAtomic(userId, amount, operationType, expiresAt);
1276
+ }
1277
+ /**
1278
+ * Commit a reservation (phase 2 of two-phase commit - success)
1279
+ * Deducts credits and marks reservation as committed
1280
+ * Also triggers low balance notifications if balance drops below threshold
1281
+ */
1282
+ async commitCredits(userId, reservationId) {
1283
+ const reservation = await this.repository.getReservation(userId, reservationId);
1284
+ if (!reservation) {
1285
+ throw new Error(`Reservation ${reservationId} not found`);
1286
+ }
1287
+ await this.repository.commitReservationAtomic(userId, reservationId);
1288
+ const credits = await this.repository.getUserCredits(userId);
1289
+ if (credits) {
1290
+ await this.repository.createJournalEntry({
1291
+ userId,
1292
+ entryType: "debit",
1293
+ amount: reservation.amount,
1294
+ balanceAfter: credits.balance,
1295
+ source: "operation_commit",
1296
+ referenceId: reservationId,
1297
+ referenceType: "reservation",
1298
+ description: `Committed ${reservation.amount} credits for ${getOperationLabel(reservation.operationType)}`,
1299
+ metadata: {
1300
+ operationType: reservation.operationType
1301
+ }
1302
+ });
1303
+ if (this.lowBalanceCallback) {
1304
+ this.lowBalanceCallback(userId, credits.balance).catch((error) => {
1305
+ console.error("[Credits] Failed to send low balance notification:", error);
1306
+ });
1307
+ }
1308
+ }
1309
+ }
1310
+ /**
1311
+ * Release a reservation (phase 2 of two-phase commit - failure)
1312
+ * Returns reserved credits and marks reservation as released
1313
+ */
1314
+ async releaseCredits(userId, reservationId) {
1315
+ const reservation = await this.repository.getReservation(userId, reservationId);
1316
+ await this.repository.releaseReservationAtomic(userId, reservationId);
1317
+ if (reservation?.status === "reserved") {
1318
+ const credits = await this.repository.getUserCredits(userId);
1319
+ if (credits) {
1320
+ await this.repository.createJournalEntry({
1321
+ userId,
1322
+ entryType: "credit",
1323
+ amount: 0,
1324
+ // No actual credits returned (they were reserved, not spent)
1325
+ balanceAfter: credits.balance,
1326
+ source: "operation_release",
1327
+ referenceId: reservationId,
1328
+ referenceType: "reservation",
1329
+ description: `Released ${reservation.amount} reserved credits for ${getOperationLabel(reservation.operationType)}`,
1330
+ metadata: {
1331
+ operationType: reservation.operationType,
1332
+ amount: reservation.amount
1333
+ }
1334
+ });
1335
+ }
1336
+ }
1337
+ }
1338
+ /**
1339
+ * Log usage for audit trail
1340
+ * @param log - Usage log data
1341
+ */
1342
+ async logUsage(log) {
1343
+ await this.repository.logUsage(log);
1344
+ }
1345
+ /**
1346
+ * Add credits to user account (for purchases, bonuses, etc.)
1347
+ * @param userId - User ID
1348
+ * @param amount - Credits to add
1349
+ * @param description - Transaction description
1350
+ * @param paymentRef - Optional payment reference
1351
+ */
1352
+ async addCredits(userId, amount, description, paymentRef) {
1353
+ return this.repository.addCreditsAtomic(userId, amount, description, paymentRef);
1354
+ }
1355
+ /**
1356
+ * Update user subscription tier
1357
+ * @param userId - User ID
1358
+ * @param tier - New subscription tier
1359
+ * @param expiresAt - Subscription expiry date (optional)
1360
+ */
1361
+ async updateTier(userId, tier, expiresAt) {
1362
+ const monthlyLimit = getConfigMonthlyLimit(tier);
1363
+ await this.repository.updateUserTier(userId, {
1364
+ tier,
1365
+ monthlyLimit: monthlyLimit === Infinity ? 0 : monthlyLimit,
1366
+ // Reset balance to new tier limit if upgrading
1367
+ balance: tier !== "free" ? monthlyLimit === Infinity ? 999999 : monthlyLimit : void 0,
1368
+ monthlyUsed: tier !== "free" ? 0 : void 0,
1369
+ subscriptionExpiresAt: expiresAt ? expiresAt.toISOString() : null
1370
+ });
1371
+ }
1372
+ /**
1373
+ * Get usage logs with optional filtering
1374
+ * @param userId - Optional user ID filter
1375
+ * @param limit - Max results
1376
+ * @param offset - Skip results
1377
+ * @returns List of usage logs
1378
+ */
1379
+ async getUsageLogs(userId, limit = 50, offset = 0) {
1380
+ return this.repository.getUsageLogs({ userId, limit, offset });
1381
+ }
1382
+ /**
1383
+ * Get user-friendly usage history
1384
+ * Combines journal entries into a user-facing format
1385
+ *
1386
+ * @param userId - User ID
1387
+ * @param limit - Max results per page
1388
+ * @param offset - Skip results for pagination
1389
+ * @returns Paginated usage history response
1390
+ */
1391
+ async getUsageHistory(userId, limit = 20, offset = 0) {
1392
+ const [entries, total] = await Promise.all([
1393
+ this.repository.getJournalEntries({ userId, limit, offset }),
1394
+ this.repository.getJournalEntriesCount({ userId })
1395
+ ]);
1396
+ const historyEntries = entries.map((entry) => {
1397
+ const type = this.mapSourceToHistoryType(entry.source);
1398
+ const creditsChange = entry.entryType === "credit" ? entry.amount : -entry.amount;
1399
+ return {
1400
+ id: entry.id,
1401
+ type,
1402
+ creditsChange,
1403
+ balanceAfter: entry.balanceAfter,
1404
+ description: entry.description,
1405
+ createdAt: typeof entry.createdAt === "string" ? entry.createdAt : toDate(entry.createdAt).toISOString()
1406
+ };
1407
+ });
1408
+ return {
1409
+ entries: historyEntries,
1410
+ pagination: {
1411
+ total,
1412
+ limit,
1413
+ offset,
1414
+ hasMore: offset + entries.length < total
1415
+ }
1416
+ };
1417
+ }
1418
+ /**
1419
+ * Map journal entry source to user-friendly history type
1420
+ */
1421
+ mapSourceToHistoryType(source) {
1422
+ switch (source) {
1423
+ case "operation_commit":
1424
+ case "reservation_expired":
1425
+ return "usage";
1426
+ case "purchase":
1427
+ return "purchase";
1428
+ case "subscription_grant":
1429
+ case "subscription_upgrade":
1430
+ case "bonus":
1431
+ return "bonus";
1432
+ case "monthly_reset":
1433
+ return "reset";
1434
+ case "refund":
1435
+ case "operation_release":
1436
+ return "refund";
1437
+ case "admin_adjustment":
1438
+ case "subscription_downgrade":
1439
+ case "expiry":
1440
+ default:
1441
+ return "adjustment";
1442
+ }
1443
+ }
1444
+ /**
1445
+ * Get journal entries directly (for admin or debugging)
1446
+ * @param query - Journal entry query parameters
1447
+ * @returns List of journal entries
1448
+ */
1449
+ async getJournalEntries(query) {
1450
+ return this.repository.getJournalEntries(query);
1451
+ }
1452
+ /**
1453
+ * Get the underlying repository (for advanced use cases)
1454
+ */
1455
+ getRepository() {
1456
+ return this.repository;
1457
+ }
1458
+ };
1459
+ function createCreditsService(repository) {
1460
+ return new CreditsService(repository);
1461
+ }
1462
+
1463
+ // src/adapters/generic.ts
1464
+ function generateRequestId() {
1465
+ const timestamp = Date.now();
1466
+ const randomPart = Math.random().toString(36).substring(2, 14);
1467
+ return `req_${timestamp}_${randomPart}`;
1468
+ }
1469
+ function createGenericAdapter(config) {
1470
+ const { repository, authProvider, deferred, operationCosts } = config;
1471
+ function getOperationCost2(operationType) {
1472
+ const cost = operationCosts[operationType];
1473
+ if (cost === void 0) {
1474
+ throw new Error(
1475
+ `Unknown operation type: "${operationType}". Configured types: ${Object.keys(operationCosts).join(", ")}`
1476
+ );
1477
+ }
1478
+ return cost;
1479
+ }
1480
+ function logUsage(params) {
1481
+ deferred.defer(async () => {
1482
+ await repository.logUsage({
1483
+ userId: params.userId,
1484
+ operationType: params.operationType,
1485
+ provider: "gemini",
1486
+ creditsUsed: params.creditsUsed,
1487
+ success: params.success,
1488
+ errorMessage: params.errorMessage,
1489
+ resourceId: params.resourceId,
1490
+ resourceType: params.resourceType,
1491
+ requestId: params.requestId
1492
+ });
1493
+ });
1494
+ }
1495
+ return {
1496
+ withCredits(options, handler) {
1497
+ return async (data) => {
1498
+ const requestId = generateRequestId();
1499
+ const user = await authProvider.getCurrentUser();
1500
+ if (!user?.id) {
1501
+ return { success: false, error: "Authentication required" };
1502
+ }
1503
+ if (isPreviewMode(data)) {
1504
+ const dummyReservation = createDummyReservation(user.id, options.operationType);
1505
+ return handler(user, data, dummyReservation);
1506
+ }
1507
+ let reservation;
1508
+ try {
1509
+ const cost = options.customCost ?? getOperationCost2(options.operationType);
1510
+ reservation = await reserveCreditsForOperation(
1511
+ repository,
1512
+ user.id,
1513
+ cost,
1514
+ options.operationType
1515
+ );
1516
+ } catch (error) {
1517
+ const message = error instanceof Error ? error.message : "Failed to reserve credits";
1518
+ logUsage({
1519
+ userId: user.id,
1520
+ operationType: options.operationType,
1521
+ creditsUsed: 0,
1522
+ success: false,
1523
+ errorMessage: message,
1524
+ resourceId: options.resourceId,
1525
+ resourceType: options.resourceType,
1526
+ requestId
1527
+ });
1528
+ return { success: false, error: message };
1529
+ }
1530
+ try {
1531
+ const result = await handler(user, data, reservation);
1532
+ if (result.success) {
1533
+ await commitReservationWithJournal(repository, user.id, reservation.id);
1534
+ logUsage({
1535
+ userId: user.id,
1536
+ operationType: options.operationType,
1537
+ creditsUsed: reservation.amount,
1538
+ success: true,
1539
+ resourceId: options.resourceId,
1540
+ resourceType: options.resourceType,
1541
+ requestId
1542
+ });
1543
+ } else {
1544
+ await releaseReservationWithJournal(repository, user.id, reservation.id);
1545
+ logUsage({
1546
+ userId: user.id,
1547
+ operationType: options.operationType,
1548
+ creditsUsed: 0,
1549
+ success: false,
1550
+ errorMessage: result.error,
1551
+ resourceId: options.resourceId,
1552
+ resourceType: options.resourceType,
1553
+ requestId
1554
+ });
1555
+ }
1556
+ return result;
1557
+ } catch (error) {
1558
+ const message = error instanceof Error ? error.message : "An unexpected error occurred";
1559
+ await releaseReservationWithJournal(repository, user.id, reservation.id);
1560
+ logUsage({
1561
+ userId: user.id,
1562
+ operationType: options.operationType,
1563
+ creditsUsed: 0,
1564
+ success: false,
1565
+ errorMessage: message,
1566
+ resourceId: options.resourceId,
1567
+ resourceType: options.resourceType,
1568
+ requestId
1569
+ });
1570
+ console.error("Action error:", error);
1571
+ return { success: false, error: message };
1572
+ }
1573
+ };
1574
+ }
1575
+ };
1576
+ }
1577
+ function isPreviewMode(data) {
1578
+ return typeof data === "object" && data !== null && "preview" in data && data.preview === true;
1579
+ }
1580
+ function createDummyReservation(userId, operationType) {
1581
+ return {
1582
+ id: "preview-mode",
1583
+ userId,
1584
+ amount: 0,
1585
+ operationType,
1586
+ status: "released",
1587
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1588
+ expiresAt: (/* @__PURE__ */ new Date()).toISOString()
1589
+ };
1590
+ }
1591
+
1592
+ // src/notifications/index.ts
1593
+ var cooldownState = /* @__PURE__ */ new Map();
1594
+ function isNotificationAllowed(userId, eventType) {
1595
+ const state = cooldownState.get(userId);
1596
+ if (!state) return true;
1597
+ const now = Date.now();
1598
+ let lastNotification;
1599
+ switch (eventType) {
1600
+ case "lowBalance":
1601
+ lastNotification = state.lastLowBalanceNotification;
1602
+ break;
1603
+ case "depleted":
1604
+ lastNotification = state.lastDepletedNotification;
1605
+ break;
1606
+ case "expiring":
1607
+ lastNotification = state.lastExpiringNotification;
1608
+ break;
1609
+ }
1610
+ if (!lastNotification) return true;
1611
+ return now - lastNotification.getTime() > getConfig().lowBalanceNotificationCooldownHours * 60 * 60 * 1e3;
1612
+ }
1613
+ function recordNotification(userId, eventType) {
1614
+ const state = cooldownState.get(userId) || {};
1615
+ const now = /* @__PURE__ */ new Date();
1616
+ switch (eventType) {
1617
+ case "lowBalance":
1618
+ state.lastLowBalanceNotification = now;
1619
+ break;
1620
+ case "depleted":
1621
+ state.lastDepletedNotification = now;
1622
+ break;
1623
+ case "expiring":
1624
+ state.lastExpiringNotification = now;
1625
+ break;
1626
+ }
1627
+ cooldownState.set(userId, state);
1628
+ }
1629
+ var notificationHandlers = [];
1630
+ function registerNotificationHandler(handler) {
1631
+ notificationHandlers.push(handler);
1632
+ }
1633
+ function clearNotificationHandlers() {
1634
+ notificationHandlers.length = 0;
1635
+ }
1636
+ function clearCooldownState() {
1637
+ cooldownState.clear();
1638
+ }
1639
+ async function dispatchNotification(event) {
1640
+ if (notificationHandlers.length === 0) {
1641
+ console.debug(`[Credits] No notification handlers registered for: ${event.type}`);
1642
+ return;
1643
+ }
1644
+ await Promise.allSettled(
1645
+ notificationHandlers.map(
1646
+ (handler) => handler.handleNotification(event).catch((error) => {
1647
+ console.error(`[Credits] Notification handler error:`, error);
1648
+ })
1649
+ )
1650
+ );
1651
+ }
1652
+ async function checkAndNotifyLowBalance(userId, newBalance, threshold = getConfig().lowBalanceThreshold) {
1653
+ if (newBalance <= 0 && isNotificationAllowed(userId, "depleted")) {
1654
+ recordNotification(userId, "depleted");
1655
+ await dispatchNotification({
1656
+ type: "balance_depleted",
1657
+ userId
1658
+ });
1659
+ return;
1660
+ }
1661
+ if (newBalance > 0 && newBalance <= threshold && isNotificationAllowed(userId, "lowBalance")) {
1662
+ recordNotification(userId, "lowBalance");
1663
+ await dispatchNotification({
1664
+ type: "low_balance",
1665
+ userId,
1666
+ balance: newBalance,
1667
+ threshold
1668
+ });
1669
+ }
1670
+ }
1671
+ async function notifySubscriptionExpiring(userId, expiresAt, daysRemaining) {
1672
+ if (!isNotificationAllowed(userId, "expiring")) {
1673
+ return;
1674
+ }
1675
+ recordNotification(userId, "expiring");
1676
+ await dispatchNotification({
1677
+ type: "subscription_expiring",
1678
+ userId,
1679
+ expiresAt,
1680
+ daysRemaining
1681
+ });
1682
+ }
1683
+ async function notifySubscriptionExpired(userId, wasDowngraded) {
1684
+ await dispatchNotification({
1685
+ type: "subscription_expired",
1686
+ userId,
1687
+ wasDowngraded
1688
+ });
1689
+ }
1690
+ var ConsoleNotificationHandler = class {
1691
+ async handleNotification(event) {
1692
+ switch (event.type) {
1693
+ case "low_balance":
1694
+ console.log(
1695
+ `[Credits Notification] Low balance alert for user ${event.userId}: ${event.balance} credits remaining (threshold: ${event.threshold})`
1696
+ );
1697
+ break;
1698
+ case "balance_depleted":
1699
+ console.log(
1700
+ `[Credits Notification] Balance depleted for user ${event.userId}`
1701
+ );
1702
+ break;
1703
+ case "subscription_expiring":
1704
+ console.log(
1705
+ `[Credits Notification] Subscription expiring for user ${event.userId}: ${event.daysRemaining} days remaining`
1706
+ );
1707
+ break;
1708
+ case "subscription_expired":
1709
+ console.log(
1710
+ `[Credits Notification] Subscription expired for user ${event.userId}: downgraded=${event.wasDowngraded}`
1711
+ );
1712
+ break;
1713
+ }
1714
+ }
1715
+ };
1716
+
1717
+ // src/sdk/types.ts
1718
+ var SDKErrorCode = {
1719
+ NETWORK_ERROR: "NETWORK_ERROR",
1720
+ AUTHENTICATION_ERROR: "AUTHENTICATION_ERROR",
1721
+ AUTHORIZATION_ERROR: "AUTHORIZATION_ERROR",
1722
+ INSUFFICIENT_CREDITS: "INSUFFICIENT_CREDITS",
1723
+ RESERVATION_NOT_FOUND: "RESERVATION_NOT_FOUND",
1724
+ RESERVATION_EXPIRED: "RESERVATION_EXPIRED",
1725
+ VALIDATION_ERROR: "VALIDATION_ERROR",
1726
+ SERVER_ERROR: "SERVER_ERROR",
1727
+ UNKNOWN_ERROR: "UNKNOWN_ERROR"
1728
+ };
1729
+
1730
+ // src/sdk/errors.ts
1731
+ var CreditsSDKError = class extends Error {
1732
+ code;
1733
+ details;
1734
+ statusCode;
1735
+ constructor(message, code, options) {
1736
+ super(message, { cause: options?.cause });
1737
+ this.name = "CreditsSDKError";
1738
+ this.code = code;
1739
+ this.details = options?.details;
1740
+ this.statusCode = options?.statusCode;
1741
+ }
1742
+ /**
1743
+ * Check if this is a specific error type
1744
+ */
1745
+ is(code) {
1746
+ return this.code === code;
1747
+ }
1748
+ /**
1749
+ * Create a JSON representation for logging
1750
+ */
1751
+ toJSON() {
1752
+ return {
1753
+ name: this.name,
1754
+ message: this.message,
1755
+ code: this.code,
1756
+ details: this.details,
1757
+ statusCode: this.statusCode
1758
+ };
1759
+ }
1760
+ };
1761
+ var NetworkError = class extends CreditsSDKError {
1762
+ constructor(message, cause) {
1763
+ super(message, SDKErrorCode.NETWORK_ERROR, { cause });
1764
+ this.name = "NetworkError";
1765
+ }
1766
+ };
1767
+ var AuthenticationError = class extends CreditsSDKError {
1768
+ constructor(message = "Authentication required") {
1769
+ super(message, SDKErrorCode.AUTHENTICATION_ERROR, { statusCode: 401 });
1770
+ this.name = "AuthenticationError";
1771
+ }
1772
+ };
1773
+ var AuthorizationError = class extends CreditsSDKError {
1774
+ constructor(message = "Access denied") {
1775
+ super(message, SDKErrorCode.AUTHORIZATION_ERROR, { statusCode: 403 });
1776
+ this.name = "AuthorizationError";
1777
+ }
1778
+ };
1779
+ var InsufficientCreditsError = class extends CreditsSDKError {
1780
+ available;
1781
+ required;
1782
+ constructor(available, required) {
1783
+ super(
1784
+ `Insufficient credits: available ${available}, required ${required}`,
1785
+ SDKErrorCode.INSUFFICIENT_CREDITS,
1786
+ {
1787
+ statusCode: 402,
1788
+ details: { available, required }
1789
+ }
1790
+ );
1791
+ this.name = "InsufficientCreditsError";
1792
+ this.available = available;
1793
+ this.required = required;
1794
+ }
1795
+ };
1796
+ var ReservationNotFoundError = class extends CreditsSDKError {
1797
+ reservationId;
1798
+ constructor(reservationId) {
1799
+ super(
1800
+ `Reservation not found: ${reservationId}`,
1801
+ SDKErrorCode.RESERVATION_NOT_FOUND,
1802
+ {
1803
+ statusCode: 404,
1804
+ details: { reservationId }
1805
+ }
1806
+ );
1807
+ this.name = "ReservationNotFoundError";
1808
+ this.reservationId = reservationId;
1809
+ }
1810
+ };
1811
+ var ReservationExpiredError = class extends CreditsSDKError {
1812
+ reservationId;
1813
+ constructor(reservationId) {
1814
+ super(
1815
+ `Reservation expired: ${reservationId}`,
1816
+ SDKErrorCode.RESERVATION_EXPIRED,
1817
+ {
1818
+ statusCode: 410,
1819
+ details: { reservationId }
1820
+ }
1821
+ );
1822
+ this.name = "ReservationExpiredError";
1823
+ this.reservationId = reservationId;
1824
+ }
1825
+ };
1826
+ var ValidationError = class extends CreditsSDKError {
1827
+ field;
1828
+ constructor(message, field) {
1829
+ super(message, SDKErrorCode.VALIDATION_ERROR, {
1830
+ statusCode: 400,
1831
+ details: field ? { field } : void 0
1832
+ });
1833
+ this.name = "ValidationError";
1834
+ this.field = field;
1835
+ }
1836
+ };
1837
+ var ServerError = class extends CreditsSDKError {
1838
+ constructor(message = "Internal server error") {
1839
+ super(message, SDKErrorCode.SERVER_ERROR, { statusCode: 500 });
1840
+ this.name = "ServerError";
1841
+ }
1842
+ };
1843
+ function parseApiError(statusCode, body) {
1844
+ const errorCode = body.error?.code || "";
1845
+ const errorMessage = body.error?.message || "An error occurred";
1846
+ const details = body.error?.details;
1847
+ switch (statusCode) {
1848
+ case 401:
1849
+ return new AuthenticationError(errorMessage);
1850
+ case 403:
1851
+ return new AuthorizationError(errorMessage);
1852
+ case 402:
1853
+ if (details?.available !== void 0 && details?.required !== void 0) {
1854
+ return new InsufficientCreditsError(
1855
+ details.available,
1856
+ details.required
1857
+ );
1858
+ }
1859
+ return new CreditsSDKError(errorMessage, SDKErrorCode.INSUFFICIENT_CREDITS, {
1860
+ statusCode,
1861
+ details
1862
+ });
1863
+ case 404:
1864
+ if (errorCode === "RESERVATION_NOT_FOUND" && details?.reservationId) {
1865
+ return new ReservationNotFoundError(details.reservationId);
1866
+ }
1867
+ return new CreditsSDKError(errorMessage, SDKErrorCode.RESERVATION_NOT_FOUND, {
1868
+ statusCode,
1869
+ details
1870
+ });
1871
+ case 410:
1872
+ if (errorCode === "RESERVATION_EXPIRED" && details?.reservationId) {
1873
+ return new ReservationExpiredError(details.reservationId);
1874
+ }
1875
+ return new CreditsSDKError(errorMessage, SDKErrorCode.RESERVATION_EXPIRED, {
1876
+ statusCode,
1877
+ details
1878
+ });
1879
+ case 400:
1880
+ case 422:
1881
+ return new ValidationError(errorMessage, details?.field);
1882
+ case 500:
1883
+ default:
1884
+ if (statusCode >= 500) {
1885
+ return new ServerError(errorMessage);
1886
+ }
1887
+ return new CreditsSDKError(errorMessage, SDKErrorCode.UNKNOWN_ERROR, {
1888
+ statusCode,
1889
+ details
1890
+ });
1891
+ }
1892
+ }
1893
+
1894
+ // src/sdk/client.ts
1895
+ var CreditsClient = class {
1896
+ baseUrl;
1897
+ getAuthToken;
1898
+ fetchFn;
1899
+ constructor(config) {
1900
+ this.baseUrl = config.baseUrl.replace(/\/$/, "");
1901
+ this.getAuthToken = config.getAuthToken;
1902
+ this.fetchFn = config.fetch ?? fetch;
1903
+ }
1904
+ /**
1905
+ * Make an authenticated request to the API
1906
+ */
1907
+ async request(path, options = {}) {
1908
+ const { method = "GET", body, params } = options;
1909
+ let url = `${this.baseUrl}${path}`;
1910
+ if (params) {
1911
+ const searchParams = new URLSearchParams();
1912
+ for (const [key, value] of Object.entries(params)) {
1913
+ if (value !== void 0) {
1914
+ searchParams.set(key, String(value));
1915
+ }
1916
+ }
1917
+ const queryString = searchParams.toString();
1918
+ if (queryString) {
1919
+ url += `?${queryString}`;
1920
+ }
1921
+ }
1922
+ const headers = {
1923
+ "Content-Type": "application/json"
1924
+ };
1925
+ if (this.getAuthToken) {
1926
+ try {
1927
+ const token = await this.getAuthToken();
1928
+ if (token) {
1929
+ headers["Authorization"] = `Bearer ${token}`;
1930
+ }
1931
+ } catch {
1932
+ }
1933
+ }
1934
+ let response;
1935
+ try {
1936
+ response = await this.fetchFn(url, {
1937
+ method,
1938
+ headers,
1939
+ body: body ? JSON.stringify(body) : void 0
1940
+ });
1941
+ } catch (error) {
1942
+ throw new NetworkError(
1943
+ error instanceof Error ? error.message : "Network request failed",
1944
+ error instanceof Error ? error : void 0
1945
+ );
1946
+ }
1947
+ const responseBody = await response.json();
1948
+ if (!response.ok) {
1949
+ throw parseApiError(response.status, responseBody);
1950
+ }
1951
+ return responseBody.data;
1952
+ }
1953
+ /**
1954
+ * Get the current user's credit balance
1955
+ *
1956
+ * @returns User's credit information
1957
+ * @throws AuthenticationError if not authenticated
1958
+ */
1959
+ async getBalance() {
1960
+ return this.request("/api/v1/credits/balance");
1961
+ }
1962
+ /**
1963
+ * Check if the user has sufficient credits for an operation
1964
+ *
1965
+ * @param operationType - Type of operation to check
1966
+ * @param customCost - Optional custom cost to check against
1967
+ * @returns Credit availability status
1968
+ * @throws AuthenticationError if not authenticated
1969
+ */
1970
+ async checkAvailability(operationType, customCost) {
1971
+ return this.request("/api/v1/credits/availability", {
1972
+ params: {
1973
+ operationType,
1974
+ customCost: customCost?.toString()
1975
+ }
1976
+ });
1977
+ }
1978
+ /**
1979
+ * Reserve credits for an operation
1980
+ *
1981
+ * This is the first step in the two-phase commit pattern.
1982
+ * Reserved credits are held for 5 minutes before expiring.
1983
+ *
1984
+ * @param operationType - Type of operation
1985
+ * @param options - Optional reservation options
1986
+ * @returns Reservation details including ID and expiry
1987
+ * @throws InsufficientCreditsError if not enough credits
1988
+ * @throws AuthenticationError if not authenticated
1989
+ */
1990
+ async reserve(operationType, options) {
1991
+ return this.request("/api/v1/credits/reserve", {
1992
+ method: "POST",
1993
+ body: {
1994
+ operationType,
1995
+ ...options
1996
+ }
1997
+ });
1998
+ }
1999
+ /**
2000
+ * Commit a reservation (deduct credits)
2001
+ *
2002
+ * This is the second step in the two-phase commit pattern.
2003
+ * Call this after your operation succeeds.
2004
+ *
2005
+ * @param reservationId - ID of the reservation to commit
2006
+ * @throws ReservationNotFoundError if reservation doesn't exist
2007
+ * @throws ReservationExpiredError if reservation has expired
2008
+ * @throws AuthenticationError if not authenticated
2009
+ */
2010
+ async commit(reservationId) {
2011
+ await this.request("/api/v1/credits/commit", {
2012
+ method: "POST",
2013
+ body: { reservationId }
2014
+ });
2015
+ }
2016
+ /**
2017
+ * Release a reservation (refund credits)
2018
+ *
2019
+ * Call this if your operation fails after reserving credits.
2020
+ *
2021
+ * @param reservationId - ID of the reservation to release
2022
+ * @throws ReservationNotFoundError if reservation doesn't exist
2023
+ * @throws AuthenticationError if not authenticated
2024
+ */
2025
+ async release(reservationId) {
2026
+ await this.request("/api/v1/credits/release", {
2027
+ method: "POST",
2028
+ body: { reservationId }
2029
+ });
2030
+ }
2031
+ /**
2032
+ * Get usage history for the current user
2033
+ *
2034
+ * @param options - Optional pagination options
2035
+ * @returns Paginated list of usage entries
2036
+ * @throws AuthenticationError if not authenticated
2037
+ */
2038
+ async getHistory(options) {
2039
+ return this.request("/api/v1/credits/history", {
2040
+ params: {
2041
+ page: options?.page,
2042
+ limit: options?.limit
2043
+ }
2044
+ });
2045
+ }
2046
+ /**
2047
+ * Get the available credits (balance + bonus - reserved)
2048
+ *
2049
+ * Convenience method that calculates available credits from balance.
2050
+ *
2051
+ * @returns Number of available credits
2052
+ * @throws AuthenticationError if not authenticated
2053
+ */
2054
+ async getAvailableCredits() {
2055
+ const balance = await this.getBalance();
2056
+ return balance.balance + balance.bonusCredits - balance.reserved;
2057
+ }
2058
+ /**
2059
+ * Execute an operation with automatic credit handling
2060
+ *
2061
+ * This is a convenience method that handles the full two-phase commit
2062
+ * pattern automatically.
2063
+ *
2064
+ * @param operationType - Type of operation
2065
+ * @param operation - The operation to execute
2066
+ * @param options - Optional reservation options
2067
+ * @returns Result of the operation
2068
+ * @throws InsufficientCreditsError if not enough credits
2069
+ * @throws AuthenticationError if not authenticated
2070
+ *
2071
+ * @example
2072
+ * ```typescript
2073
+ * const result = await client.withCredits(
2074
+ * "story_generation",
2075
+ * async () => {
2076
+ * return generateStory(data);
2077
+ * }
2078
+ * );
2079
+ * ```
2080
+ */
2081
+ async withCredits(operationType, operation, options) {
2082
+ const reservation = await this.reserve(operationType, options);
2083
+ try {
2084
+ const result = await operation();
2085
+ await this.commit(reservation.reservationId);
2086
+ return result;
2087
+ } catch (error) {
2088
+ try {
2089
+ await this.release(reservation.reservationId);
2090
+ } catch {
2091
+ }
2092
+ throw error;
2093
+ }
2094
+ }
2095
+ };
2096
+
2097
+ // src/sdk/admin-client.ts
2098
+ var AdminCreditsClient = class {
2099
+ baseUrl;
2100
+ apiKey;
2101
+ fetchFn;
2102
+ constructor(config) {
2103
+ this.baseUrl = config.baseUrl.replace(/\/$/, "");
2104
+ this.apiKey = config.apiKey;
2105
+ this.fetchFn = config.fetch ?? fetch;
2106
+ }
2107
+ /**
2108
+ * Make an authenticated admin request to the API
2109
+ */
2110
+ async request(path, options = {}) {
2111
+ const { method = "GET", body, params } = options;
2112
+ let url = `${this.baseUrl}${path}`;
2113
+ if (params) {
2114
+ const searchParams = new URLSearchParams();
2115
+ for (const [key, value] of Object.entries(params)) {
2116
+ if (value !== void 0) {
2117
+ searchParams.set(key, String(value));
2118
+ }
2119
+ }
2120
+ const queryString = searchParams.toString();
2121
+ if (queryString) {
2122
+ url += `?${queryString}`;
2123
+ }
2124
+ }
2125
+ const headers = {
2126
+ "Content-Type": "application/json",
2127
+ "x-api-key": this.apiKey
2128
+ };
2129
+ let response;
2130
+ try {
2131
+ response = await this.fetchFn(url, {
2132
+ method,
2133
+ headers,
2134
+ body: body ? JSON.stringify(body) : void 0
2135
+ });
2136
+ } catch (error) {
2137
+ throw new NetworkError(
2138
+ error instanceof Error ? error.message : "Network request failed",
2139
+ error instanceof Error ? error : void 0
2140
+ );
2141
+ }
2142
+ const responseBody = await response.json();
2143
+ if (!response.ok) {
2144
+ throw parseApiError(response.status, responseBody);
2145
+ }
2146
+ return responseBody.data;
2147
+ }
2148
+ /**
2149
+ * Get a specific user's credits
2150
+ *
2151
+ * @param userId - ID of the user
2152
+ * @returns User's credit information
2153
+ * @throws AuthorizationError if API key is invalid
2154
+ */
2155
+ async getUserCredits(userId) {
2156
+ return this.request(`/api/admin/credits/users/${userId}`);
2157
+ }
2158
+ /**
2159
+ * Add credits to a user's account
2160
+ *
2161
+ * Credits are added to bonusCredits, which don't expire on monthly reset.
2162
+ *
2163
+ * @param userId - ID of the user
2164
+ * @param amount - Amount of credits to add
2165
+ * @param description - Reason for adding credits
2166
+ * @throws AuthorizationError if API key is invalid
2167
+ */
2168
+ async addCredits(userId, amount, description) {
2169
+ await this.request(
2170
+ `/api/admin/credits/users/${userId}/add`,
2171
+ {
2172
+ method: "POST",
2173
+ body: { amount, description }
2174
+ }
2175
+ );
2176
+ }
2177
+ /**
2178
+ * Update a user's subscription tier
2179
+ *
2180
+ * @param userId - ID of the user
2181
+ * @param tier - New subscription tier
2182
+ * @param expiresAt - Optional expiration date (ISO 8601)
2183
+ * @throws AuthorizationError if API key is invalid
2184
+ */
2185
+ async updateSubscription(userId, tier, expiresAt) {
2186
+ const body = { tier };
2187
+ if (expiresAt) {
2188
+ body.expiresAt = expiresAt;
2189
+ }
2190
+ await this.request(
2191
+ `/api/admin/credits/users/${userId}/subscription`,
2192
+ {
2193
+ method: "PUT",
2194
+ body
2195
+ }
2196
+ );
2197
+ }
2198
+ /**
2199
+ * Get the current credits configuration
2200
+ *
2201
+ * @returns Credits system configuration
2202
+ * @throws AuthorizationError if API key is invalid
2203
+ */
2204
+ async getConfig() {
2205
+ return this.request("/api/admin/credits/config");
2206
+ }
2207
+ /**
2208
+ * Update operation costs
2209
+ *
2210
+ * @param costs - New operation costs (operation type -> cost)
2211
+ * @throws AuthorizationError if API key is invalid
2212
+ */
2213
+ async updateCosts(costs) {
2214
+ await this.request("/api/admin/credits/config/costs", {
2215
+ method: "PUT",
2216
+ body: { operationCosts: costs }
2217
+ });
2218
+ }
2219
+ /**
2220
+ * List all users with their credits
2221
+ *
2222
+ * @param options - Pagination and filtering options
2223
+ * @returns Paginated list of users
2224
+ * @throws AuthorizationError if API key is invalid
2225
+ */
2226
+ async listUsers(options) {
2227
+ return this.request("/api/admin/credits/users", {
2228
+ params: {
2229
+ page: options?.page,
2230
+ limit: options?.limit,
2231
+ tier: options?.tier,
2232
+ search: options?.search
2233
+ }
2234
+ });
2235
+ }
2236
+ /**
2237
+ * Deduct credits from a user's account
2238
+ *
2239
+ * @param userId - ID of the user
2240
+ * @param amount - Amount of credits to deduct
2241
+ * @param description - Reason for deducting credits
2242
+ * @throws AuthorizationError if API key is invalid
2243
+ */
2244
+ async deductCredits(userId, amount, description) {
2245
+ await this.request(
2246
+ `/api/admin/credits/users/${userId}/deduct`,
2247
+ {
2248
+ method: "POST",
2249
+ body: { amount, description }
2250
+ }
2251
+ );
2252
+ }
2253
+ /**
2254
+ * Get usage statistics for a user
2255
+ *
2256
+ * @param userId - ID of the user
2257
+ * @param options - Optional date range
2258
+ * @returns Usage statistics
2259
+ * @throws AuthorizationError if API key is invalid
2260
+ */
2261
+ async getUserStats(userId, options) {
2262
+ return this.request(`/api/admin/credits/users/${userId}/stats`, {
2263
+ params: {
2264
+ startDate: options?.startDate,
2265
+ endDate: options?.endDate
2266
+ }
2267
+ });
2268
+ }
2269
+ };
2270
+
2271
+ // src/utils/error-utils.ts
2272
+ function parseCreditError(error) {
2273
+ const match = error.match(/Available:\s*(\d+)\s*,\s*Required:\s*(\d+)/i);
2274
+ if (!match) {
2275
+ return null;
2276
+ }
2277
+ const available = parseInt(match[1], 10);
2278
+ const required = parseInt(match[2], 10);
2279
+ if (isNaN(available) || isNaN(required)) {
2280
+ return null;
2281
+ }
2282
+ return {
2283
+ available,
2284
+ required,
2285
+ shortfall: required - available,
2286
+ rawMessage: error
2287
+ };
2288
+ }
2289
+ function isCreditErrorMessage(error) {
2290
+ const lowerError = error.toLowerCase();
2291
+ return lowerError.includes("insufficient credits") || lowerError.includes("not enough credits");
2292
+ }
2293
+
2294
+ // src/utils/index.ts
2295
+ function getTotalCredits(credits) {
2296
+ return credits.balance + credits.bonusCredits;
2297
+ }
2298
+ function generateRequestId2() {
2299
+ const timestamp = Date.now();
2300
+ const randomPart = Math.random().toString(36).substring(2, 14);
2301
+ return `req_${timestamp}_${randomPart}`;
2302
+ }
2303
+ function isPreviewMode2(data) {
2304
+ return typeof data === "object" && data !== null && "preview" in data && data.preview === true;
2305
+ }
2306
+ function createDummyReservation2(userId, operationType) {
2307
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2308
+ return {
2309
+ id: "preview-mode",
2310
+ userId,
2311
+ amount: 0,
2312
+ operationType,
2313
+ status: "released",
2314
+ createdAt: now,
2315
+ expiresAt: now
2316
+ };
2317
+ }
2318
+ export {
2319
+ AdminCreditsClient,
2320
+ ApiKeyCreditsProvider,
2321
+ AuthenticationError,
2322
+ AuthorizationError,
2323
+ ConsoleNotificationHandler,
2324
+ CreditError,
2325
+ CreditErrorCode,
2326
+ CreditsClient,
2327
+ CreditsSDKError,
2328
+ CreditsService,
2329
+ InMemoryCreditRepository,
2330
+ NetworkError,
2331
+ SDKErrorCode,
2332
+ InsufficientCreditsError as SDKInsufficientCreditsError,
2333
+ ReservationExpiredError as SDKReservationExpiredError,
2334
+ ReservationNotFoundError as SDKReservationNotFoundError,
2335
+ ServerError,
2336
+ ValidationError,
2337
+ calculateAvailableCredits,
2338
+ checkAndNotifyLowBalance,
2339
+ clearConfigProvider,
2340
+ clearCooldownState,
2341
+ clearNotificationHandlers,
2342
+ commitReservationWithJournal,
2343
+ createApiKeyProvider,
2344
+ createCreditsService,
2345
+ createDeferredExecutor,
2346
+ createDummyReservation2 as createDummyReservation,
2347
+ createGenericAdapter,
2348
+ createInMemoryCreditRepository,
2349
+ createInsufficientCreditsError,
2350
+ createInvalidOperationTypeError,
2351
+ createReservationAlreadyProcessedError,
2352
+ createReservationExpiredError,
2353
+ createReservationNotFoundError,
2354
+ createUserNotFoundError,
2355
+ generateId,
2356
+ generateRequestId2 as generateRequestId,
2357
+ genericDeferred,
2358
+ getConfig,
2359
+ getConfigMonthlyLimit,
2360
+ getConfigOperationCost,
2361
+ getConfigProvider,
2362
+ getConfigTierConfig,
2363
+ getDefaultFreeCredits,
2364
+ getMonthlyLimit,
2365
+ getNextMonthlyReset,
2366
+ getOperationCost,
2367
+ getOperationCosts,
2368
+ getOperationCostsWithLabels,
2369
+ getOperationLabel,
2370
+ getOperationLabels,
2371
+ getReservationExpiryMs,
2372
+ getTierConfig,
2373
+ getTotalCredits,
2374
+ getValidOperationTypes,
2375
+ initializeConfig,
2376
+ isCreditError,
2377
+ isCreditErrorMessage,
2378
+ isFeatureEnabled,
2379
+ isInsufficientCreditsError,
2380
+ isPreviewMode2 as isPreviewMode,
2381
+ isValidOperationType,
2382
+ loadConfigFromEnv,
2383
+ noopDeferred,
2384
+ notifySubscriptionExpired,
2385
+ notifySubscriptionExpiring,
2386
+ parseApiError,
2387
+ parseCreditError,
2388
+ registerConfigProvider,
2389
+ registerNotificationHandler,
2390
+ releaseReservationWithJournal,
2391
+ reserveCreditsForOperation,
2392
+ resetConfig,
2393
+ synchronousDeferred,
2394
+ toClientUserCredits,
2395
+ toDate,
2396
+ toPortableTimestamp,
2397
+ verifyAdminApiKey,
2398
+ verifyBearerToken
2399
+ };
64
2400
  //# sourceMappingURL=index.js.map