@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.
Files changed (43) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +384 -377
  3. package/dist/adapters/index.cjs +12 -8
  4. package/dist/adapters/index.cjs.map +1 -1
  5. package/dist/adapters/index.d.cts +3 -2
  6. package/dist/adapters/index.d.ts +3 -2
  7. package/dist/adapters/index.js +12 -8
  8. package/dist/adapters/index.js.map +1 -1
  9. package/dist/auth/index.cjs.map +1 -1
  10. package/dist/auth/index.js.map +1 -1
  11. package/dist/core/index.cjs +106 -4
  12. package/dist/core/index.cjs.map +1 -1
  13. package/dist/core/index.d.cts +92 -5
  14. package/dist/core/index.d.ts +92 -5
  15. package/dist/core/index.js +96 -4
  16. package/dist/core/index.js.map +1 -1
  17. package/dist/index.cjs +282 -45
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +9 -7
  20. package/dist/index.d.ts +9 -7
  21. package/dist/index.js +270 -45
  22. package/dist/index.js.map +1 -1
  23. package/dist/repository/index.cjs +251 -41
  24. package/dist/repository/index.cjs.map +1 -1
  25. package/dist/repository/index.d.cts +34 -6
  26. package/dist/repository/index.d.ts +34 -6
  27. package/dist/repository/index.js +248 -40
  28. package/dist/repository/index.js.map +1 -1
  29. package/dist/sdk/index.cjs.map +1 -1
  30. package/dist/sdk/index.d.cts +2 -2
  31. package/dist/sdk/index.d.ts +2 -2
  32. package/dist/sdk/index.js.map +1 -1
  33. package/dist/service/index.cjs +12 -6
  34. package/dist/service/index.cjs.map +1 -1
  35. package/dist/service/index.d.cts +2 -2
  36. package/dist/service/index.d.ts +2 -2
  37. package/dist/service/index.js +12 -6
  38. package/dist/service/index.js.map +1 -1
  39. package/dist/{types-BmG5Omzj.d.ts → types-3c-IAwZI.d.ts} +38 -3
  40. package/dist/{types-DJ8yqaVr.d.cts → types-MaViK5tD.d.cts} +23 -1
  41. package/dist/{types-DJ8yqaVr.d.ts → types-MaViK5tD.d.ts} +23 -1
  42. package/dist/{types-C8hZMe8a.d.cts → types-XSjNXO5l.d.cts} +38 -3
  43. package/package.json +1 -1
package/dist/index.d.cts CHANGED
@@ -1,10 +1,10 @@
1
- import { S as SubscriptionTier, T as TierConfig, a as PortableReservation } from './types-DJ8yqaVr.cjs';
2
- export { A as AIProviderType, B as BuiltinTier, g as CreditCheckResult, C as CreditOperationType, b as CreditSource, D as DeductCreditsResult, J as JournalReferenceType, M as MonthlyResetResult, f as PortableJournalEntry, P as PortableTransaction, d as PortableUsageLog, c as PortableUserCredits, R as ReservationStatus, h as ResourceType, g as SDKCreditCheckResult, e as SubscriptionExpiryResult, i as TransactionType, U as UsageHistoryEntry, j as UsageHistoryResponse, c as UserCredits, W as WithCreditsOptions, k as calculateAvailableCredits, t as toDate, l as toPortableTimestamp } from './types-DJ8yqaVr.cjs';
3
- export { A as AddCreditsAtomicOptions, a as AddCreditsOutcome, b as AddCreditsV2Input, C as CommitOutcome, c as CreateJournalEntryInput, d as CreateReservationInput, e as CreateTransactionInput, f as CreateUsageLogInput, g as CreditBalanceUpdate, h as CreditRepositoryFactory, E as ExpireOutcome, i as ExpireReservationV2Options, I as ICreditRepository, j as ICreditRepositoryCreditsV2, k as ICreditRepositoryV2, J as JournalEntryQuery, R as ReleaseOutcome, l as ReservationTransitionOptions, m as ReserveCreditsV2Input, n as ReserveOutcome, T as TerminalReservationStatus, o as TierUpdateInput, U as UsageLogQuery, p as isCreditedOutcome, q as isReservedOutcome, r as isWinningOutcome, s as reservationJournalKey, t as supportsCreditsV2, u as supportsIdempotentCredit, v as toClientUserCredits } from './types-C8hZMe8a.cjs';
4
- export { CREDIT_AMOUNT_MAX, CREDIT_AMOUNT_SCALE, CreditError, CreditErrorCode, CreditErrorCodeType, PaymentEventPayload, RESERVATION_STATUSES, RESERVED_JOURNAL_KEY_PREFIX, ReservationIntegrityView, StoredPaymentEvent, TERMINAL_RESERVATION_STATUSES, assertDirectStatusWriteAllowed, assertHoldPlaced, assertKnownReservationStatus, assertPublicJournalKey, assertRepresentableAmount, assertRepresentableFields, assertRepresentableTierAmount, assertUnkeyedDirectReservation, assertUnreferencedDirectTransaction, assertValidCreditAmount, assertValidIdempotencyKey, assertValidStoredAmount, assertValidStoredAmountRaw, backedBalanceFloor, classifyDatabaseError, commitReservationWithJournal, createIdempotencyConflictError, createInsufficientCreditsError, createInvalidAmountError, createInvalidOperationTypeError, createPaymentRefConflictError, createReservationAlreadyProcessedError, createReservationExpiredError, createReservationNotFoundError, createTransientError, createUnsupportedOperationError, createUserNotFoundError, describePaymentMismatch, getSqlState, hasPlacedHold, isCreditError, isIdempotencyConflictError, isInsufficientCreditsError, isRepresentableAmount, isReservationStatus, isReservedJournalKey, isTerminalReservationStatus, isTransientDatabaseError, isTransientError, isUniqueViolation, isValidCreditAmount, isValidIdempotencyKey, normalizePaymentRef, numericToCents, releaseReservationWithJournal, reserveCreditsForOperation, sameAmount, storedMonthlyLimit, sumAmounts, terminalStatusOf, toCents } from './core/index.cjs';
1
+ import { S as SubscriptionTier, T as TierConfig, a as PortableReservation } from './types-MaViK5tD.cjs';
2
+ export { A as AIProviderType, B as BuiltinTier, g as CreditCheckResult, C as CreditOperationType, b as CreditSource, D as DeductCreditsResult, J as JournalReferenceType, M as MonthlyResetResult, f as PortableJournalEntry, P as PortableTransaction, d as PortableUsageLog, c as PortableUserCredits, R as ReservationStatus, h as ResourceType, g as SDKCreditCheckResult, e as SubscriptionExpiryResult, i as TransactionType, U as UsageHistoryEntry, j as UsageHistoryResponse, c as UserCredits, W as WithCreditsOptions, k as calculateAvailableCredits, t as toDate, l as toPortableTimestamp } from './types-MaViK5tD.cjs';
3
+ export { A as AddCreditsAtomicOptions, a as AddCreditsOutcome, b as AddCreditsV2Input, C as CommitOutcome, c as CreateJournalEntryInput, d as CreateReservationInput, e as CreateTransactionInput, f as CreateUsageLogInput, g as CreditBalanceUpdate, h as CreditPolicyOptions, i as CreditRepositoryFactory, E as ExpireOutcome, j as ExpireReservationV2Options, I as ICreditRepository, k as ICreditRepositoryCreditsV2, l as ICreditRepositoryV2, J as JournalEntryQuery, R as ReleaseOutcome, m as ReservationTransitionOptions, n as ReserveCreditsV2Input, o as ReserveOutcome, T as TerminalReservationStatus, p as TierUpdateInput, U as UsageLogQuery, q as isCreditedOutcome, r as isReservedOutcome, s as isWinningOutcome, t as reservationJournalKey, u as supportsCreditsV2, v as supportsIdempotentCredit, w as toClientUserCredits } from './types-XSjNXO5l.cjs';
4
+ export { CREDIT_AMOUNT_MAX, CREDIT_AMOUNT_SCALE, CreditError, CreditErrorCode, CreditErrorCodeType, PaymentEventPayload, RESERVATION_STATUSES, RESERVED_JOURNAL_KEY_PREFIX, ReservationIntegrityView, StoredPaymentEvent, TERMINAL_RESERVATION_STATUSES, assertDirectStatusWriteAllowed, assertHoldPlaced, assertKnownReservationStatus, assertPublicJournalKey, assertRepresentableAmount, assertRepresentableFields, assertRepresentableTierAmount, assertUnkeyedDirectReservation, assertUnreferencedDirectTransaction, assertValidBalanceTarget, assertValidCreditAmount, assertValidIdempotencyKey, assertValidStoredAmount, assertValidStoredAmountRaw, assertValidTierBalanceInput, attributeExpiredBacking, backedBalanceFloor, cappedBalance, classifyDatabaseError, commitReservationWithJournal, createIdempotencyConflictError, createInsufficientCreditsError, createInvalidAmountError, createInvalidOperationTypeError, createPaymentRefConflictError, createReservationAlreadyProcessedError, createReservationExpiredError, createReservationNotFoundError, createTransientError, createUnsupportedOperationError, createUserNotFoundError, describePaymentMismatch, expiredHoldBackingPatch, getSqlState, hasPlacedHold, isCreditError, isIdempotencyConflictError, isInsufficientCreditsError, isRepresentableAmount, isReservationStatus, isReservedJournalKey, isTerminalReservationStatus, isTransientDatabaseError, isTransientError, isUniqueViolation, isValidCreditAmount, isValidIdempotencyKey, normalizePaymentRef, numericToCents, planBalanceCap, planBalanceTarget, planHoldCommit, releaseReservationWithJournal, reserveCreditsForOperation, sameAmount, settleExpiredHoldBacking, spendableBalance, storedMonthlyLimit, sumAmounts, terminalStatusOf, toCents } from './core/index.cjs';
5
5
  export { D as DeferredExecutor, c as createDeferredExecutor, g as genericDeferred, n as noopDeferred, s as synchronousDeferred } from './deferred-C7rrRfkm.cjs';
6
6
  import { z } from 'zod';
7
- export { InMemoryCreditRepository, addCreditsThroughRepository, commitThroughRepository, createInMemoryCreditRepository, generateId, getNextMonthlyReset, releaseThroughRepository, reserveThroughRepository } from './repository/index.cjs';
7
+ export { BalanceValidationResult, InMemoryCreditRepository, addCreditsThroughRepository, assertValidBalanceUpdate, commitThroughRepository, createInMemoryCreditRepository, generateId, getNextMonthlyReset, releaseThroughRepository, reserveThroughRepository, validateBalanceUpdate } from './repository/index.cjs';
8
8
  export { C as CreditsAuthProviderFactory, a as CreditsUser, I as ICreditsAuthProvider } from './types-OZU3I5OM.cjs';
9
9
  export { ApiKeyCreditsProvider, createApiKeyProvider, verifyAdminApiKey, verifyBearerToken } from './auth/index.cjs';
10
10
  export { CreditsService, DeductCreditsOptions, LowBalanceNotificationCallback, ReserveCreditsOptions, SubscriptionExpiredNotificationCallback, createCreditsService } from './service/index.cjs';
@@ -18,7 +18,9 @@ declare const creditSystemConfigSchema: z.ZodObject<{
18
18
  /**
19
19
  * Operation costs in credits
20
20
  * Keys are operation type names (any non-empty string)
21
- * Values are positive numbers representing credit cost
21
+ * Values are non-negative numbers representing credit cost. 0 marks a free
22
+ * operation: `withCredits` runs it without touching the ledger. Amounts that
23
+ * move credits stay strictly positive, so `reserveCredits` refuses a 0.
22
24
  */
23
25
  operationCosts: z.ZodRecord<z.ZodString, z.ZodNumber>;
24
26
  /**
@@ -59,7 +61,7 @@ declare const creditSystemConfigSchema: z.ZodObject<{
59
61
  */
60
62
  reservationExpiryMs: z.ZodDefault<z.ZodNumber>;
61
63
  /**
62
- * Default credits for new users (free tier)
64
+ * Default credits for new users (free tier). 0 means no signup grant.
63
65
  */
64
66
  defaultFreeCredits: z.ZodDefault<z.ZodNumber>;
65
67
  /**
package/dist/index.d.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { S as SubscriptionTier, T as TierConfig, a as PortableReservation } from './types-DJ8yqaVr.js';
2
- export { A as AIProviderType, B as BuiltinTier, g as CreditCheckResult, C as CreditOperationType, b as CreditSource, D as DeductCreditsResult, J as JournalReferenceType, M as MonthlyResetResult, f as PortableJournalEntry, P as PortableTransaction, d as PortableUsageLog, c as PortableUserCredits, R as ReservationStatus, h as ResourceType, g as SDKCreditCheckResult, e as SubscriptionExpiryResult, i as TransactionType, U as UsageHistoryEntry, j as UsageHistoryResponse, c as UserCredits, W as WithCreditsOptions, k as calculateAvailableCredits, t as toDate, l as toPortableTimestamp } from './types-DJ8yqaVr.js';
3
- export { A as AddCreditsAtomicOptions, a as AddCreditsOutcome, b as AddCreditsV2Input, C as CommitOutcome, c as CreateJournalEntryInput, d as CreateReservationInput, e as CreateTransactionInput, f as CreateUsageLogInput, g as CreditBalanceUpdate, h as CreditRepositoryFactory, E as ExpireOutcome, i as ExpireReservationV2Options, I as ICreditRepository, j as ICreditRepositoryCreditsV2, k as ICreditRepositoryV2, J as JournalEntryQuery, R as ReleaseOutcome, l as ReservationTransitionOptions, m as ReserveCreditsV2Input, n as ReserveOutcome, T as TerminalReservationStatus, o as TierUpdateInput, U as UsageLogQuery, p as isCreditedOutcome, q as isReservedOutcome, r as isWinningOutcome, s as reservationJournalKey, t as supportsCreditsV2, u as supportsIdempotentCredit, v as toClientUserCredits } from './types-BmG5Omzj.js';
4
- export { CREDIT_AMOUNT_MAX, CREDIT_AMOUNT_SCALE, CreditError, CreditErrorCode, CreditErrorCodeType, PaymentEventPayload, RESERVATION_STATUSES, RESERVED_JOURNAL_KEY_PREFIX, ReservationIntegrityView, StoredPaymentEvent, TERMINAL_RESERVATION_STATUSES, assertDirectStatusWriteAllowed, assertHoldPlaced, assertKnownReservationStatus, assertPublicJournalKey, assertRepresentableAmount, assertRepresentableFields, assertRepresentableTierAmount, assertUnkeyedDirectReservation, assertUnreferencedDirectTransaction, assertValidCreditAmount, assertValidIdempotencyKey, assertValidStoredAmount, assertValidStoredAmountRaw, backedBalanceFloor, classifyDatabaseError, commitReservationWithJournal, createIdempotencyConflictError, createInsufficientCreditsError, createInvalidAmountError, createInvalidOperationTypeError, createPaymentRefConflictError, createReservationAlreadyProcessedError, createReservationExpiredError, createReservationNotFoundError, createTransientError, createUnsupportedOperationError, createUserNotFoundError, describePaymentMismatch, getSqlState, hasPlacedHold, isCreditError, isIdempotencyConflictError, isInsufficientCreditsError, isRepresentableAmount, isReservationStatus, isReservedJournalKey, isTerminalReservationStatus, isTransientDatabaseError, isTransientError, isUniqueViolation, isValidCreditAmount, isValidIdempotencyKey, normalizePaymentRef, numericToCents, releaseReservationWithJournal, reserveCreditsForOperation, sameAmount, storedMonthlyLimit, sumAmounts, terminalStatusOf, toCents } from './core/index.js';
1
+ import { S as SubscriptionTier, T as TierConfig, a as PortableReservation } from './types-MaViK5tD.js';
2
+ export { A as AIProviderType, B as BuiltinTier, g as CreditCheckResult, C as CreditOperationType, b as CreditSource, D as DeductCreditsResult, J as JournalReferenceType, M as MonthlyResetResult, f as PortableJournalEntry, P as PortableTransaction, d as PortableUsageLog, c as PortableUserCredits, R as ReservationStatus, h as ResourceType, g as SDKCreditCheckResult, e as SubscriptionExpiryResult, i as TransactionType, U as UsageHistoryEntry, j as UsageHistoryResponse, c as UserCredits, W as WithCreditsOptions, k as calculateAvailableCredits, t as toDate, l as toPortableTimestamp } from './types-MaViK5tD.js';
3
+ export { A as AddCreditsAtomicOptions, a as AddCreditsOutcome, b as AddCreditsV2Input, C as CommitOutcome, c as CreateJournalEntryInput, d as CreateReservationInput, e as CreateTransactionInput, f as CreateUsageLogInput, g as CreditBalanceUpdate, h as CreditPolicyOptions, i as CreditRepositoryFactory, E as ExpireOutcome, j as ExpireReservationV2Options, I as ICreditRepository, k as ICreditRepositoryCreditsV2, l as ICreditRepositoryV2, J as JournalEntryQuery, R as ReleaseOutcome, m as ReservationTransitionOptions, n as ReserveCreditsV2Input, o as ReserveOutcome, T as TerminalReservationStatus, p as TierUpdateInput, U as UsageLogQuery, q as isCreditedOutcome, r as isReservedOutcome, s as isWinningOutcome, t as reservationJournalKey, u as supportsCreditsV2, v as supportsIdempotentCredit, w as toClientUserCredits } from './types-3c-IAwZI.js';
4
+ export { CREDIT_AMOUNT_MAX, CREDIT_AMOUNT_SCALE, CreditError, CreditErrorCode, CreditErrorCodeType, PaymentEventPayload, RESERVATION_STATUSES, RESERVED_JOURNAL_KEY_PREFIX, ReservationIntegrityView, StoredPaymentEvent, TERMINAL_RESERVATION_STATUSES, assertDirectStatusWriteAllowed, assertHoldPlaced, assertKnownReservationStatus, assertPublicJournalKey, assertRepresentableAmount, assertRepresentableFields, assertRepresentableTierAmount, assertUnkeyedDirectReservation, assertUnreferencedDirectTransaction, assertValidBalanceTarget, assertValidCreditAmount, assertValidIdempotencyKey, assertValidStoredAmount, assertValidStoredAmountRaw, assertValidTierBalanceInput, attributeExpiredBacking, backedBalanceFloor, cappedBalance, classifyDatabaseError, commitReservationWithJournal, createIdempotencyConflictError, createInsufficientCreditsError, createInvalidAmountError, createInvalidOperationTypeError, createPaymentRefConflictError, createReservationAlreadyProcessedError, createReservationExpiredError, createReservationNotFoundError, createTransientError, createUnsupportedOperationError, createUserNotFoundError, describePaymentMismatch, expiredHoldBackingPatch, getSqlState, hasPlacedHold, isCreditError, isIdempotencyConflictError, isInsufficientCreditsError, isRepresentableAmount, isReservationStatus, isReservedJournalKey, isTerminalReservationStatus, isTransientDatabaseError, isTransientError, isUniqueViolation, isValidCreditAmount, isValidIdempotencyKey, normalizePaymentRef, numericToCents, planBalanceCap, planBalanceTarget, planHoldCommit, releaseReservationWithJournal, reserveCreditsForOperation, sameAmount, settleExpiredHoldBacking, spendableBalance, storedMonthlyLimit, sumAmounts, terminalStatusOf, toCents } from './core/index.js';
5
5
  export { D as DeferredExecutor, c as createDeferredExecutor, g as genericDeferred, n as noopDeferred, s as synchronousDeferred } from './deferred-C7rrRfkm.js';
6
6
  import { z } from 'zod';
7
- export { InMemoryCreditRepository, addCreditsThroughRepository, commitThroughRepository, createInMemoryCreditRepository, generateId, getNextMonthlyReset, releaseThroughRepository, reserveThroughRepository } from './repository/index.js';
7
+ export { BalanceValidationResult, InMemoryCreditRepository, addCreditsThroughRepository, assertValidBalanceUpdate, commitThroughRepository, createInMemoryCreditRepository, generateId, getNextMonthlyReset, releaseThroughRepository, reserveThroughRepository, validateBalanceUpdate } from './repository/index.js';
8
8
  export { C as CreditsAuthProviderFactory, a as CreditsUser, I as ICreditsAuthProvider } from './types-OZU3I5OM.js';
9
9
  export { ApiKeyCreditsProvider, createApiKeyProvider, verifyAdminApiKey, verifyBearerToken } from './auth/index.js';
10
10
  export { CreditsService, DeductCreditsOptions, LowBalanceNotificationCallback, ReserveCreditsOptions, SubscriptionExpiredNotificationCallback, createCreditsService } from './service/index.js';
@@ -18,7 +18,9 @@ declare const creditSystemConfigSchema: z.ZodObject<{
18
18
  /**
19
19
  * Operation costs in credits
20
20
  * Keys are operation type names (any non-empty string)
21
- * Values are positive numbers representing credit cost
21
+ * Values are non-negative numbers representing credit cost. 0 marks a free
22
+ * operation: `withCredits` runs it without touching the ledger. Amounts that
23
+ * move credits stay strictly positive, so `reserveCredits` refuses a 0.
22
24
  */
23
25
  operationCosts: z.ZodRecord<z.ZodString, z.ZodNumber>;
24
26
  /**
@@ -59,7 +61,7 @@ declare const creditSystemConfigSchema: z.ZodObject<{
59
61
  */
60
62
  reservationExpiryMs: z.ZodDefault<z.ZodNumber>;
61
63
  /**
62
- * Default credits for new users (free tier)
64
+ * Default credits for new users (free tier). 0 means no signup grant.
63
65
  */
64
66
  defaultFreeCredits: z.ZodDefault<z.ZodNumber>;
65
67
  /**
package/dist/index.js CHANGED
@@ -219,6 +219,86 @@ function backedBalanceFloor(reserved, bonusCredits) {
219
219
  assertRepresentableAmount(floor, "backedBalanceFloor", { reserved, bonusCredits });
220
220
  return floor;
221
221
  }
222
+ function cappedBalance(balance, cap, reserved, bonusCredits) {
223
+ return Math.min(balance, Math.max(cap, backedBalanceFloor(reserved, bonusCredits)));
224
+ }
225
+ function spendableBalance(balance, expiredHoldBacking = 0) {
226
+ return sumAmounts(balance, -Math.min(expiredHoldBacking, balance));
227
+ }
228
+ function planBalanceTarget(target, reserved, bonusCredits) {
229
+ const balance = Math.max(target, backedBalanceFloor(reserved, bonusCredits));
230
+ return { balance, expiredHoldBacking: sumAmounts(balance, -target) };
231
+ }
232
+ function planBalanceCap(balance, cap, reserved, bonusCredits, expiredHoldBacking = 0) {
233
+ const spendable = spendableBalance(balance, expiredHoldBacking);
234
+ const target = Math.min(spendable, cap);
235
+ const kept = Math.max(
236
+ sumAmounts(balance, -spendable),
237
+ sumAmounts(backedBalanceFloor(reserved, bonusCredits), -target)
238
+ );
239
+ const next = Math.min(balance, sumAmounts(target, kept));
240
+ return { balance: next, expiredHoldBacking: Math.max(0, sumAmounts(next, -target)) };
241
+ }
242
+ function settleExpiredHoldBacking(balance, expiredHoldBacking, holdBacking) {
243
+ const held = Math.min(expiredHoldBacking, balance);
244
+ const used = Math.min(held, holdBacking);
245
+ return { used, remaining: sumAmounts(held, -used) };
246
+ }
247
+ function planHoldCommit(balance, bonusCredits, expiredHoldBacking, holdBacking, amount) {
248
+ const backing = settleExpiredHoldBacking(balance, expiredHoldBacking, holdBacking);
249
+ const rest = sumAmounts(amount, -backing.used);
250
+ const fromSpendable = Math.min(spendableBalance(balance, expiredHoldBacking), rest);
251
+ const fromBonus = sumAmounts(rest, -fromSpendable);
252
+ return {
253
+ covered: fromBonus <= bonusCredits,
254
+ balance: sumAmounts(balance, -backing.used, -fromSpendable),
255
+ bonusCredits: sumAmounts(bonusCredits, -fromBonus),
256
+ expiredHoldBacking: backing.remaining
257
+ };
258
+ }
259
+ function attributeExpiredBacking(holds, total) {
260
+ const placed = holds.filter((hold) => hold.holdPlacedAt).sort(
261
+ (a, b) => a.holdPlacedAt === b.holdPlacedAt ? a.id < b.id ? -1 : 1 : a.holdPlacedAt < b.holdPlacedAt ? -1 : 1
262
+ );
263
+ const shares = /* @__PURE__ */ new Map();
264
+ let left = total;
265
+ for (const hold of placed) {
266
+ const kept = Math.min(hold.expiredBacking ?? 0, hold.amount, left);
267
+ shares.set(hold.id, kept);
268
+ left = sumAmounts(left, -kept);
269
+ }
270
+ for (const hold of placed) {
271
+ const share = shares.get(hold.id);
272
+ const added = Math.min(sumAmounts(hold.amount, -share), left);
273
+ shares.set(hold.id, sumAmounts(share, added));
274
+ left = sumAmounts(left, -added);
275
+ }
276
+ return shares;
277
+ }
278
+ function expiredHoldBackingPatch(next, stored = 0) {
279
+ return next > 0 || stored > 0 ? { expiredHoldBacking: next } : {};
280
+ }
281
+ function assertValidBalanceTarget(value, field, context) {
282
+ assertRepresentableAmount(value, field, context);
283
+ if (value < 0) {
284
+ throw new CreditError(
285
+ `${field} cannot be negative (got ${value})`,
286
+ CreditErrorCode.INVALID_AMOUNT,
287
+ { field, value, ...context }
288
+ );
289
+ }
290
+ }
291
+ function assertValidTierBalanceInput(input, context) {
292
+ if (input.balanceCap === void 0) return;
293
+ if (input.balance !== void 0) {
294
+ throw new CreditError(
295
+ "A tier update cannot set both balance and balanceCap",
296
+ CreditErrorCode.INVALID_AMOUNT,
297
+ { balance: input.balance, balanceCap: input.balanceCap, ...context }
298
+ );
299
+ }
300
+ assertValidBalanceTarget(input.balanceCap, "balanceCap", context);
301
+ }
222
302
  function assertRepresentableTierAmount(value, field, context) {
223
303
  if (value === Number.POSITIVE_INFINITY) return;
224
304
  assertRepresentableAmount(value, field, context);
@@ -599,11 +679,13 @@ var creditSystemConfigSchema = z.object({
599
679
  /**
600
680
  * Operation costs in credits
601
681
  * Keys are operation type names (any non-empty string)
602
- * Values are positive numbers representing credit cost
682
+ * Values are non-negative numbers representing credit cost. 0 marks a free
683
+ * operation: `withCredits` runs it without touching the ledger. Amounts that
684
+ * move credits stay strictly positive, so `reserveCredits` refuses a 0.
603
685
  */
604
686
  operationCosts: z.record(
605
687
  z.string().min(1),
606
- z.number().positive()
688
+ z.number().nonnegative()
607
689
  ),
608
690
  /**
609
691
  * Display labels for operation types (English canonical labels).
@@ -625,9 +707,9 @@ var creditSystemConfigSchema = z.object({
625
707
  */
626
708
  reservationExpiryMs: z.number().positive().default(5 * 60 * 1e3),
627
709
  /**
628
- * Default credits for new users (free tier)
710
+ * Default credits for new users (free tier). 0 means no signup grant.
629
711
  */
630
- defaultFreeCredits: z.number().positive().default(25),
712
+ defaultFreeCredits: z.number().min(0).default(25),
631
713
  /**
632
714
  * Grace period in days after subscription expires before downgrade
633
715
  */
@@ -927,6 +1009,8 @@ function toClientUserCredits(credits) {
927
1009
  monthlyUsed: credits.monthlyUsed,
928
1010
  monthlyResetAt: toISOString(credits.monthlyResetAt),
929
1011
  subscriptionExpiresAt: credits.subscriptionExpiresAt ? toISOString(credits.subscriptionExpiresAt) : null,
1012
+ ...credits.expiredHoldBacking !== void 0 ? { expiredHoldBacking: credits.expiredHoldBacking } : {},
1013
+ ...credits.signupGrant !== void 0 ? { signupGrant: credits.signupGrant } : {},
930
1014
  createdAt: toISOString(credits.createdAt),
931
1015
  updatedAt: toISOString(credits.updatedAt)
932
1016
  };
@@ -1158,11 +1242,36 @@ function toDate2(value) {
1158
1242
  return /* @__PURE__ */ new Date();
1159
1243
  }
1160
1244
  function getNextMonthlyReset(from = /* @__PURE__ */ new Date()) {
1161
- const next = new Date(from);
1162
- next.setMonth(next.getMonth() + 1);
1163
- next.setDate(1);
1164
- next.setHours(0, 0, 0, 0);
1165
- return next;
1245
+ return new Date(from.getFullYear(), from.getMonth() + 1, 1);
1246
+ }
1247
+ function validateBalanceUpdate(current, updates) {
1248
+ const next = (base, absolute, increment) => sumAmounts(absolute ?? base, increment ?? 0);
1249
+ const balance = next(current.balance, updates.balance, updates.balanceIncrement);
1250
+ const bonusCredits = next(
1251
+ current.bonusCredits ?? 0,
1252
+ updates.bonusCredits,
1253
+ updates.bonusCreditsIncrement
1254
+ );
1255
+ const reserved = next(current.reserved ?? 0, updates.reserved, updates.reservedIncrement);
1256
+ const errors = [];
1257
+ if (balance < 0) errors.push(`Balance cannot be negative: ${balance}`);
1258
+ const marked = updates.balance === void 0 ? current.expiredHoldBacking ?? 0 : 0;
1259
+ if ((updates.balanceIncrement ?? 0) < 0 && balance >= 0 && balance < marked) {
1260
+ errors.push(`Balance (${balance}) cannot go below expired hold backing (${marked})`);
1261
+ }
1262
+ if (bonusCredits < 0) errors.push(`BonusCredits cannot be negative: ${bonusCredits}`);
1263
+ if (reserved < 0) errors.push(`Reserved cannot be negative: ${reserved}`);
1264
+ const covering = sumAmounts(balance, bonusCredits);
1265
+ if (reserved > covering) {
1266
+ errors.push(`Reserved (${reserved}) exceeds balance + bonusCredits (${covering})`);
1267
+ }
1268
+ return { valid: errors.length === 0, errors };
1269
+ }
1270
+ function assertValidBalanceUpdate(current, updates) {
1271
+ const result = validateBalanceUpdate(current, updates);
1272
+ if (!result.valid) {
1273
+ throw new Error(`Invalid balance update: ${result.errors.join("; ")}`);
1274
+ }
1166
1275
  }
1167
1276
 
1168
1277
  // src/repository/memory/snapshot.ts
@@ -1342,6 +1451,22 @@ function assertTrustworthy(reservation, transition) {
1342
1451
  assertLockedAmount(reservation, transition);
1343
1452
  assertHoldPlaced(reservation, transition);
1344
1453
  }
1454
+ function openHolds(store, userId) {
1455
+ return [...store.reservations.get(userId)?.values() ?? []].filter(
1456
+ (reservation) => reservation.status === "reserved"
1457
+ );
1458
+ }
1459
+ function writeExpiredHoldBacking(store, credits, next) {
1460
+ const stored = credits.expiredHoldBacking;
1461
+ if (next <= 0 && !stored) return;
1462
+ const holds = openHolds(store, credits.userId);
1463
+ const shares = attributeExpiredBacking(holds, next);
1464
+ for (const hold of holds) {
1465
+ const share = shares.get(hold.id) ?? 0;
1466
+ if (share !== (hold.expiredBacking ?? 0)) hold.expiredBacking = share;
1467
+ }
1468
+ Object.assign(credits, expiredHoldBackingPatch(sumAmounts(...shares.values()), stored));
1469
+ }
1345
1470
  function getReservation(store, userId, reservationId) {
1346
1471
  return store.reservations.get(userId)?.get(reservationId);
1347
1472
  }
@@ -1439,16 +1564,22 @@ async function commitReservationV2(store, userId, reservationId, options) {
1439
1564
  const amount = reservation.amount;
1440
1565
  await yieldPoint(store);
1441
1566
  assertReservedCoversHold(credits, userId, reservationId, amount);
1442
- if (sumAmounts(credits.balance, credits.bonusCredits) < amount) {
1567
+ const paid = planHoldCommit(
1568
+ credits.balance,
1569
+ credits.bonusCredits,
1570
+ credits.expiredHoldBacking ?? 0,
1571
+ reservation.expiredBacking ?? 0,
1572
+ amount
1573
+ );
1574
+ if (!paid.covered) {
1443
1575
  throw new CreditError(
1444
1576
  `Insufficient credits to commit reservation ${reservationId}`,
1445
1577
  CreditErrorCode.INSUFFICIENT_CREDITS,
1446
1578
  { userId, reservationId, required: amount }
1447
1579
  );
1448
1580
  }
1449
- const fromBalance = Math.min(credits.balance, amount);
1450
- const nextBalance = sumAmounts(credits.balance, -fromBalance);
1451
- const nextBonusCredits = sumAmounts(credits.bonusCredits, -sumAmounts(amount, -fromBalance));
1581
+ const nextBalance = paid.balance;
1582
+ const nextBonusCredits = paid.bonusCredits;
1452
1583
  const balanceAfter = sumAmounts(nextBalance, nextBonusCredits);
1453
1584
  const journal = {
1454
1585
  userId,
@@ -1482,6 +1613,10 @@ async function commitReservationV2(store, userId, reservationId, options) {
1482
1613
  credits.bonusCredits = nextBonusCredits;
1483
1614
  credits.reserved = nextReserved;
1484
1615
  credits.monthlyUsed = nextMonthlyUsed;
1616
+ Object.assign(
1617
+ credits,
1618
+ expiredHoldBackingPatch(paid.expiredHoldBacking, credits.expiredHoldBacking)
1619
+ );
1485
1620
  credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1486
1621
  const journalEntryId = commitTransitionJournal(store, journal, existingEntryId);
1487
1622
  return {
@@ -1561,19 +1696,26 @@ function finishUnspent(store, reservation, transition) {
1561
1696
  reservation.id,
1562
1697
  reservation.amount
1563
1698
  );
1699
+ const backing = settleExpiredHoldBacking(
1700
+ credits.balance,
1701
+ credits.expiredHoldBacking ?? 0,
1702
+ reservation.expiredBacking ?? 0
1703
+ );
1704
+ const nextBalance = sumAmounts(credits.balance, -backing.used);
1564
1705
  const journal = {
1565
1706
  userId: reservation.userId,
1566
1707
  operation: `${transition.transition}Reservation`,
1567
1708
  entryType: "credit",
1568
1709
  amount: 0,
1569
- balanceAfter: sumAmounts(credits.balance, credits.bonusCredits),
1710
+ balanceAfter: sumAmounts(nextBalance, credits.bonusCredits),
1570
1711
  source: transition.source,
1571
1712
  reservationId: reservation.id,
1572
1713
  description: transition.options?.description ?? `${transition.verb} ${reservation.amount} reserved credits for ${getOperationLabel(reservation.operationType)}`,
1573
1714
  metadata: {
1574
1715
  ...transition.options?.metadata,
1575
1716
  operationType: reservation.operationType,
1576
- amount: reservation.amount
1717
+ amount: reservation.amount,
1718
+ ...backing.used > 0 ? { expiredCreditsRemoved: backing.used } : {}
1577
1719
  },
1578
1720
  idempotencyKey: reservationJournalKey(reservation.id, transition.transition)
1579
1721
  };
@@ -1586,6 +1728,8 @@ function finishUnspent(store, reservation, transition) {
1586
1728
  reservation.status = transition.status;
1587
1729
  reservation.completedAt = (/* @__PURE__ */ new Date()).toISOString();
1588
1730
  credits.reserved = nextReserved;
1731
+ credits.balance = nextBalance;
1732
+ Object.assign(credits, expiredHoldBackingPatch(backing.remaining, credits.expiredHoldBacking));
1589
1733
  credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1590
1734
  return commitTransitionJournal(store, journal, existingEntryId);
1591
1735
  }
@@ -1596,6 +1740,10 @@ function isoOf(value) {
1596
1740
  }
1597
1741
  var InMemoryCreditRepository = class {
1598
1742
  store = new MemoryStore();
1743
+ options;
1744
+ constructor(options = {}) {
1745
+ this.options = options;
1746
+ }
1599
1747
  // ==================== User Credits ====================
1600
1748
  /**
1601
1749
  * A copy, never the live record.
@@ -1612,7 +1760,8 @@ var InMemoryCreditRepository = class {
1612
1760
  return copyRecord(credits) ?? null;
1613
1761
  }
1614
1762
  async initializeUserCredits(userId, tier, initialBalance) {
1615
- return copyRecord(this.seedUser(userId, tier, initialBalance));
1763
+ const existing = this.store.users.get(userId);
1764
+ return copyRecord(existing ?? this.seedUser(userId, tier, initialBalance));
1616
1765
  }
1617
1766
  /**
1618
1767
  * The synchronous body of `initializeUserCredits`.
@@ -1663,8 +1812,11 @@ var InMemoryCreditRepository = class {
1663
1812
  if (!credits) {
1664
1813
  throw new Error(`User ${userId} not found`);
1665
1814
  }
1815
+ assertValidBalanceUpdate(credits, updates);
1666
1816
  const next = { ...credits };
1667
- if (updates.balance !== void 0) next.balance = updates.balance;
1817
+ if (updates.balance !== void 0) {
1818
+ next.balance = updates.balance;
1819
+ }
1668
1820
  if (updates.bonusCredits !== void 0) next.bonusCredits = updates.bonusCredits;
1669
1821
  if (updates.reserved !== void 0) next.reserved = updates.reserved;
1670
1822
  if (updates.tier !== void 0) next.tier = updates.tier;
@@ -1701,6 +1853,9 @@ var InMemoryCreditRepository = class {
1701
1853
  userId,
1702
1854
  operation: "updateUserCredits"
1703
1855
  });
1856
+ if ((credits.expiredHoldBacking ?? 0) > 0 && updates.balance !== void 0) {
1857
+ writeExpiredHoldBacking(this.store, next, 0);
1858
+ }
1704
1859
  next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1705
1860
  this.store.users.set(userId, next);
1706
1861
  }
@@ -1709,17 +1864,28 @@ var InMemoryCreditRepository = class {
1709
1864
  { monthlyLimit: input.monthlyLimit, balance: input.balance, monthlyUsed: input.monthlyUsed },
1710
1865
  { userId, operation: "updateUserTier" }
1711
1866
  );
1867
+ assertValidTierBalanceInput(input, { userId, operation: "updateUserTier" });
1712
1868
  const credits = this.store.users.get(userId);
1713
1869
  if (!credits) {
1714
1870
  throw new Error(`User ${userId} not found`);
1715
1871
  }
1872
+ if (input.balanceCap !== void 0 && !isFreeTier(credits.tier)) {
1873
+ const capped = planBalanceCap(
1874
+ credits.balance,
1875
+ input.balanceCap,
1876
+ credits.reserved,
1877
+ credits.bonusCredits,
1878
+ credits.expiredHoldBacking
1879
+ );
1880
+ credits.balance = capped.balance;
1881
+ writeExpiredHoldBacking(this.store, credits, capped.expiredHoldBacking);
1882
+ }
1716
1883
  credits.tier = input.tier;
1717
1884
  credits.monthlyLimit = input.monthlyLimit;
1718
1885
  if (input.balance !== void 0) {
1719
- credits.balance = Math.max(
1720
- input.balance,
1721
- backedBalanceFloor(credits.reserved, credits.bonusCredits)
1722
- );
1886
+ const planned = planBalanceTarget(input.balance, credits.reserved, credits.bonusCredits);
1887
+ credits.balance = planned.balance;
1888
+ writeExpiredHoldBacking(this.store, credits, planned.expiredHoldBacking);
1723
1889
  }
1724
1890
  if (input.monthlyUsed !== void 0) credits.monthlyUsed = input.monthlyUsed;
1725
1891
  if (input.subscriptionExpiresAt !== void 0) {
@@ -1940,13 +2106,17 @@ var InMemoryCreditRepository = class {
1940
2106
  if (!credits) {
1941
2107
  throw new Error(`User credits not found for userId: ${userId}`);
1942
2108
  }
1943
- const available = sumAmounts(credits.balance, credits.bonusCredits, -credits.reserved);
2109
+ const spendable = spendableBalance(credits.balance, credits.expiredHoldBacking);
2110
+ const available = Math.min(
2111
+ sumAmounts(credits.balance, credits.bonusCredits, -credits.reserved),
2112
+ sumAmounts(spendable, credits.bonusCredits)
2113
+ );
1944
2114
  if (available < amount) {
1945
2115
  throw new Error(
1946
2116
  `Insufficient credits. Available: ${available}, requested: ${amount}`
1947
2117
  );
1948
2118
  }
1949
- const balanceDeduction = Math.min(credits.balance, amount);
2119
+ const balanceDeduction = Math.min(spendable, amount);
1950
2120
  const bonusDeduction = sumAmounts(amount, -balanceDeduction);
1951
2121
  const previousBalance = sumAmounts(credits.balance, credits.bonusCredits);
1952
2122
  const nextBalance = sumAmounts(credits.balance, -balanceDeduction);
@@ -2183,7 +2353,11 @@ var InMemoryCreditRepository = class {
2183
2353
  return { expiredCount, creditsReleased, errors };
2184
2354
  }
2185
2355
  // ==================== Atomic Monthly Reset ====================
2186
- async atomicMonthlyReset(userId, tier, expectedResetAt) {
2356
+ /**
2357
+ * @param _tier - Not trusted. The caller read it earlier and the account may
2358
+ * have changed tier since, so the reset uses the tier stored on the account.
2359
+ */
2360
+ async atomicMonthlyReset(userId, _tier, expectedResetAt) {
2187
2361
  const credits = this.store.users.get(userId);
2188
2362
  if (!credits) {
2189
2363
  throw new Error(`User ${userId} not found`);
@@ -2193,17 +2367,41 @@ var InMemoryCreditRepository = class {
2193
2367
  if (currentResetAt !== expected) {
2194
2368
  return { wasReset: false, credits: { ...credits } };
2195
2369
  }
2196
- assertRepresentableTierAmount(getConfigMonthlyLimit(tier), "monthlyLimit", { userId, tier });
2370
+ const tier = credits.tier;
2371
+ const spendable = spendableBalance(credits.balance, credits.expiredHoldBacking);
2372
+ const decision = this.options.resolveMonthlyReset?.({ tier, balance: spendable });
2373
+ if (decision === null) {
2374
+ return { wasReset: false, credits: { ...credits } };
2375
+ }
2376
+ if (decision) {
2377
+ assertValidBalanceTarget(decision.balance, "resolveMonthlyReset balance", { userId, tier });
2378
+ if (decision.monthlyLimit !== void 0) {
2379
+ assertValidBalanceTarget(decision.monthlyLimit, "resolveMonthlyReset monthlyLimit", {
2380
+ userId,
2381
+ tier
2382
+ });
2383
+ }
2384
+ } else {
2385
+ assertRepresentableTierAmount(getConfigMonthlyLimit(tier), "monthlyLimit", { userId, tier });
2386
+ }
2197
2387
  const nextReset = getNextMonthlyReset();
2198
- const target = monthlyResetBalance(tier);
2199
- const floor = backedBalanceFloor(credits.reserved, credits.bonusCredits);
2200
2388
  const previousBalance = credits.balance;
2201
- const newBalance = target.kind === "atLeast" ? Math.max(credits.balance, target.value, floor) : Math.max(target.value, floor);
2389
+ let target;
2390
+ if (decision) {
2391
+ target = decision.balance;
2392
+ } else {
2393
+ const configured = monthlyResetBalance(tier);
2394
+ target = configured.kind === "atLeast" ? Math.max(spendable, configured.value) : configured.value;
2395
+ }
2396
+ const planned = planBalanceTarget(target, credits.reserved, credits.bonusCredits);
2397
+ const newBalance = planned.balance;
2202
2398
  const change = sumAmounts(newBalance, -previousBalance);
2203
2399
  const balanceAfter = sumAmounts(newBalance, credits.bonusCredits);
2204
2400
  assertRepresentableAmount(Math.abs(change), "journal amount", { userId });
2205
2401
  assertRepresentableAmount(balanceAfter, "journal balanceAfter", { userId });
2206
2402
  credits.balance = newBalance;
2403
+ writeExpiredHoldBacking(this.store, credits, planned.expiredHoldBacking);
2404
+ if (decision?.monthlyLimit !== void 0) credits.monthlyLimit = decision.monthlyLimit;
2207
2405
  credits.monthlyUsed = 0;
2208
2406
  credits.monthlyResetAt = nextReset.toISOString();
2209
2407
  credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -2257,23 +2455,34 @@ var InMemoryCreditRepository = class {
2257
2455
  };
2258
2456
  }
2259
2457
  const defaultTier = getDefaultTier();
2260
- const configured = getConfigMonthlyLimit(defaultTier);
2261
- assertRepresentableTierAmount(configured, "monthlyLimit", {
2262
- userId,
2263
- tier: defaultTier
2264
- });
2265
- const limit = storedMonthlyLimit(configured);
2458
+ let limit;
2459
+ if (this.options.getDowngradeBalanceCap) {
2460
+ limit = this.options.getDowngradeBalanceCap();
2461
+ assertValidBalanceTarget(limit, "getDowngradeBalanceCap", { userId, tier: defaultTier });
2462
+ } else {
2463
+ const configured = getConfigMonthlyLimit(defaultTier);
2464
+ assertRepresentableTierAmount(configured, "monthlyLimit", {
2465
+ userId,
2466
+ tier: defaultTier
2467
+ });
2468
+ limit = storedMonthlyLimit(configured);
2469
+ }
2266
2470
  const previousTier = credits.tier;
2267
2471
  const previousBalance = credits.balance;
2268
- const newBalance = Math.max(
2269
- Math.min(credits.balance, limit),
2270
- backedBalanceFloor(credits.reserved, credits.bonusCredits)
2472
+ const planned = planBalanceCap(
2473
+ credits.balance,
2474
+ limit,
2475
+ credits.reserved,
2476
+ credits.bonusCredits,
2477
+ credits.expiredHoldBacking
2271
2478
  );
2479
+ const newBalance = planned.balance;
2272
2480
  const balanceAfter = sumAmounts(newBalance, credits.bonusCredits);
2273
2481
  assertRepresentableAmount(balanceAfter, "journal balanceAfter", { userId });
2274
2482
  credits.tier = defaultTier;
2275
2483
  credits.monthlyLimit = limit;
2276
2484
  credits.balance = newBalance;
2485
+ writeExpiredHoldBacking(this.store, credits, planned.expiredHoldBacking);
2277
2486
  credits.subscriptionExpiresAt = null;
2278
2487
  credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
2279
2488
  this.store.users.set(userId, credits);
@@ -2331,8 +2540,8 @@ var InMemoryCreditRepository = class {
2331
2540
  return copyRecords(Array.from(userReservations.values()));
2332
2541
  }
2333
2542
  };
2334
- function createInMemoryCreditRepository() {
2335
- return new InMemoryCreditRepository();
2543
+ function createInMemoryCreditRepository(options = {}) {
2544
+ return new InMemoryCreditRepository(options);
2336
2545
  }
2337
2546
 
2338
2547
  // src/auth/api-key.ts
@@ -2495,9 +2704,11 @@ var CreditsService = class {
2495
2704
  source: "monthly_reset",
2496
2705
  referenceId: `reset-${Date.now()}`,
2497
2706
  referenceType: "reset",
2498
- description: `Monthly credit reset for ${data.tier} tier.`,
2707
+ // The tier the reset ran on, which a concurrent downgrade may have
2708
+ // changed since `data` was read.
2709
+ description: `Monthly credit reset for ${resetResult.credits.tier} tier.`,
2499
2710
  metadata: {
2500
- tier: data.tier,
2711
+ tier: resetResult.credits.tier,
2501
2712
  previousBalance: data.balance,
2502
2713
  newBalance: resetResult.credits.balance
2503
2714
  }
@@ -2936,9 +3147,11 @@ function createGenericAdapter(config) {
2936
3147
  return handler(user, data, dummyReservation);
2937
3148
  }
2938
3149
  let reservation;
3150
+ let isFree = false;
2939
3151
  try {
2940
3152
  const cost = options.customCost ?? getOperationCost2(options.operationType);
2941
- reservation = await reserveCreditsForOperation(
3153
+ isFree = options.customCost === void 0 && cost === 0;
3154
+ reservation = isFree ? { ...createDummyReservation(user.id, options.operationType), id: "free-operation" } : await reserveCreditsForOperation(
2942
3155
  repository,
2943
3156
  user.id,
2944
3157
  cost,
@@ -2961,7 +3174,7 @@ function createGenericAdapter(config) {
2961
3174
  try {
2962
3175
  const result = await handler(user, data, reservation);
2963
3176
  if (result.success) {
2964
- await commitReservationWithJournal(repository, user.id, reservation.id);
3177
+ if (!isFree) await commitReservationWithJournal(repository, user.id, reservation.id);
2965
3178
  logUsage({
2966
3179
  userId: user.id,
2967
3180
  operationType: options.operationType,
@@ -2972,7 +3185,7 @@ function createGenericAdapter(config) {
2972
3185
  requestId
2973
3186
  });
2974
3187
  } else {
2975
- await releaseReservationWithJournal(repository, user.id, reservation.id);
3188
+ if (!isFree) await releaseReservationWithJournal(repository, user.id, reservation.id);
2976
3189
  logUsage({
2977
3190
  userId: user.id,
2978
3191
  operationType: options.operationType,
@@ -2987,7 +3200,7 @@ function createGenericAdapter(config) {
2987
3200
  return result;
2988
3201
  } catch (error) {
2989
3202
  const message = error instanceof Error ? error.message : "An unexpected error occurred";
2990
- await releaseReservationWithJournal(repository, user.id, reservation.id);
3203
+ if (!isFree) await releaseReservationWithJournal(repository, user.id, reservation.id);
2991
3204
  logUsage({
2992
3205
  userId: user.id,
2993
3206
  operationType: options.operationType,
@@ -3781,12 +3994,17 @@ export {
3781
3994
  assertRepresentableTierAmount,
3782
3995
  assertUnkeyedDirectReservation,
3783
3996
  assertUnreferencedDirectTransaction,
3997
+ assertValidBalanceTarget,
3998
+ assertValidBalanceUpdate,
3784
3999
  assertValidCreditAmount,
3785
4000
  assertValidIdempotencyKey,
3786
4001
  assertValidStoredAmount,
3787
4002
  assertValidStoredAmountRaw,
4003
+ assertValidTierBalanceInput,
4004
+ attributeExpiredBacking,
3788
4005
  backedBalanceFloor,
3789
4006
  calculateAvailableCredits,
4007
+ cappedBalance,
3790
4008
  checkAndNotifyLowBalance,
3791
4009
  classifyDatabaseError,
3792
4010
  clearConfigProvider,
@@ -3812,6 +4030,7 @@ export {
3812
4030
  createUnsupportedOperationError,
3813
4031
  createUserNotFoundError,
3814
4032
  describePaymentMismatch,
4033
+ expiredHoldBackingPatch,
3815
4034
  generateId,
3816
4035
  generateRequestId2 as generateRequestId,
3817
4036
  genericDeferred,
@@ -3870,6 +4089,9 @@ export {
3870
4089
  parseApiError,
3871
4090
  parseCreditError,
3872
4091
  parseTier,
4092
+ planBalanceCap,
4093
+ planBalanceTarget,
4094
+ planHoldCommit,
3873
4095
  registerConfigProvider,
3874
4096
  registerNotificationHandler,
3875
4097
  releaseReservationWithJournal,
@@ -3879,6 +4101,8 @@ export {
3879
4101
  reserveThroughRepository,
3880
4102
  resetConfig,
3881
4103
  sameAmount,
4104
+ settleExpiredHoldBacking,
4105
+ spendableBalance,
3882
4106
  storedMonthlyLimit,
3883
4107
  sumAmounts,
3884
4108
  supportsCreditsV2,
@@ -3890,6 +4114,7 @@ export {
3890
4114
  toClientUserCredits,
3891
4115
  toDate,
3892
4116
  toPortableTimestamp,
4117
+ validateBalanceUpdate,
3893
4118
  verifyAdminApiKey,
3894
4119
  verifyBearerToken
3895
4120
  };