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