@nehorai/credits 1.4.0 → 1.6.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 (38) hide show
  1. package/dist/adapters/index.cjs +13 -3
  2. package/dist/adapters/index.cjs.map +1 -1
  3. package/dist/adapters/index.d.cts +2 -2
  4. package/dist/adapters/index.d.ts +2 -2
  5. package/dist/adapters/index.js +13 -3
  6. package/dist/adapters/index.js.map +1 -1
  7. package/dist/core/index.cjs +13 -3
  8. package/dist/core/index.cjs.map +1 -1
  9. package/dist/core/index.d.cts +3 -3
  10. package/dist/core/index.d.ts +3 -3
  11. package/dist/core/index.js +13 -3
  12. package/dist/core/index.js.map +1 -1
  13. package/dist/index.cjs +155 -17
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +75 -16
  16. package/dist/index.d.ts +75 -16
  17. package/dist/index.js +146 -17
  18. package/dist/index.js.map +1 -1
  19. package/dist/repository/index.cjs +47 -11
  20. package/dist/repository/index.cjs.map +1 -1
  21. package/dist/repository/index.d.cts +3 -3
  22. package/dist/repository/index.d.ts +3 -3
  23. package/dist/repository/index.js +47 -11
  24. package/dist/repository/index.js.map +1 -1
  25. package/dist/sdk/index.d.cts +2 -2
  26. package/dist/sdk/index.d.ts +2 -2
  27. package/dist/service/index.cjs +130 -10
  28. package/dist/service/index.cjs.map +1 -1
  29. package/dist/service/index.d.cts +43 -3
  30. package/dist/service/index.d.ts +43 -3
  31. package/dist/service/index.js +130 -10
  32. package/dist/service/index.js.map +1 -1
  33. package/dist/{types-D1vdeBcA.d.cts → types-CZNv1U3e.d.cts} +1 -1
  34. package/dist/{types-BBxIWHK2.d.cts → types-CjLj3GW5.d.cts} +37 -3
  35. package/dist/{types-BBxIWHK2.d.ts → types-CjLj3GW5.d.ts} +37 -3
  36. package/dist/{types-aJsyv8df.d.ts → types-G4WK0rM-.d.ts} +1 -1
  37. package/package.json +10 -9
  38. package/LICENSE +0 -21
package/dist/index.cjs CHANGED
@@ -37,6 +37,7 @@ __export(src_exports, {
37
37
  SDKReservationExpiredError: () => ReservationExpiredError,
38
38
  SDKReservationNotFoundError: () => ReservationNotFoundError,
39
39
  ServerError: () => ServerError,
40
+ UNLIMITED_BALANCE_SENTINEL: () => UNLIMITED_BALANCE_SENTINEL,
40
41
  ValidationError: () => ValidationError,
41
42
  calculateAvailableCredits: () => calculateAvailableCredits,
42
43
  checkAndNotifyLowBalance: () => checkAndNotifyLowBalance,
@@ -65,6 +66,7 @@ __export(src_exports, {
65
66
  getConfigProvider: () => getConfigProvider,
66
67
  getConfigTierConfig: () => getConfigTierConfig,
67
68
  getDefaultFreeCredits: () => getDefaultFreeCredits,
69
+ getDefaultTier: () => getDefaultTier,
68
70
  getMonthlyLimit: () => getMonthlyLimit,
69
71
  getNextMonthlyReset: () => getNextMonthlyReset,
70
72
  getOperationCost: () => getOperationCost,
@@ -75,26 +77,33 @@ __export(src_exports, {
75
77
  getReservationExpiryMs: () => getReservationExpiryMs,
76
78
  getTierConfig: () => getTierConfig,
77
79
  getTotalCredits: () => getTotalCredits,
80
+ getUnlimitedSentinelBalance: () => getUnlimitedSentinelBalance,
78
81
  getValidOperationTypes: () => getValidOperationTypes,
82
+ getValidTiers: () => getValidTiers,
79
83
  initializeConfig: () => initializeConfig,
80
84
  isCreditError: () => isCreditError,
81
85
  isCreditErrorMessage: () => isCreditErrorMessage,
82
86
  isFeatureEnabled: () => isFeatureEnabled,
87
+ isFreeTier: () => isFreeTier,
83
88
  isInsufficientCreditsError: () => isInsufficientCreditsError,
84
89
  isPreviewMode: () => isPreviewMode2,
90
+ isUnlimitedTier: () => isUnlimitedTier,
85
91
  isValidOperationType: () => isValidOperationType,
92
+ isValidTier: () => isValidTier,
86
93
  loadConfigFromEnv: () => loadConfigFromEnv,
87
94
  noopDeferred: () => noopDeferred,
88
95
  notifySubscriptionExpired: () => notifySubscriptionExpired,
89
96
  notifySubscriptionExpiring: () => notifySubscriptionExpiring,
90
97
  parseApiError: () => parseApiError,
91
98
  parseCreditError: () => parseCreditError,
99
+ parseTier: () => parseTier,
92
100
  registerConfigProvider: () => registerConfigProvider,
93
101
  registerNotificationHandler: () => registerNotificationHandler,
94
102
  releaseReservationWithJournal: () => releaseReservationWithJournal,
95
103
  reserveCreditsForOperation: () => reserveCreditsForOperation,
96
104
  resetConfig: () => resetConfig,
97
105
  synchronousDeferred: () => synchronousDeferred,
106
+ tierSchema: () => tierSchema,
98
107
  toClientUserCredits: () => toClientUserCredits,
99
108
  toDate: () => toDate,
100
109
  toPortableTimestamp: () => toPortableTimestamp,
@@ -270,10 +279,13 @@ function clearConfigProvider() {
270
279
 
271
280
  // src/config/index.ts
272
281
  var tierConfigSchema = import_zod.z.object({
273
- tier: import_zod.z.enum(["free", "basic", "premium", "unlimited"]),
282
+ tier: import_zod.z.string().min(1),
274
283
  monthlyCredits: import_zod.z.number().min(0),
275
284
  priceUsd: import_zod.z.number().min(0),
276
- features: import_zod.z.array(import_zod.z.string())
285
+ features: import_zod.z.array(import_zod.z.string()),
286
+ isFree: import_zod.z.boolean().optional(),
287
+ unlimited: import_zod.z.boolean().optional(),
288
+ isDefault: import_zod.z.boolean().optional()
277
289
  });
278
290
  var creditSystemConfigSchema = import_zod.z.object({
279
291
  /**
@@ -297,7 +309,7 @@ var creditSystemConfigSchema = import_zod.z.object({
297
309
  * Tier configurations
298
310
  */
299
311
  tierConfigs: import_zod.z.record(
300
- import_zod.z.enum(["free", "basic", "premium", "unlimited"]),
312
+ import_zod.z.string().min(1),
301
313
  tierConfigSchema
302
314
  ),
303
315
  /**
@@ -353,6 +365,8 @@ var DEFAULT_CONFIG = {
353
365
  tier: "free",
354
366
  monthlyCredits: 25,
355
367
  priceUsd: 0,
368
+ isFree: true,
369
+ isDefault: true,
356
370
  features: [
357
371
  "25 credits per month",
358
372
  "Basic story generation",
@@ -386,6 +400,7 @@ var DEFAULT_CONFIG = {
386
400
  tier: "unlimited",
387
401
  monthlyCredits: 0,
388
402
  // 0 = unlimited
403
+ unlimited: true,
389
404
  priceUsd: 49.99,
390
405
  features: [
391
406
  "Unlimited credits",
@@ -513,12 +528,46 @@ function getConfigOperationCost(operationType) {
513
528
  }
514
529
  return cost;
515
530
  }
531
+ function getValidTiers() {
532
+ return Object.keys(getConfig().tierConfigs);
533
+ }
534
+ function isValidTier(tier) {
535
+ return tier in getConfig().tierConfigs;
536
+ }
537
+ function isFreeTier(tier) {
538
+ const c = getConfig().tierConfigs[tier];
539
+ if (!c) return false;
540
+ return c.isFree ?? c.priceUsd === 0;
541
+ }
542
+ function isUnlimitedTier(tier) {
543
+ const c = getConfig().tierConfigs[tier];
544
+ if (!c) return false;
545
+ return c.unlimited ?? c.monthlyCredits === 0;
546
+ }
547
+ function getDefaultTier() {
548
+ const entries = Object.entries(getConfig().tierConfigs);
549
+ const def = entries.find(([, c]) => c.isDefault)?.[0] ?? entries.find(([, c]) => c.isFree ?? c.priceUsd === 0)?.[0];
550
+ return def ?? "free";
551
+ }
552
+ var UNLIMITED_BALANCE_SENTINEL = 999999;
553
+ function getUnlimitedSentinelBalance() {
554
+ return UNLIMITED_BALANCE_SENTINEL;
555
+ }
516
556
  function getConfigTierConfig(tier) {
517
- return getConfig().tierConfigs[tier];
557
+ const cfg = getConfig().tierConfigs[tier];
558
+ if (cfg) return cfg;
559
+ const fallback = getDefaultTier();
560
+ console.warn(
561
+ `[Credits Config] Unknown tier "${tier}". Falling back to "${fallback}". Valid: ${getValidTiers().join(", ")}`
562
+ );
563
+ return getConfig().tierConfigs[fallback];
518
564
  }
519
565
  function getConfigMonthlyLimit(tier) {
520
- const config = getConfig().tierConfigs[tier];
521
- return config.monthlyCredits === 0 ? Infinity : config.monthlyCredits;
566
+ return isUnlimitedTier(tier) ? Infinity : getConfigTierConfig(tier).monthlyCredits;
567
+ }
568
+ var tierSchema = import_zod.z.string().refine(isValidTier, { message: "Unknown subscription tier" });
569
+ function parseTier(value) {
570
+ return tierSchema.parse(value);
522
571
  }
523
572
  function isFeatureEnabled(feature) {
524
573
  return getConfig().features[feature];
@@ -1093,7 +1142,7 @@ var InMemoryCreditRepository = class {
1093
1142
  if (!credits) {
1094
1143
  throw new Error(`User ${userId} not found`);
1095
1144
  }
1096
- if (credits.tier === "free" || !credits.subscriptionExpiresAt) {
1145
+ if (isFreeTier(credits.tier) || !credits.subscriptionExpiresAt) {
1097
1146
  return {
1098
1147
  wasDowngraded: false,
1099
1148
  inGracePeriod: false,
@@ -1120,11 +1169,11 @@ var InMemoryCreditRepository = class {
1120
1169
  credits
1121
1170
  };
1122
1171
  }
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);
1172
+ const defaultTier = getDefaultTier();
1173
+ const defaultTierConfig = getConfigTierConfig(defaultTier);
1174
+ credits.tier = defaultTier;
1175
+ credits.monthlyLimit = defaultTierConfig.monthlyCredits;
1176
+ credits.balance = Math.min(credits.balance, defaultTierConfig.monthlyCredits);
1128
1177
  credits.subscriptionExpiresAt = null;
1129
1178
  credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1130
1179
  this.users.set(userId, credits);
@@ -1264,7 +1313,7 @@ var CreditsService = class {
1264
1313
  if (!data) {
1265
1314
  return null;
1266
1315
  }
1267
- if (data.tier !== "free" && data.subscriptionExpiresAt) {
1316
+ if (!isFreeTier(data.tier) && data.subscriptionExpiresAt) {
1268
1317
  const expiryResult = await this.repository.checkAndHandleSubscriptionExpiry(
1269
1318
  userId,
1270
1319
  getConfig().subscriptionGracePeriodDays
@@ -1333,7 +1382,7 @@ var CreditsService = class {
1333
1382
  async initializeUserCredits(userId) {
1334
1383
  const credits = await this.repository.initializeUserCredits(
1335
1384
  userId,
1336
- "free",
1385
+ getDefaultTier(),
1337
1386
  getConfig().defaultFreeCredits
1338
1387
  );
1339
1388
  return toClientUserCredits(credits);
@@ -1465,6 +1514,83 @@ var CreditsService = class {
1465
1514
  async addCredits(userId, amount, description, paymentRef) {
1466
1515
  return this.repository.addCreditsAtomic(userId, amount, description, paymentRef);
1467
1516
  }
1517
+ /**
1518
+ * One-shot atomic "deduct-if-sufficient" credit charge.
1519
+ *
1520
+ * Unlike the reserve/commit/release two-phase flow, this performs a single
1521
+ * atomic check-and-deduct against the repository — suited for synchronous,
1522
+ * in-request operations that don't need a hold across an async job. Exactly
1523
+ * one call is made to `repository.deductCreditsAtomic`, which enforces
1524
+ * atomicity so concurrent callers can never both succeed against the same
1525
+ * limited balance.
1526
+ *
1527
+ * On success, creates a journal entry (same audit trail as `commitCredits`)
1528
+ * and, if `options.operationType` is provided, a usage log entry via the
1529
+ * same `logUsage` path.
1530
+ *
1531
+ * @param userId - User ID
1532
+ * @param amount - Credits to deduct (must be > 0)
1533
+ * @param options - Optional usage-log / journal metadata
1534
+ * @returns Typed result: `{ success: true, newBalance }` or
1535
+ * `{ success: false, reason: 'insufficient', available, required, shortfall }`
1536
+ * @throws Error if amount is not a positive number
1537
+ */
1538
+ async deductCredits(userId, amount, options) {
1539
+ if (!(amount > 0)) {
1540
+ throw new Error(`deductCredits amount must be positive (got ${amount})`);
1541
+ }
1542
+ let deduction;
1543
+ try {
1544
+ deduction = await this.repository.deductCreditsAtomic(userId, amount);
1545
+ } catch (error) {
1546
+ const credits = await this.repository.getUserCredits(userId);
1547
+ const available = credits ? credits.balance + credits.bonusCredits - credits.reserved : 0;
1548
+ if (available >= amount) {
1549
+ throw error;
1550
+ }
1551
+ return {
1552
+ success: false,
1553
+ reason: "insufficient",
1554
+ available,
1555
+ required: amount,
1556
+ shortfall: amount - available
1557
+ };
1558
+ }
1559
+ const operationType = options?.operationType;
1560
+ await this.repository.createJournalEntry({
1561
+ userId,
1562
+ entryType: "debit",
1563
+ amount,
1564
+ balanceAfter: deduction.newBalance,
1565
+ source: "operation_commit",
1566
+ referenceId: options?.requestId ?? `deduct-${Date.now()}`,
1567
+ referenceType: "adjustment",
1568
+ description: operationType ? `Deducted ${amount} credits for ${getOperationLabel(operationType)}` : `Deducted ${amount} credits`,
1569
+ metadata: {
1570
+ ...operationType && { operationType },
1571
+ ...options?.metadata
1572
+ }
1573
+ });
1574
+ if (operationType) {
1575
+ await this.logUsage({
1576
+ userId,
1577
+ operationType,
1578
+ provider: "gemini",
1579
+ creditsUsed: amount,
1580
+ success: true,
1581
+ resourceId: options?.resourceId,
1582
+ resourceType: options?.resourceType,
1583
+ requestId: options?.requestId,
1584
+ metadata: options?.metadata
1585
+ });
1586
+ }
1587
+ if (this.lowBalanceCallback) {
1588
+ this.lowBalanceCallback(userId, deduction.newBalance).catch((error) => {
1589
+ console.error("[Credits] Failed to send low balance notification:", error);
1590
+ });
1591
+ }
1592
+ return { success: true, newBalance: deduction.newBalance };
1593
+ }
1468
1594
  /**
1469
1595
  * Update user subscription tier
1470
1596
  * @param userId - User ID
@@ -1473,12 +1599,15 @@ var CreditsService = class {
1473
1599
  */
1474
1600
  async updateTier(userId, tier, expiresAt) {
1475
1601
  const monthlyLimit = getConfigMonthlyLimit(tier);
1602
+ const unlimited = isUnlimitedTier(tier);
1603
+ const free = isFreeTier(tier);
1476
1604
  await this.repository.updateUserTier(userId, {
1477
1605
  tier,
1478
- monthlyLimit: monthlyLimit === Infinity ? 0 : monthlyLimit,
1606
+ // Stored monthlyLimit uses the 0-means-unlimited convention.
1607
+ monthlyLimit: unlimited ? 0 : monthlyLimit,
1479
1608
  // 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,
1609
+ balance: !free ? unlimited ? getUnlimitedSentinelBalance() : monthlyLimit : void 0,
1610
+ monthlyUsed: !free ? 0 : void 0,
1482
1611
  subscriptionExpiresAt: expiresAt ? expiresAt.toISOString() : null
1483
1612
  });
1484
1613
  }
@@ -2447,6 +2576,7 @@ function createDummyReservation2(userId, operationType) {
2447
2576
  SDKReservationExpiredError,
2448
2577
  SDKReservationNotFoundError,
2449
2578
  ServerError,
2579
+ UNLIMITED_BALANCE_SENTINEL,
2450
2580
  ValidationError,
2451
2581
  calculateAvailableCredits,
2452
2582
  checkAndNotifyLowBalance,
@@ -2475,6 +2605,7 @@ function createDummyReservation2(userId, operationType) {
2475
2605
  getConfigProvider,
2476
2606
  getConfigTierConfig,
2477
2607
  getDefaultFreeCredits,
2608
+ getDefaultTier,
2478
2609
  getMonthlyLimit,
2479
2610
  getNextMonthlyReset,
2480
2611
  getOperationCost,
@@ -2485,26 +2616,33 @@ function createDummyReservation2(userId, operationType) {
2485
2616
  getReservationExpiryMs,
2486
2617
  getTierConfig,
2487
2618
  getTotalCredits,
2619
+ getUnlimitedSentinelBalance,
2488
2620
  getValidOperationTypes,
2621
+ getValidTiers,
2489
2622
  initializeConfig,
2490
2623
  isCreditError,
2491
2624
  isCreditErrorMessage,
2492
2625
  isFeatureEnabled,
2626
+ isFreeTier,
2493
2627
  isInsufficientCreditsError,
2494
2628
  isPreviewMode,
2629
+ isUnlimitedTier,
2495
2630
  isValidOperationType,
2631
+ isValidTier,
2496
2632
  loadConfigFromEnv,
2497
2633
  noopDeferred,
2498
2634
  notifySubscriptionExpired,
2499
2635
  notifySubscriptionExpiring,
2500
2636
  parseApiError,
2501
2637
  parseCreditError,
2638
+ parseTier,
2502
2639
  registerConfigProvider,
2503
2640
  registerNotificationHandler,
2504
2641
  releaseReservationWithJournal,
2505
2642
  reserveCreditsForOperation,
2506
2643
  resetConfig,
2507
2644
  synchronousDeferred,
2645
+ tierSchema,
2508
2646
  toClientUserCredits,
2509
2647
  toDate,
2510
2648
  toPortableTimestamp,