@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.
- package/dist/adapters/index.cjs +13 -3
- package/dist/adapters/index.cjs.map +1 -1
- package/dist/adapters/index.d.cts +2 -2
- package/dist/adapters/index.d.ts +2 -2
- package/dist/adapters/index.js +13 -3
- package/dist/adapters/index.js.map +1 -1
- package/dist/core/index.cjs +13 -3
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +3 -3
- package/dist/core/index.d.ts +3 -3
- package/dist/core/index.js +13 -3
- package/dist/core/index.js.map +1 -1
- package/dist/index.cjs +155 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +75 -16
- package/dist/index.d.ts +75 -16
- package/dist/index.js +146 -17
- package/dist/index.js.map +1 -1
- package/dist/repository/index.cjs +47 -11
- package/dist/repository/index.cjs.map +1 -1
- package/dist/repository/index.d.cts +3 -3
- package/dist/repository/index.d.ts +3 -3
- package/dist/repository/index.js +47 -11
- package/dist/repository/index.js.map +1 -1
- package/dist/sdk/index.d.cts +2 -2
- package/dist/sdk/index.d.ts +2 -2
- package/dist/service/index.cjs +130 -10
- package/dist/service/index.cjs.map +1 -1
- package/dist/service/index.d.cts +43 -3
- package/dist/service/index.d.ts +43 -3
- package/dist/service/index.js +130 -10
- package/dist/service/index.js.map +1 -1
- package/dist/{types-D1vdeBcA.d.cts → types-CZNv1U3e.d.cts} +1 -1
- package/dist/{types-BBxIWHK2.d.cts → types-CjLj3GW5.d.cts} +37 -3
- package/dist/{types-BBxIWHK2.d.ts → types-CjLj3GW5.d.ts} +37 -3
- package/dist/{types-aJsyv8df.d.ts → types-G4WK0rM-.d.ts} +1 -1
- package/package.json +10 -9
- 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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
521
|
-
|
|
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
|
|
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
|
|
1124
|
-
const
|
|
1125
|
-
credits.tier =
|
|
1126
|
-
credits.monthlyLimit =
|
|
1127
|
-
credits.balance = Math.min(credits.balance,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
1481
|
-
monthlyUsed:
|
|
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,
|