@nehorai/credits 1.2.0 → 1.4.0

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