@nehorai/credits 2.0.0 → 2.2.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/LICENSE +21 -21
- package/README.md +384 -377
- package/dist/adapters/index.cjs +12 -8
- package/dist/adapters/index.cjs.map +1 -1
- package/dist/adapters/index.d.cts +3 -2
- package/dist/adapters/index.d.ts +3 -2
- package/dist/adapters/index.js +12 -8
- package/dist/adapters/index.js.map +1 -1
- package/dist/auth/index.cjs.map +1 -1
- package/dist/auth/index.js.map +1 -1
- package/dist/core/index.cjs +106 -4
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +92 -5
- package/dist/core/index.d.ts +92 -5
- package/dist/core/index.js +96 -4
- package/dist/core/index.js.map +1 -1
- package/dist/index.cjs +282 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -7
- package/dist/index.d.ts +9 -7
- package/dist/index.js +270 -45
- package/dist/index.js.map +1 -1
- package/dist/repository/index.cjs +251 -41
- package/dist/repository/index.cjs.map +1 -1
- package/dist/repository/index.d.cts +34 -6
- package/dist/repository/index.d.ts +34 -6
- package/dist/repository/index.js +248 -40
- package/dist/repository/index.js.map +1 -1
- package/dist/sdk/index.cjs.map +1 -1
- package/dist/sdk/index.d.cts +2 -2
- package/dist/sdk/index.d.ts +2 -2
- package/dist/sdk/index.js.map +1 -1
- package/dist/service/index.cjs +12 -6
- package/dist/service/index.cjs.map +1 -1
- package/dist/service/index.d.cts +2 -2
- package/dist/service/index.d.ts +2 -2
- package/dist/service/index.js +12 -6
- package/dist/service/index.js.map +1 -1
- package/dist/{types-BmG5Omzj.d.ts → types-3c-IAwZI.d.ts} +38 -3
- package/dist/{types-DJ8yqaVr.d.cts → types-MaViK5tD.d.cts} +23 -1
- package/dist/{types-DJ8yqaVr.d.ts → types-MaViK5tD.d.ts} +23 -1
- package/dist/{types-C8hZMe8a.d.cts → types-XSjNXO5l.d.cts} +38 -3
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { I as ICreditRepository, b as AddCreditsV2Input, a as AddCreditsOutcome,
|
|
2
|
-
export {
|
|
3
|
-
import { c as PortableUserCredits, S as SubscriptionTier, a as PortableReservation, R as ReservationStatus, P as PortableTransaction, d as PortableUsageLog, f as PortableJournalEntry, M as MonthlyResetResult, e as SubscriptionExpiryResult } from '../types-
|
|
1
|
+
import { I as ICreditRepository, b as AddCreditsV2Input, a as AddCreditsOutcome, m as ReservationTransitionOptions, C as CommitOutcome, R as ReleaseOutcome, n as ReserveCreditsV2Input, o as ReserveOutcome, g as CreditBalanceUpdate, h as CreditPolicyOptions, p as TierUpdateInput, d as CreateReservationInput, j as ExpireReservationV2Options, E as ExpireOutcome, A as AddCreditsAtomicOptions, e as CreateTransactionInput, f as CreateUsageLogInput, U as UsageLogQuery, c as CreateJournalEntryInput, J as JournalEntryQuery } from '../types-XSjNXO5l.cjs';
|
|
2
|
+
export { i as CreditRepositoryFactory, k as ICreditRepositoryCreditsV2, l as ICreditRepositoryV2, t as reservationJournalKey, u as supportsCreditsV2, v as supportsIdempotentCredit, w as toClientUserCredits } from '../types-XSjNXO5l.cjs';
|
|
3
|
+
import { c as PortableUserCredits, S as SubscriptionTier, a as PortableReservation, R as ReservationStatus, P as PortableTransaction, d as PortableUsageLog, f as PortableJournalEntry, M as MonthlyResetResult, e as SubscriptionExpiryResult } from '../types-MaViK5tD.cjs';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* One reservation flow for both repository generations.
|
|
@@ -62,6 +62,7 @@ declare function addCreditsThroughRepository(repository: ICreditRepository, inpu
|
|
|
62
62
|
* These are pure functions with no server-only dependencies,
|
|
63
63
|
* making them safe to use in any repository implementation.
|
|
64
64
|
*/
|
|
65
|
+
|
|
65
66
|
/**
|
|
66
67
|
* Generate a unique ID
|
|
67
68
|
* Uses timestamp + random string for uniqueness
|
|
@@ -77,6 +78,27 @@ declare function toDate(value: Date | string | unknown): Date;
|
|
|
77
78
|
* Used for calculating monthly reset dates
|
|
78
79
|
*/
|
|
79
80
|
declare function getNextMonthlyReset(from?: Date): Date;
|
|
81
|
+
/** Result of validating a balance update. */
|
|
82
|
+
interface BalanceValidationResult {
|
|
83
|
+
valid: boolean;
|
|
84
|
+
errors: string[];
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Whether an update leaves a valid account: no negative field, and `reserved` covered
|
|
88
|
+
* by `balance + bonusCredits`, which is the floor outstanding holds put under the
|
|
89
|
+
* balance. An increment is applied on top of the absolute value when a call names both.
|
|
90
|
+
*
|
|
91
|
+
* An update is a delta the caller asked for, so one that cannot be honoured is refused,
|
|
92
|
+
* as a deduct is. Only tier writes and resets stop at the floor: they state a target.
|
|
93
|
+
*
|
|
94
|
+
* A negative balance increment is a charge, so it draws only on the balance above
|
|
95
|
+
* `expiredHoldBacking`: those credits are expired and leave when their hold closes, so
|
|
96
|
+
* charging them would hand the charge back. An absolute balance replaces the field and
|
|
97
|
+
* the marks with it, so only an increment on the stored balance meets this limit.
|
|
98
|
+
*/
|
|
99
|
+
declare function validateBalanceUpdate(current: Pick<PortableUserCredits, "balance" | "bonusCredits" | "reserved" | "expiredHoldBacking">, updates: CreditBalanceUpdate): BalanceValidationResult;
|
|
100
|
+
/** Refuse an update {@link validateBalanceUpdate} finds invalid. */
|
|
101
|
+
declare function assertValidBalanceUpdate(current: Pick<PortableUserCredits, "balance" | "bonusCredits" | "reserved" | "expiredHoldBacking">, updates: CreditBalanceUpdate): void;
|
|
80
102
|
|
|
81
103
|
/**
|
|
82
104
|
* In-Memory Credit Repository Implementation
|
|
@@ -92,6 +114,8 @@ declare function getNextMonthlyReset(from?: Date): Date;
|
|
|
92
114
|
|
|
93
115
|
declare class InMemoryCreditRepository implements ICreditRepository {
|
|
94
116
|
private readonly store;
|
|
117
|
+
private readonly options;
|
|
118
|
+
constructor(options?: CreditPolicyOptions);
|
|
95
119
|
/**
|
|
96
120
|
* A copy, never the live record.
|
|
97
121
|
*
|
|
@@ -192,7 +216,11 @@ declare class InMemoryCreditRepository implements ICreditRepository {
|
|
|
192
216
|
creditsReleased: number;
|
|
193
217
|
errors: string[];
|
|
194
218
|
}>;
|
|
195
|
-
|
|
219
|
+
/**
|
|
220
|
+
* @param _tier - Not trusted. The caller read it earlier and the account may
|
|
221
|
+
* have changed tier since, so the reset uses the tier stored on the account.
|
|
222
|
+
*/
|
|
223
|
+
atomicMonthlyReset(userId: string, _tier: SubscriptionTier, expectedResetAt: Date | string): Promise<MonthlyResetResult>;
|
|
196
224
|
checkAndHandleSubscriptionExpiry(userId: string, gracePeriodDays?: number): Promise<SubscriptionExpiryResult>;
|
|
197
225
|
/**
|
|
198
226
|
* Clear all data (useful for testing)
|
|
@@ -221,6 +249,6 @@ declare class InMemoryCreditRepository implements ICreditRepository {
|
|
|
221
249
|
* Create a new in-memory repository instance
|
|
222
250
|
* Each call creates a fresh, isolated instance
|
|
223
251
|
*/
|
|
224
|
-
declare function createInMemoryCreditRepository(): InMemoryCreditRepository;
|
|
252
|
+
declare function createInMemoryCreditRepository(options?: CreditPolicyOptions): InMemoryCreditRepository;
|
|
225
253
|
|
|
226
|
-
export { AddCreditsAtomicOptions, AddCreditsV2Input, CreateJournalEntryInput, CreateReservationInput, CreateTransactionInput, CreateUsageLogInput, CreditBalanceUpdate, ExpireReservationV2Options, ICreditRepository, InMemoryCreditRepository, JournalEntryQuery, ReservationTransitionOptions, ReserveCreditsV2Input, TierUpdateInput, UsageLogQuery, addCreditsThroughRepository, commitThroughRepository, createInMemoryCreditRepository, generateId, getNextMonthlyReset, releaseThroughRepository, reserveThroughRepository, toDate };
|
|
254
|
+
export { AddCreditsAtomicOptions, AddCreditsV2Input, type BalanceValidationResult, CreateJournalEntryInput, CreateReservationInput, CreateTransactionInput, CreateUsageLogInput, CreditBalanceUpdate, ExpireReservationV2Options, ICreditRepository, InMemoryCreditRepository, JournalEntryQuery, ReservationTransitionOptions, ReserveCreditsV2Input, TierUpdateInput, UsageLogQuery, addCreditsThroughRepository, assertValidBalanceUpdate, commitThroughRepository, createInMemoryCreditRepository, generateId, getNextMonthlyReset, releaseThroughRepository, reserveThroughRepository, toDate, validateBalanceUpdate };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { I as ICreditRepository, b as AddCreditsV2Input, a as AddCreditsOutcome,
|
|
2
|
-
export {
|
|
3
|
-
import { c as PortableUserCredits, S as SubscriptionTier, a as PortableReservation, R as ReservationStatus, P as PortableTransaction, d as PortableUsageLog, f as PortableJournalEntry, M as MonthlyResetResult, e as SubscriptionExpiryResult } from '../types-
|
|
1
|
+
import { I as ICreditRepository, b as AddCreditsV2Input, a as AddCreditsOutcome, m as ReservationTransitionOptions, C as CommitOutcome, R as ReleaseOutcome, n as ReserveCreditsV2Input, o as ReserveOutcome, g as CreditBalanceUpdate, h as CreditPolicyOptions, p as TierUpdateInput, d as CreateReservationInput, j as ExpireReservationV2Options, E as ExpireOutcome, A as AddCreditsAtomicOptions, e as CreateTransactionInput, f as CreateUsageLogInput, U as UsageLogQuery, c as CreateJournalEntryInput, J as JournalEntryQuery } from '../types-3c-IAwZI.js';
|
|
2
|
+
export { i as CreditRepositoryFactory, k as ICreditRepositoryCreditsV2, l as ICreditRepositoryV2, t as reservationJournalKey, u as supportsCreditsV2, v as supportsIdempotentCredit, w as toClientUserCredits } from '../types-3c-IAwZI.js';
|
|
3
|
+
import { c as PortableUserCredits, S as SubscriptionTier, a as PortableReservation, R as ReservationStatus, P as PortableTransaction, d as PortableUsageLog, f as PortableJournalEntry, M as MonthlyResetResult, e as SubscriptionExpiryResult } from '../types-MaViK5tD.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* One reservation flow for both repository generations.
|
|
@@ -62,6 +62,7 @@ declare function addCreditsThroughRepository(repository: ICreditRepository, inpu
|
|
|
62
62
|
* These are pure functions with no server-only dependencies,
|
|
63
63
|
* making them safe to use in any repository implementation.
|
|
64
64
|
*/
|
|
65
|
+
|
|
65
66
|
/**
|
|
66
67
|
* Generate a unique ID
|
|
67
68
|
* Uses timestamp + random string for uniqueness
|
|
@@ -77,6 +78,27 @@ declare function toDate(value: Date | string | unknown): Date;
|
|
|
77
78
|
* Used for calculating monthly reset dates
|
|
78
79
|
*/
|
|
79
80
|
declare function getNextMonthlyReset(from?: Date): Date;
|
|
81
|
+
/** Result of validating a balance update. */
|
|
82
|
+
interface BalanceValidationResult {
|
|
83
|
+
valid: boolean;
|
|
84
|
+
errors: string[];
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Whether an update leaves a valid account: no negative field, and `reserved` covered
|
|
88
|
+
* by `balance + bonusCredits`, which is the floor outstanding holds put under the
|
|
89
|
+
* balance. An increment is applied on top of the absolute value when a call names both.
|
|
90
|
+
*
|
|
91
|
+
* An update is a delta the caller asked for, so one that cannot be honoured is refused,
|
|
92
|
+
* as a deduct is. Only tier writes and resets stop at the floor: they state a target.
|
|
93
|
+
*
|
|
94
|
+
* A negative balance increment is a charge, so it draws only on the balance above
|
|
95
|
+
* `expiredHoldBacking`: those credits are expired and leave when their hold closes, so
|
|
96
|
+
* charging them would hand the charge back. An absolute balance replaces the field and
|
|
97
|
+
* the marks with it, so only an increment on the stored balance meets this limit.
|
|
98
|
+
*/
|
|
99
|
+
declare function validateBalanceUpdate(current: Pick<PortableUserCredits, "balance" | "bonusCredits" | "reserved" | "expiredHoldBacking">, updates: CreditBalanceUpdate): BalanceValidationResult;
|
|
100
|
+
/** Refuse an update {@link validateBalanceUpdate} finds invalid. */
|
|
101
|
+
declare function assertValidBalanceUpdate(current: Pick<PortableUserCredits, "balance" | "bonusCredits" | "reserved" | "expiredHoldBacking">, updates: CreditBalanceUpdate): void;
|
|
80
102
|
|
|
81
103
|
/**
|
|
82
104
|
* In-Memory Credit Repository Implementation
|
|
@@ -92,6 +114,8 @@ declare function getNextMonthlyReset(from?: Date): Date;
|
|
|
92
114
|
|
|
93
115
|
declare class InMemoryCreditRepository implements ICreditRepository {
|
|
94
116
|
private readonly store;
|
|
117
|
+
private readonly options;
|
|
118
|
+
constructor(options?: CreditPolicyOptions);
|
|
95
119
|
/**
|
|
96
120
|
* A copy, never the live record.
|
|
97
121
|
*
|
|
@@ -192,7 +216,11 @@ declare class InMemoryCreditRepository implements ICreditRepository {
|
|
|
192
216
|
creditsReleased: number;
|
|
193
217
|
errors: string[];
|
|
194
218
|
}>;
|
|
195
|
-
|
|
219
|
+
/**
|
|
220
|
+
* @param _tier - Not trusted. The caller read it earlier and the account may
|
|
221
|
+
* have changed tier since, so the reset uses the tier stored on the account.
|
|
222
|
+
*/
|
|
223
|
+
atomicMonthlyReset(userId: string, _tier: SubscriptionTier, expectedResetAt: Date | string): Promise<MonthlyResetResult>;
|
|
196
224
|
checkAndHandleSubscriptionExpiry(userId: string, gracePeriodDays?: number): Promise<SubscriptionExpiryResult>;
|
|
197
225
|
/**
|
|
198
226
|
* Clear all data (useful for testing)
|
|
@@ -221,6 +249,6 @@ declare class InMemoryCreditRepository implements ICreditRepository {
|
|
|
221
249
|
* Create a new in-memory repository instance
|
|
222
250
|
* Each call creates a fresh, isolated instance
|
|
223
251
|
*/
|
|
224
|
-
declare function createInMemoryCreditRepository(): InMemoryCreditRepository;
|
|
252
|
+
declare function createInMemoryCreditRepository(options?: CreditPolicyOptions): InMemoryCreditRepository;
|
|
225
253
|
|
|
226
|
-
export { AddCreditsAtomicOptions, AddCreditsV2Input, CreateJournalEntryInput, CreateReservationInput, CreateTransactionInput, CreateUsageLogInput, CreditBalanceUpdate, ExpireReservationV2Options, ICreditRepository, InMemoryCreditRepository, JournalEntryQuery, ReservationTransitionOptions, ReserveCreditsV2Input, TierUpdateInput, UsageLogQuery, addCreditsThroughRepository, commitThroughRepository, createInMemoryCreditRepository, generateId, getNextMonthlyReset, releaseThroughRepository, reserveThroughRepository, toDate };
|
|
254
|
+
export { AddCreditsAtomicOptions, AddCreditsV2Input, type BalanceValidationResult, CreateJournalEntryInput, CreateReservationInput, CreateTransactionInput, CreateUsageLogInput, CreditBalanceUpdate, ExpireReservationV2Options, ICreditRepository, InMemoryCreditRepository, JournalEntryQuery, ReservationTransitionOptions, ReserveCreditsV2Input, TierUpdateInput, UsageLogQuery, addCreditsThroughRepository, assertValidBalanceUpdate, commitThroughRepository, createInMemoryCreditRepository, generateId, getNextMonthlyReset, releaseThroughRepository, reserveThroughRepository, toDate, validateBalanceUpdate };
|
package/dist/repository/index.js
CHANGED
|
@@ -16,6 +16,8 @@ function toClientUserCredits(credits) {
|
|
|
16
16
|
monthlyUsed: credits.monthlyUsed,
|
|
17
17
|
monthlyResetAt: toISOString(credits.monthlyResetAt),
|
|
18
18
|
subscriptionExpiresAt: credits.subscriptionExpiresAt ? toISOString(credits.subscriptionExpiresAt) : null,
|
|
19
|
+
...credits.expiredHoldBacking !== void 0 ? { expiredHoldBacking: credits.expiredHoldBacking } : {},
|
|
20
|
+
...credits.signupGrant !== void 0 ? { signupGrant: credits.signupGrant } : {},
|
|
19
21
|
createdAt: toISOString(credits.createdAt),
|
|
20
22
|
updatedAt: toISOString(credits.updatedAt)
|
|
21
23
|
};
|
|
@@ -106,6 +108,83 @@ function backedBalanceFloor(reserved, bonusCredits) {
|
|
|
106
108
|
assertRepresentableAmount(floor, "backedBalanceFloor", { reserved, bonusCredits });
|
|
107
109
|
return floor;
|
|
108
110
|
}
|
|
111
|
+
function spendableBalance(balance, expiredHoldBacking = 0) {
|
|
112
|
+
return sumAmounts(balance, -Math.min(expiredHoldBacking, balance));
|
|
113
|
+
}
|
|
114
|
+
function planBalanceTarget(target, reserved, bonusCredits) {
|
|
115
|
+
const balance = Math.max(target, backedBalanceFloor(reserved, bonusCredits));
|
|
116
|
+
return { balance, expiredHoldBacking: sumAmounts(balance, -target) };
|
|
117
|
+
}
|
|
118
|
+
function planBalanceCap(balance, cap, reserved, bonusCredits, expiredHoldBacking = 0) {
|
|
119
|
+
const spendable = spendableBalance(balance, expiredHoldBacking);
|
|
120
|
+
const target = Math.min(spendable, cap);
|
|
121
|
+
const kept = Math.max(
|
|
122
|
+
sumAmounts(balance, -spendable),
|
|
123
|
+
sumAmounts(backedBalanceFloor(reserved, bonusCredits), -target)
|
|
124
|
+
);
|
|
125
|
+
const next = Math.min(balance, sumAmounts(target, kept));
|
|
126
|
+
return { balance: next, expiredHoldBacking: Math.max(0, sumAmounts(next, -target)) };
|
|
127
|
+
}
|
|
128
|
+
function settleExpiredHoldBacking(balance, expiredHoldBacking, holdBacking) {
|
|
129
|
+
const held = Math.min(expiredHoldBacking, balance);
|
|
130
|
+
const used = Math.min(held, holdBacking);
|
|
131
|
+
return { used, remaining: sumAmounts(held, -used) };
|
|
132
|
+
}
|
|
133
|
+
function planHoldCommit(balance, bonusCredits, expiredHoldBacking, holdBacking, amount) {
|
|
134
|
+
const backing = settleExpiredHoldBacking(balance, expiredHoldBacking, holdBacking);
|
|
135
|
+
const rest = sumAmounts(amount, -backing.used);
|
|
136
|
+
const fromSpendable = Math.min(spendableBalance(balance, expiredHoldBacking), rest);
|
|
137
|
+
const fromBonus = sumAmounts(rest, -fromSpendable);
|
|
138
|
+
return {
|
|
139
|
+
covered: fromBonus <= bonusCredits,
|
|
140
|
+
balance: sumAmounts(balance, -backing.used, -fromSpendable),
|
|
141
|
+
bonusCredits: sumAmounts(bonusCredits, -fromBonus),
|
|
142
|
+
expiredHoldBacking: backing.remaining
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function attributeExpiredBacking(holds, total) {
|
|
146
|
+
const placed = holds.filter((hold) => hold.holdPlacedAt).sort(
|
|
147
|
+
(a, b) => a.holdPlacedAt === b.holdPlacedAt ? a.id < b.id ? -1 : 1 : a.holdPlacedAt < b.holdPlacedAt ? -1 : 1
|
|
148
|
+
);
|
|
149
|
+
const shares = /* @__PURE__ */ new Map();
|
|
150
|
+
let left = total;
|
|
151
|
+
for (const hold of placed) {
|
|
152
|
+
const kept = Math.min(hold.expiredBacking ?? 0, hold.amount, left);
|
|
153
|
+
shares.set(hold.id, kept);
|
|
154
|
+
left = sumAmounts(left, -kept);
|
|
155
|
+
}
|
|
156
|
+
for (const hold of placed) {
|
|
157
|
+
const share = shares.get(hold.id);
|
|
158
|
+
const added = Math.min(sumAmounts(hold.amount, -share), left);
|
|
159
|
+
shares.set(hold.id, sumAmounts(share, added));
|
|
160
|
+
left = sumAmounts(left, -added);
|
|
161
|
+
}
|
|
162
|
+
return shares;
|
|
163
|
+
}
|
|
164
|
+
function expiredHoldBackingPatch(next, stored = 0) {
|
|
165
|
+
return next > 0 || stored > 0 ? { expiredHoldBacking: next } : {};
|
|
166
|
+
}
|
|
167
|
+
function assertValidBalanceTarget(value, field, context) {
|
|
168
|
+
assertRepresentableAmount(value, field, context);
|
|
169
|
+
if (value < 0) {
|
|
170
|
+
throw new CreditError(
|
|
171
|
+
`${field} cannot be negative (got ${value})`,
|
|
172
|
+
CreditErrorCode.INVALID_AMOUNT,
|
|
173
|
+
{ field, value, ...context }
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function assertValidTierBalanceInput(input, context) {
|
|
178
|
+
if (input.balanceCap === void 0) return;
|
|
179
|
+
if (input.balance !== void 0) {
|
|
180
|
+
throw new CreditError(
|
|
181
|
+
"A tier update cannot set both balance and balanceCap",
|
|
182
|
+
CreditErrorCode.INVALID_AMOUNT,
|
|
183
|
+
{ balance: input.balance, balanceCap: input.balanceCap, ...context }
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
assertValidBalanceTarget(input.balanceCap, "balanceCap", context);
|
|
187
|
+
}
|
|
109
188
|
function assertRepresentableTierAmount(value, field, context) {
|
|
110
189
|
if (value === Number.POSITIVE_INFINITY) return;
|
|
111
190
|
assertRepresentableAmount(value, field, context);
|
|
@@ -295,11 +374,13 @@ var creditSystemConfigSchema = z.object({
|
|
|
295
374
|
/**
|
|
296
375
|
* Operation costs in credits
|
|
297
376
|
* Keys are operation type names (any non-empty string)
|
|
298
|
-
* Values are
|
|
377
|
+
* Values are non-negative numbers representing credit cost. 0 marks a free
|
|
378
|
+
* operation: `withCredits` runs it without touching the ledger. Amounts that
|
|
379
|
+
* move credits stay strictly positive, so `reserveCredits` refuses a 0.
|
|
299
380
|
*/
|
|
300
381
|
operationCosts: z.record(
|
|
301
382
|
z.string().min(1),
|
|
302
|
-
z.number().
|
|
383
|
+
z.number().nonnegative()
|
|
303
384
|
),
|
|
304
385
|
/**
|
|
305
386
|
* Display labels for operation types (English canonical labels).
|
|
@@ -321,9 +402,9 @@ var creditSystemConfigSchema = z.object({
|
|
|
321
402
|
*/
|
|
322
403
|
reservationExpiryMs: z.number().positive().default(5 * 60 * 1e3),
|
|
323
404
|
/**
|
|
324
|
-
* Default credits for new users (free tier)
|
|
405
|
+
* Default credits for new users (free tier). 0 means no signup grant.
|
|
325
406
|
*/
|
|
326
|
-
defaultFreeCredits: z.number().
|
|
407
|
+
defaultFreeCredits: z.number().min(0).default(25),
|
|
327
408
|
/**
|
|
328
409
|
* Grace period in days after subscription expires before downgrade
|
|
329
410
|
*/
|
|
@@ -753,11 +834,36 @@ function toDate(value) {
|
|
|
753
834
|
return /* @__PURE__ */ new Date();
|
|
754
835
|
}
|
|
755
836
|
function getNextMonthlyReset(from = /* @__PURE__ */ new Date()) {
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
next
|
|
760
|
-
|
|
837
|
+
return new Date(from.getFullYear(), from.getMonth() + 1, 1);
|
|
838
|
+
}
|
|
839
|
+
function validateBalanceUpdate(current, updates) {
|
|
840
|
+
const next = (base, absolute, increment) => sumAmounts(absolute ?? base, increment ?? 0);
|
|
841
|
+
const balance = next(current.balance, updates.balance, updates.balanceIncrement);
|
|
842
|
+
const bonusCredits = next(
|
|
843
|
+
current.bonusCredits ?? 0,
|
|
844
|
+
updates.bonusCredits,
|
|
845
|
+
updates.bonusCreditsIncrement
|
|
846
|
+
);
|
|
847
|
+
const reserved = next(current.reserved ?? 0, updates.reserved, updates.reservedIncrement);
|
|
848
|
+
const errors = [];
|
|
849
|
+
if (balance < 0) errors.push(`Balance cannot be negative: ${balance}`);
|
|
850
|
+
const marked = updates.balance === void 0 ? current.expiredHoldBacking ?? 0 : 0;
|
|
851
|
+
if ((updates.balanceIncrement ?? 0) < 0 && balance >= 0 && balance < marked) {
|
|
852
|
+
errors.push(`Balance (${balance}) cannot go below expired hold backing (${marked})`);
|
|
853
|
+
}
|
|
854
|
+
if (bonusCredits < 0) errors.push(`BonusCredits cannot be negative: ${bonusCredits}`);
|
|
855
|
+
if (reserved < 0) errors.push(`Reserved cannot be negative: ${reserved}`);
|
|
856
|
+
const covering = sumAmounts(balance, bonusCredits);
|
|
857
|
+
if (reserved > covering) {
|
|
858
|
+
errors.push(`Reserved (${reserved}) exceeds balance + bonusCredits (${covering})`);
|
|
859
|
+
}
|
|
860
|
+
return { valid: errors.length === 0, errors };
|
|
861
|
+
}
|
|
862
|
+
function assertValidBalanceUpdate(current, updates) {
|
|
863
|
+
const result = validateBalanceUpdate(current, updates);
|
|
864
|
+
if (!result.valid) {
|
|
865
|
+
throw new Error(`Invalid balance update: ${result.errors.join("; ")}`);
|
|
866
|
+
}
|
|
761
867
|
}
|
|
762
868
|
|
|
763
869
|
// src/repository/memory/snapshot.ts
|
|
@@ -1038,6 +1144,22 @@ function assertTrustworthy(reservation, transition) {
|
|
|
1038
1144
|
assertLockedAmount(reservation, transition);
|
|
1039
1145
|
assertHoldPlaced(reservation, transition);
|
|
1040
1146
|
}
|
|
1147
|
+
function openHolds(store, userId) {
|
|
1148
|
+
return [...store.reservations.get(userId)?.values() ?? []].filter(
|
|
1149
|
+
(reservation) => reservation.status === "reserved"
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1152
|
+
function writeExpiredHoldBacking(store, credits, next) {
|
|
1153
|
+
const stored = credits.expiredHoldBacking;
|
|
1154
|
+
if (next <= 0 && !stored) return;
|
|
1155
|
+
const holds = openHolds(store, credits.userId);
|
|
1156
|
+
const shares = attributeExpiredBacking(holds, next);
|
|
1157
|
+
for (const hold of holds) {
|
|
1158
|
+
const share = shares.get(hold.id) ?? 0;
|
|
1159
|
+
if (share !== (hold.expiredBacking ?? 0)) hold.expiredBacking = share;
|
|
1160
|
+
}
|
|
1161
|
+
Object.assign(credits, expiredHoldBackingPatch(sumAmounts(...shares.values()), stored));
|
|
1162
|
+
}
|
|
1041
1163
|
function getReservation(store, userId, reservationId) {
|
|
1042
1164
|
return store.reservations.get(userId)?.get(reservationId);
|
|
1043
1165
|
}
|
|
@@ -1135,16 +1257,22 @@ async function commitReservationV2(store, userId, reservationId, options) {
|
|
|
1135
1257
|
const amount = reservation.amount;
|
|
1136
1258
|
await yieldPoint(store);
|
|
1137
1259
|
assertReservedCoversHold(credits, userId, reservationId, amount);
|
|
1138
|
-
|
|
1260
|
+
const paid = planHoldCommit(
|
|
1261
|
+
credits.balance,
|
|
1262
|
+
credits.bonusCredits,
|
|
1263
|
+
credits.expiredHoldBacking ?? 0,
|
|
1264
|
+
reservation.expiredBacking ?? 0,
|
|
1265
|
+
amount
|
|
1266
|
+
);
|
|
1267
|
+
if (!paid.covered) {
|
|
1139
1268
|
throw new CreditError(
|
|
1140
1269
|
`Insufficient credits to commit reservation ${reservationId}`,
|
|
1141
1270
|
CreditErrorCode.INSUFFICIENT_CREDITS,
|
|
1142
1271
|
{ userId, reservationId, required: amount }
|
|
1143
1272
|
);
|
|
1144
1273
|
}
|
|
1145
|
-
const
|
|
1146
|
-
const
|
|
1147
|
-
const nextBonusCredits = sumAmounts(credits.bonusCredits, -sumAmounts(amount, -fromBalance));
|
|
1274
|
+
const nextBalance = paid.balance;
|
|
1275
|
+
const nextBonusCredits = paid.bonusCredits;
|
|
1148
1276
|
const balanceAfter = sumAmounts(nextBalance, nextBonusCredits);
|
|
1149
1277
|
const journal = {
|
|
1150
1278
|
userId,
|
|
@@ -1178,6 +1306,10 @@ async function commitReservationV2(store, userId, reservationId, options) {
|
|
|
1178
1306
|
credits.bonusCredits = nextBonusCredits;
|
|
1179
1307
|
credits.reserved = nextReserved;
|
|
1180
1308
|
credits.monthlyUsed = nextMonthlyUsed;
|
|
1309
|
+
Object.assign(
|
|
1310
|
+
credits,
|
|
1311
|
+
expiredHoldBackingPatch(paid.expiredHoldBacking, credits.expiredHoldBacking)
|
|
1312
|
+
);
|
|
1181
1313
|
credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1182
1314
|
const journalEntryId = commitTransitionJournal(store, journal, existingEntryId);
|
|
1183
1315
|
return {
|
|
@@ -1257,19 +1389,26 @@ function finishUnspent(store, reservation, transition) {
|
|
|
1257
1389
|
reservation.id,
|
|
1258
1390
|
reservation.amount
|
|
1259
1391
|
);
|
|
1392
|
+
const backing = settleExpiredHoldBacking(
|
|
1393
|
+
credits.balance,
|
|
1394
|
+
credits.expiredHoldBacking ?? 0,
|
|
1395
|
+
reservation.expiredBacking ?? 0
|
|
1396
|
+
);
|
|
1397
|
+
const nextBalance = sumAmounts(credits.balance, -backing.used);
|
|
1260
1398
|
const journal = {
|
|
1261
1399
|
userId: reservation.userId,
|
|
1262
1400
|
operation: `${transition.transition}Reservation`,
|
|
1263
1401
|
entryType: "credit",
|
|
1264
1402
|
amount: 0,
|
|
1265
|
-
balanceAfter: sumAmounts(
|
|
1403
|
+
balanceAfter: sumAmounts(nextBalance, credits.bonusCredits),
|
|
1266
1404
|
source: transition.source,
|
|
1267
1405
|
reservationId: reservation.id,
|
|
1268
1406
|
description: transition.options?.description ?? `${transition.verb} ${reservation.amount} reserved credits for ${getOperationLabel(reservation.operationType)}`,
|
|
1269
1407
|
metadata: {
|
|
1270
1408
|
...transition.options?.metadata,
|
|
1271
1409
|
operationType: reservation.operationType,
|
|
1272
|
-
amount: reservation.amount
|
|
1410
|
+
amount: reservation.amount,
|
|
1411
|
+
...backing.used > 0 ? { expiredCreditsRemoved: backing.used } : {}
|
|
1273
1412
|
},
|
|
1274
1413
|
idempotencyKey: reservationJournalKey(reservation.id, transition.transition)
|
|
1275
1414
|
};
|
|
@@ -1282,6 +1421,8 @@ function finishUnspent(store, reservation, transition) {
|
|
|
1282
1421
|
reservation.status = transition.status;
|
|
1283
1422
|
reservation.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1284
1423
|
credits.reserved = nextReserved;
|
|
1424
|
+
credits.balance = nextBalance;
|
|
1425
|
+
Object.assign(credits, expiredHoldBackingPatch(backing.remaining, credits.expiredHoldBacking));
|
|
1285
1426
|
credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1286
1427
|
return commitTransitionJournal(store, journal, existingEntryId);
|
|
1287
1428
|
}
|
|
@@ -1292,6 +1433,10 @@ function isoOf(value) {
|
|
|
1292
1433
|
}
|
|
1293
1434
|
var InMemoryCreditRepository = class {
|
|
1294
1435
|
store = new MemoryStore();
|
|
1436
|
+
options;
|
|
1437
|
+
constructor(options = {}) {
|
|
1438
|
+
this.options = options;
|
|
1439
|
+
}
|
|
1295
1440
|
// ==================== User Credits ====================
|
|
1296
1441
|
/**
|
|
1297
1442
|
* A copy, never the live record.
|
|
@@ -1308,7 +1453,8 @@ var InMemoryCreditRepository = class {
|
|
|
1308
1453
|
return copyRecord(credits) ?? null;
|
|
1309
1454
|
}
|
|
1310
1455
|
async initializeUserCredits(userId, tier, initialBalance) {
|
|
1311
|
-
|
|
1456
|
+
const existing = this.store.users.get(userId);
|
|
1457
|
+
return copyRecord(existing ?? this.seedUser(userId, tier, initialBalance));
|
|
1312
1458
|
}
|
|
1313
1459
|
/**
|
|
1314
1460
|
* The synchronous body of `initializeUserCredits`.
|
|
@@ -1359,8 +1505,11 @@ var InMemoryCreditRepository = class {
|
|
|
1359
1505
|
if (!credits) {
|
|
1360
1506
|
throw new Error(`User ${userId} not found`);
|
|
1361
1507
|
}
|
|
1508
|
+
assertValidBalanceUpdate(credits, updates);
|
|
1362
1509
|
const next = { ...credits };
|
|
1363
|
-
if (updates.balance !== void 0)
|
|
1510
|
+
if (updates.balance !== void 0) {
|
|
1511
|
+
next.balance = updates.balance;
|
|
1512
|
+
}
|
|
1364
1513
|
if (updates.bonusCredits !== void 0) next.bonusCredits = updates.bonusCredits;
|
|
1365
1514
|
if (updates.reserved !== void 0) next.reserved = updates.reserved;
|
|
1366
1515
|
if (updates.tier !== void 0) next.tier = updates.tier;
|
|
@@ -1397,6 +1546,9 @@ var InMemoryCreditRepository = class {
|
|
|
1397
1546
|
userId,
|
|
1398
1547
|
operation: "updateUserCredits"
|
|
1399
1548
|
});
|
|
1549
|
+
if ((credits.expiredHoldBacking ?? 0) > 0 && updates.balance !== void 0) {
|
|
1550
|
+
writeExpiredHoldBacking(this.store, next, 0);
|
|
1551
|
+
}
|
|
1400
1552
|
next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1401
1553
|
this.store.users.set(userId, next);
|
|
1402
1554
|
}
|
|
@@ -1405,17 +1557,28 @@ var InMemoryCreditRepository = class {
|
|
|
1405
1557
|
{ monthlyLimit: input.monthlyLimit, balance: input.balance, monthlyUsed: input.monthlyUsed },
|
|
1406
1558
|
{ userId, operation: "updateUserTier" }
|
|
1407
1559
|
);
|
|
1560
|
+
assertValidTierBalanceInput(input, { userId, operation: "updateUserTier" });
|
|
1408
1561
|
const credits = this.store.users.get(userId);
|
|
1409
1562
|
if (!credits) {
|
|
1410
1563
|
throw new Error(`User ${userId} not found`);
|
|
1411
1564
|
}
|
|
1565
|
+
if (input.balanceCap !== void 0 && !isFreeTier(credits.tier)) {
|
|
1566
|
+
const capped = planBalanceCap(
|
|
1567
|
+
credits.balance,
|
|
1568
|
+
input.balanceCap,
|
|
1569
|
+
credits.reserved,
|
|
1570
|
+
credits.bonusCredits,
|
|
1571
|
+
credits.expiredHoldBacking
|
|
1572
|
+
);
|
|
1573
|
+
credits.balance = capped.balance;
|
|
1574
|
+
writeExpiredHoldBacking(this.store, credits, capped.expiredHoldBacking);
|
|
1575
|
+
}
|
|
1412
1576
|
credits.tier = input.tier;
|
|
1413
1577
|
credits.monthlyLimit = input.monthlyLimit;
|
|
1414
1578
|
if (input.balance !== void 0) {
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
);
|
|
1579
|
+
const planned = planBalanceTarget(input.balance, credits.reserved, credits.bonusCredits);
|
|
1580
|
+
credits.balance = planned.balance;
|
|
1581
|
+
writeExpiredHoldBacking(this.store, credits, planned.expiredHoldBacking);
|
|
1419
1582
|
}
|
|
1420
1583
|
if (input.monthlyUsed !== void 0) credits.monthlyUsed = input.monthlyUsed;
|
|
1421
1584
|
if (input.subscriptionExpiresAt !== void 0) {
|
|
@@ -1636,13 +1799,17 @@ var InMemoryCreditRepository = class {
|
|
|
1636
1799
|
if (!credits) {
|
|
1637
1800
|
throw new Error(`User credits not found for userId: ${userId}`);
|
|
1638
1801
|
}
|
|
1639
|
-
const
|
|
1802
|
+
const spendable = spendableBalance(credits.balance, credits.expiredHoldBacking);
|
|
1803
|
+
const available = Math.min(
|
|
1804
|
+
sumAmounts(credits.balance, credits.bonusCredits, -credits.reserved),
|
|
1805
|
+
sumAmounts(spendable, credits.bonusCredits)
|
|
1806
|
+
);
|
|
1640
1807
|
if (available < amount) {
|
|
1641
1808
|
throw new Error(
|
|
1642
1809
|
`Insufficient credits. Available: ${available}, requested: ${amount}`
|
|
1643
1810
|
);
|
|
1644
1811
|
}
|
|
1645
|
-
const balanceDeduction = Math.min(
|
|
1812
|
+
const balanceDeduction = Math.min(spendable, amount);
|
|
1646
1813
|
const bonusDeduction = sumAmounts(amount, -balanceDeduction);
|
|
1647
1814
|
const previousBalance = sumAmounts(credits.balance, credits.bonusCredits);
|
|
1648
1815
|
const nextBalance = sumAmounts(credits.balance, -balanceDeduction);
|
|
@@ -1879,7 +2046,11 @@ var InMemoryCreditRepository = class {
|
|
|
1879
2046
|
return { expiredCount, creditsReleased, errors };
|
|
1880
2047
|
}
|
|
1881
2048
|
// ==================== Atomic Monthly Reset ====================
|
|
1882
|
-
|
|
2049
|
+
/**
|
|
2050
|
+
* @param _tier - Not trusted. The caller read it earlier and the account may
|
|
2051
|
+
* have changed tier since, so the reset uses the tier stored on the account.
|
|
2052
|
+
*/
|
|
2053
|
+
async atomicMonthlyReset(userId, _tier, expectedResetAt) {
|
|
1883
2054
|
const credits = this.store.users.get(userId);
|
|
1884
2055
|
if (!credits) {
|
|
1885
2056
|
throw new Error(`User ${userId} not found`);
|
|
@@ -1889,17 +2060,41 @@ var InMemoryCreditRepository = class {
|
|
|
1889
2060
|
if (currentResetAt !== expected) {
|
|
1890
2061
|
return { wasReset: false, credits: { ...credits } };
|
|
1891
2062
|
}
|
|
1892
|
-
|
|
2063
|
+
const tier = credits.tier;
|
|
2064
|
+
const spendable = spendableBalance(credits.balance, credits.expiredHoldBacking);
|
|
2065
|
+
const decision = this.options.resolveMonthlyReset?.({ tier, balance: spendable });
|
|
2066
|
+
if (decision === null) {
|
|
2067
|
+
return { wasReset: false, credits: { ...credits } };
|
|
2068
|
+
}
|
|
2069
|
+
if (decision) {
|
|
2070
|
+
assertValidBalanceTarget(decision.balance, "resolveMonthlyReset balance", { userId, tier });
|
|
2071
|
+
if (decision.monthlyLimit !== void 0) {
|
|
2072
|
+
assertValidBalanceTarget(decision.monthlyLimit, "resolveMonthlyReset monthlyLimit", {
|
|
2073
|
+
userId,
|
|
2074
|
+
tier
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
2077
|
+
} else {
|
|
2078
|
+
assertRepresentableTierAmount(getConfigMonthlyLimit(tier), "monthlyLimit", { userId, tier });
|
|
2079
|
+
}
|
|
1893
2080
|
const nextReset = getNextMonthlyReset();
|
|
1894
|
-
const target = monthlyResetBalance(tier);
|
|
1895
|
-
const floor = backedBalanceFloor(credits.reserved, credits.bonusCredits);
|
|
1896
2081
|
const previousBalance = credits.balance;
|
|
1897
|
-
|
|
2082
|
+
let target;
|
|
2083
|
+
if (decision) {
|
|
2084
|
+
target = decision.balance;
|
|
2085
|
+
} else {
|
|
2086
|
+
const configured = monthlyResetBalance(tier);
|
|
2087
|
+
target = configured.kind === "atLeast" ? Math.max(spendable, configured.value) : configured.value;
|
|
2088
|
+
}
|
|
2089
|
+
const planned = planBalanceTarget(target, credits.reserved, credits.bonusCredits);
|
|
2090
|
+
const newBalance = planned.balance;
|
|
1898
2091
|
const change = sumAmounts(newBalance, -previousBalance);
|
|
1899
2092
|
const balanceAfter = sumAmounts(newBalance, credits.bonusCredits);
|
|
1900
2093
|
assertRepresentableAmount(Math.abs(change), "journal amount", { userId });
|
|
1901
2094
|
assertRepresentableAmount(balanceAfter, "journal balanceAfter", { userId });
|
|
1902
2095
|
credits.balance = newBalance;
|
|
2096
|
+
writeExpiredHoldBacking(this.store, credits, planned.expiredHoldBacking);
|
|
2097
|
+
if (decision?.monthlyLimit !== void 0) credits.monthlyLimit = decision.monthlyLimit;
|
|
1903
2098
|
credits.monthlyUsed = 0;
|
|
1904
2099
|
credits.monthlyResetAt = nextReset.toISOString();
|
|
1905
2100
|
credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1953,23 +2148,34 @@ var InMemoryCreditRepository = class {
|
|
|
1953
2148
|
};
|
|
1954
2149
|
}
|
|
1955
2150
|
const defaultTier = getDefaultTier();
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
tier: defaultTier
|
|
1960
|
-
}
|
|
1961
|
-
|
|
2151
|
+
let limit;
|
|
2152
|
+
if (this.options.getDowngradeBalanceCap) {
|
|
2153
|
+
limit = this.options.getDowngradeBalanceCap();
|
|
2154
|
+
assertValidBalanceTarget(limit, "getDowngradeBalanceCap", { userId, tier: defaultTier });
|
|
2155
|
+
} else {
|
|
2156
|
+
const configured = getConfigMonthlyLimit(defaultTier);
|
|
2157
|
+
assertRepresentableTierAmount(configured, "monthlyLimit", {
|
|
2158
|
+
userId,
|
|
2159
|
+
tier: defaultTier
|
|
2160
|
+
});
|
|
2161
|
+
limit = storedMonthlyLimit(configured);
|
|
2162
|
+
}
|
|
1962
2163
|
const previousTier = credits.tier;
|
|
1963
2164
|
const previousBalance = credits.balance;
|
|
1964
|
-
const
|
|
1965
|
-
|
|
1966
|
-
|
|
2165
|
+
const planned = planBalanceCap(
|
|
2166
|
+
credits.balance,
|
|
2167
|
+
limit,
|
|
2168
|
+
credits.reserved,
|
|
2169
|
+
credits.bonusCredits,
|
|
2170
|
+
credits.expiredHoldBacking
|
|
1967
2171
|
);
|
|
2172
|
+
const newBalance = planned.balance;
|
|
1968
2173
|
const balanceAfter = sumAmounts(newBalance, credits.bonusCredits);
|
|
1969
2174
|
assertRepresentableAmount(balanceAfter, "journal balanceAfter", { userId });
|
|
1970
2175
|
credits.tier = defaultTier;
|
|
1971
2176
|
credits.monthlyLimit = limit;
|
|
1972
2177
|
credits.balance = newBalance;
|
|
2178
|
+
writeExpiredHoldBacking(this.store, credits, planned.expiredHoldBacking);
|
|
1973
2179
|
credits.subscriptionExpiresAt = null;
|
|
1974
2180
|
credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1975
2181
|
this.store.users.set(userId, credits);
|
|
@@ -2027,12 +2233,13 @@ var InMemoryCreditRepository = class {
|
|
|
2027
2233
|
return copyRecords(Array.from(userReservations.values()));
|
|
2028
2234
|
}
|
|
2029
2235
|
};
|
|
2030
|
-
function createInMemoryCreditRepository() {
|
|
2031
|
-
return new InMemoryCreditRepository();
|
|
2236
|
+
function createInMemoryCreditRepository(options = {}) {
|
|
2237
|
+
return new InMemoryCreditRepository(options);
|
|
2032
2238
|
}
|
|
2033
2239
|
export {
|
|
2034
2240
|
InMemoryCreditRepository,
|
|
2035
2241
|
addCreditsThroughRepository,
|
|
2242
|
+
assertValidBalanceUpdate,
|
|
2036
2243
|
commitThroughRepository,
|
|
2037
2244
|
createInMemoryCreditRepository,
|
|
2038
2245
|
generateId,
|
|
@@ -2043,6 +2250,7 @@ export {
|
|
|
2043
2250
|
supportsCreditsV2,
|
|
2044
2251
|
supportsIdempotentCredit,
|
|
2045
2252
|
toClientUserCredits,
|
|
2046
|
-
toDate
|
|
2253
|
+
toDate,
|
|
2254
|
+
validateBalanceUpdate
|
|
2047
2255
|
};
|
|
2048
2256
|
//# sourceMappingURL=index.js.map
|