@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
@@ -1,7 +1,7 @@
1
- import { R as ReservationStatus, a as PortableReservation, P as PortableTransaction, b as CreditSource, J as JournalReferenceType } from '../types-DJ8yqaVr.cjs';
2
- export { A as AIProviderType, B as BuiltinTier, g as CreditCheckResult, C as CreditOperationType, D as DeductCreditsResult, M as MonthlyResetResult, f as PortableJournalEntry, d as PortableUsageLog, c as PortableUserCredits, h as ResourceType, e as SubscriptionExpiryResult, S as SubscriptionTier, T as TierConfig, i as TransactionType, U as UsageHistoryEntry, j as UsageHistoryResponse, W as WithCreditsOptions, k as calculateAvailableCredits, t as toDate, l as toPortableTimestamp } from '../types-DJ8yqaVr.cjs';
3
- import { T as TerminalReservationStatus, I as ICreditRepository } from '../types-C8hZMe8a.cjs';
4
- export { a as AddCreditsOutcome, C as CommitOutcome, E as ExpireOutcome, R as ReleaseOutcome, n as ReserveOutcome, p as isCreditedOutcome, q as isReservedOutcome, r as isWinningOutcome } from '../types-C8hZMe8a.cjs';
1
+ import { R as ReservationStatus, a as PortableReservation, P as PortableTransaction, b as CreditSource, J as JournalReferenceType } from '../types-MaViK5tD.cjs';
2
+ export { A as AIProviderType, B as BuiltinTier, g as CreditCheckResult, C as CreditOperationType, D as DeductCreditsResult, M as MonthlyResetResult, f as PortableJournalEntry, d as PortableUsageLog, c as PortableUserCredits, h as ResourceType, e as SubscriptionExpiryResult, S as SubscriptionTier, T as TierConfig, i as TransactionType, U as UsageHistoryEntry, j as UsageHistoryResponse, W as WithCreditsOptions, k as calculateAvailableCredits, t as toDate, l as toPortableTimestamp } from '../types-MaViK5tD.cjs';
3
+ import { T as TerminalReservationStatus, I as ICreditRepository } from '../types-XSjNXO5l.cjs';
4
+ export { a as AddCreditsOutcome, C as CommitOutcome, E as ExpireOutcome, R as ReleaseOutcome, o as ReserveOutcome, q as isCreditedOutcome, r as isReservedOutcome, s as isWinningOutcome } from '../types-XSjNXO5l.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
 
7
7
  /**
@@ -393,6 +393,7 @@ declare function createPaymentRefConflictError(paymentRef: string, details: {
393
393
  * These live in core rather than in an adapter so every repository — shipped
394
394
  * or third-party — validates identically.
395
395
  */
396
+
396
397
  /** `numeric(12, 2)`: 12 significant digits, 2 of them after the point. */
397
398
  declare const CREDIT_AMOUNT_SCALE = 2;
398
399
  /** Largest representable amount: ten integer digits plus two decimals. */
@@ -479,6 +480,92 @@ declare function assertRepresentableFields(fields: Record<string, unknown>, cont
479
480
  * fund commits, so they count toward the backing.
480
481
  */
481
482
  declare function backedBalanceFloor(reserved: number, bonusCredits: number): number;
483
+ /**
484
+ * The balance left by a cap: lowered to `cap` at most, never raised, and never
485
+ * cut through the credits backing outstanding holds (see
486
+ * {@link backedBalanceFloor}). A balance already under `cap` is left alone,
487
+ * which is the point of computing this from the stored row inside a transaction
488
+ * rather than writing a value the caller read earlier.
489
+ */
490
+ declare function cappedBalance(balance: number, cap: number, reserved: number, bonusCredits: number): number;
491
+ /** A balance a lowering write lands on, and how much of it only backs holds. */
492
+ interface PlannedBalance {
493
+ balance: number;
494
+ expiredHoldBacking: number;
495
+ }
496
+ /** The part of a balance not kept only for holds. A missing field reads as 0. */
497
+ declare function spendableBalance(balance: number, expiredHoldBacking?: number): number;
498
+ /**
499
+ * An absolute balance write: `target`, floored at what backs outstanding holds.
500
+ * Whatever the floor kept above `target` is not the account's to spend.
501
+ */
502
+ declare function planBalanceTarget(target: number, reserved: number, bonusCredits: number): PlannedBalance;
503
+ /**
504
+ * A cap on the spendable part of a balance. Like {@link cappedBalance} it never
505
+ * raises the balance and never cuts through what backs holds, and it records the
506
+ * part kept above the cap so a later release cannot hand it back. Credits already
507
+ * marked stay marked, so applying a cap twice changes nothing.
508
+ */
509
+ declare function planBalanceCap(balance: number, cap: number, reserved: number, bonusCredits: number, expiredHoldBacking?: number): PlannedBalance;
510
+ /**
511
+ * What a closing hold takes out of the credits kept only for holds: its own share
512
+ * and nothing of another hold's. `holdBacking` comes from
513
+ * {@link attributeExpiredBacking}.
514
+ */
515
+ declare function settleExpiredHoldBacking(balance: number, expiredHoldBacking: number, holdBacking: number): {
516
+ used: number;
517
+ remaining: number;
518
+ };
519
+ /** The balances a commit lands on, and whether the account could pay it. */
520
+ interface PlannedHoldCommit {
521
+ covered: boolean;
522
+ balance: number;
523
+ bonusCredits: number;
524
+ expiredHoldBacking: number;
525
+ }
526
+ /**
527
+ * Pay a hold: first its own share of the marked credits, then spendable balance,
528
+ * then bonus. Marked credits backing other holds are never touched, so what this
529
+ * commit costs does not depend on which holds closed before it.
530
+ */
531
+ declare function planHoldCommit(balance: number, bonusCredits: number, expiredHoldBacking: number, holdBacking: number, amount: number): PlannedHoldCommit;
532
+ /** The fields of an open hold that decide its share of the marked credits. */
533
+ type ExpiredBackingHold = Pick<PortableReservation, "id" | "amount" | "expiredBacking" | "holdPlacedAt">;
534
+ /**
535
+ * Split `total` marked credits across open holds, returning each hold's share.
536
+ *
537
+ * Shares a hold already has are kept first, oldest hold first, so lowering `total`
538
+ * trims the newest and raising it never moves credits off a hold. Only what is left
539
+ * goes to holds with room, again oldest first, which makes the split a function of
540
+ * the holds and not of the order they later close in. A hold never carries more than
541
+ * its amount. A row without `holdPlacedAt` is a record, not a hold, and gets nothing.
542
+ *
543
+ * The account's `expiredHoldBacking` is written as the sum of these shares, in the
544
+ * same write, so what `total` could not attribute is not marked. Such credits stay out
545
+ * of reach anyway, because the `reserved` that kept them still counts against them.
546
+ */
547
+ declare function attributeExpiredBacking(holds: readonly ExpiredBackingHold[], total: number): Map<string, number>;
548
+ /**
549
+ * The field to write for a new backing value: nothing when it was and stays 0, so
550
+ * an account that never had any keeps a document without the field.
551
+ */
552
+ declare function expiredHoldBackingPatch(next: number, stored?: number): {
553
+ expiredHoldBacking?: number;
554
+ };
555
+ /**
556
+ * Validate a balance target supplied by the application: a cap or the result of
557
+ * a monthly-reset decision. It must be storable and cannot be negative.
558
+ */
559
+ declare function assertValidBalanceTarget(value: unknown, field: string, context?: Record<string, unknown>): asserts value is number;
560
+ /**
561
+ * Validate how a tier write changes the balance. `balance` sets a value the
562
+ * caller computed and `balanceCap` lowers the stored value, so naming both has
563
+ * no single meaning and is refused.
564
+ */
565
+ declare function assertValidTierBalanceInput(input: {
566
+ balance?: number;
567
+ balanceCap?: number;
568
+ }, context?: Record<string, unknown>): void;
482
569
  /**
483
570
  * Validate a limit derived from tier configuration.
484
571
  *
@@ -694,4 +781,4 @@ declare function releaseReservationWithJournal(repository: ICreditRepository, us
694
781
  */
695
782
  declare function reserveCreditsForOperation(repository: ICreditRepository, userId: string, amount: number, operationType: string, expiryMs?: number, idempotencyKey?: string): Promise<PortableReservation>;
696
783
 
697
- export { CREDIT_AMOUNT_MAX, CREDIT_AMOUNT_SCALE, CreditError, CreditErrorCode, type CreditErrorCodeType, CreditSource, JournalReferenceType, type PaymentEventPayload, PortableReservation, PortableTransaction, RESERVATION_STATUSES, RESERVED_JOURNAL_KEY_PREFIX, type ReservationIntegrityView, ReservationStatus, type StoredPaymentEvent, TERMINAL_RESERVATION_STATUSES, TerminalReservationStatus, 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 };
784
+ export { CREDIT_AMOUNT_MAX, CREDIT_AMOUNT_SCALE, CreditError, CreditErrorCode, type CreditErrorCodeType, CreditSource, JournalReferenceType, type PaymentEventPayload, PortableReservation, PortableTransaction, RESERVATION_STATUSES, RESERVED_JOURNAL_KEY_PREFIX, type ReservationIntegrityView, ReservationStatus, type StoredPaymentEvent, TERMINAL_RESERVATION_STATUSES, TerminalReservationStatus, 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 };
@@ -1,7 +1,7 @@
1
- import { R as ReservationStatus, a as PortableReservation, P as PortableTransaction, b as CreditSource, J as JournalReferenceType } from '../types-DJ8yqaVr.js';
2
- export { A as AIProviderType, B as BuiltinTier, g as CreditCheckResult, C as CreditOperationType, D as DeductCreditsResult, M as MonthlyResetResult, f as PortableJournalEntry, d as PortableUsageLog, c as PortableUserCredits, h as ResourceType, e as SubscriptionExpiryResult, S as SubscriptionTier, T as TierConfig, i as TransactionType, U as UsageHistoryEntry, j as UsageHistoryResponse, W as WithCreditsOptions, k as calculateAvailableCredits, t as toDate, l as toPortableTimestamp } from '../types-DJ8yqaVr.js';
3
- import { T as TerminalReservationStatus, I as ICreditRepository } from '../types-BmG5Omzj.js';
4
- export { a as AddCreditsOutcome, C as CommitOutcome, E as ExpireOutcome, R as ReleaseOutcome, n as ReserveOutcome, p as isCreditedOutcome, q as isReservedOutcome, r as isWinningOutcome } from '../types-BmG5Omzj.js';
1
+ import { R as ReservationStatus, a as PortableReservation, P as PortableTransaction, b as CreditSource, J as JournalReferenceType } from '../types-MaViK5tD.js';
2
+ export { A as AIProviderType, B as BuiltinTier, g as CreditCheckResult, C as CreditOperationType, D as DeductCreditsResult, M as MonthlyResetResult, f as PortableJournalEntry, d as PortableUsageLog, c as PortableUserCredits, h as ResourceType, e as SubscriptionExpiryResult, S as SubscriptionTier, T as TierConfig, i as TransactionType, U as UsageHistoryEntry, j as UsageHistoryResponse, W as WithCreditsOptions, k as calculateAvailableCredits, t as toDate, l as toPortableTimestamp } from '../types-MaViK5tD.js';
3
+ import { T as TerminalReservationStatus, I as ICreditRepository } from '../types-3c-IAwZI.js';
4
+ export { a as AddCreditsOutcome, C as CommitOutcome, E as ExpireOutcome, R as ReleaseOutcome, o as ReserveOutcome, q as isCreditedOutcome, r as isReservedOutcome, s as isWinningOutcome } from '../types-3c-IAwZI.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
 
7
7
  /**
@@ -393,6 +393,7 @@ declare function createPaymentRefConflictError(paymentRef: string, details: {
393
393
  * These live in core rather than in an adapter so every repository — shipped
394
394
  * or third-party — validates identically.
395
395
  */
396
+
396
397
  /** `numeric(12, 2)`: 12 significant digits, 2 of them after the point. */
397
398
  declare const CREDIT_AMOUNT_SCALE = 2;
398
399
  /** Largest representable amount: ten integer digits plus two decimals. */
@@ -479,6 +480,92 @@ declare function assertRepresentableFields(fields: Record<string, unknown>, cont
479
480
  * fund commits, so they count toward the backing.
480
481
  */
481
482
  declare function backedBalanceFloor(reserved: number, bonusCredits: number): number;
483
+ /**
484
+ * The balance left by a cap: lowered to `cap` at most, never raised, and never
485
+ * cut through the credits backing outstanding holds (see
486
+ * {@link backedBalanceFloor}). A balance already under `cap` is left alone,
487
+ * which is the point of computing this from the stored row inside a transaction
488
+ * rather than writing a value the caller read earlier.
489
+ */
490
+ declare function cappedBalance(balance: number, cap: number, reserved: number, bonusCredits: number): number;
491
+ /** A balance a lowering write lands on, and how much of it only backs holds. */
492
+ interface PlannedBalance {
493
+ balance: number;
494
+ expiredHoldBacking: number;
495
+ }
496
+ /** The part of a balance not kept only for holds. A missing field reads as 0. */
497
+ declare function spendableBalance(balance: number, expiredHoldBacking?: number): number;
498
+ /**
499
+ * An absolute balance write: `target`, floored at what backs outstanding holds.
500
+ * Whatever the floor kept above `target` is not the account's to spend.
501
+ */
502
+ declare function planBalanceTarget(target: number, reserved: number, bonusCredits: number): PlannedBalance;
503
+ /**
504
+ * A cap on the spendable part of a balance. Like {@link cappedBalance} it never
505
+ * raises the balance and never cuts through what backs holds, and it records the
506
+ * part kept above the cap so a later release cannot hand it back. Credits already
507
+ * marked stay marked, so applying a cap twice changes nothing.
508
+ */
509
+ declare function planBalanceCap(balance: number, cap: number, reserved: number, bonusCredits: number, expiredHoldBacking?: number): PlannedBalance;
510
+ /**
511
+ * What a closing hold takes out of the credits kept only for holds: its own share
512
+ * and nothing of another hold's. `holdBacking` comes from
513
+ * {@link attributeExpiredBacking}.
514
+ */
515
+ declare function settleExpiredHoldBacking(balance: number, expiredHoldBacking: number, holdBacking: number): {
516
+ used: number;
517
+ remaining: number;
518
+ };
519
+ /** The balances a commit lands on, and whether the account could pay it. */
520
+ interface PlannedHoldCommit {
521
+ covered: boolean;
522
+ balance: number;
523
+ bonusCredits: number;
524
+ expiredHoldBacking: number;
525
+ }
526
+ /**
527
+ * Pay a hold: first its own share of the marked credits, then spendable balance,
528
+ * then bonus. Marked credits backing other holds are never touched, so what this
529
+ * commit costs does not depend on which holds closed before it.
530
+ */
531
+ declare function planHoldCommit(balance: number, bonusCredits: number, expiredHoldBacking: number, holdBacking: number, amount: number): PlannedHoldCommit;
532
+ /** The fields of an open hold that decide its share of the marked credits. */
533
+ type ExpiredBackingHold = Pick<PortableReservation, "id" | "amount" | "expiredBacking" | "holdPlacedAt">;
534
+ /**
535
+ * Split `total` marked credits across open holds, returning each hold's share.
536
+ *
537
+ * Shares a hold already has are kept first, oldest hold first, so lowering `total`
538
+ * trims the newest and raising it never moves credits off a hold. Only what is left
539
+ * goes to holds with room, again oldest first, which makes the split a function of
540
+ * the holds and not of the order they later close in. A hold never carries more than
541
+ * its amount. A row without `holdPlacedAt` is a record, not a hold, and gets nothing.
542
+ *
543
+ * The account's `expiredHoldBacking` is written as the sum of these shares, in the
544
+ * same write, so what `total` could not attribute is not marked. Such credits stay out
545
+ * of reach anyway, because the `reserved` that kept them still counts against them.
546
+ */
547
+ declare function attributeExpiredBacking(holds: readonly ExpiredBackingHold[], total: number): Map<string, number>;
548
+ /**
549
+ * The field to write for a new backing value: nothing when it was and stays 0, so
550
+ * an account that never had any keeps a document without the field.
551
+ */
552
+ declare function expiredHoldBackingPatch(next: number, stored?: number): {
553
+ expiredHoldBacking?: number;
554
+ };
555
+ /**
556
+ * Validate a balance target supplied by the application: a cap or the result of
557
+ * a monthly-reset decision. It must be storable and cannot be negative.
558
+ */
559
+ declare function assertValidBalanceTarget(value: unknown, field: string, context?: Record<string, unknown>): asserts value is number;
560
+ /**
561
+ * Validate how a tier write changes the balance. `balance` sets a value the
562
+ * caller computed and `balanceCap` lowers the stored value, so naming both has
563
+ * no single meaning and is refused.
564
+ */
565
+ declare function assertValidTierBalanceInput(input: {
566
+ balance?: number;
567
+ balanceCap?: number;
568
+ }, context?: Record<string, unknown>): void;
482
569
  /**
483
570
  * Validate a limit derived from tier configuration.
484
571
  *
@@ -694,4 +781,4 @@ declare function releaseReservationWithJournal(repository: ICreditRepository, us
694
781
  */
695
782
  declare function reserveCreditsForOperation(repository: ICreditRepository, userId: string, amount: number, operationType: string, expiryMs?: number, idempotencyKey?: string): Promise<PortableReservation>;
696
783
 
697
- export { CREDIT_AMOUNT_MAX, CREDIT_AMOUNT_SCALE, CreditError, CreditErrorCode, type CreditErrorCodeType, CreditSource, JournalReferenceType, type PaymentEventPayload, PortableReservation, PortableTransaction, RESERVATION_STATUSES, RESERVED_JOURNAL_KEY_PREFIX, type ReservationIntegrityView, ReservationStatus, type StoredPaymentEvent, TERMINAL_RESERVATION_STATUSES, TerminalReservationStatus, 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 };
784
+ export { CREDIT_AMOUNT_MAX, CREDIT_AMOUNT_SCALE, CreditError, CreditErrorCode, type CreditErrorCodeType, CreditSource, JournalReferenceType, type PaymentEventPayload, PortableReservation, PortableTransaction, RESERVATION_STATUSES, RESERVED_JOURNAL_KEY_PREFIX, type ReservationIntegrityView, ReservationStatus, type StoredPaymentEvent, TERMINAL_RESERVATION_STATUSES, TerminalReservationStatus, 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 };
@@ -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);
@@ -593,11 +673,13 @@ var creditSystemConfigSchema = z.object({
593
673
  /**
594
674
  * Operation costs in credits
595
675
  * Keys are operation type names (any non-empty string)
596
- * Values are positive numbers representing credit cost
676
+ * Values are non-negative numbers representing credit cost. 0 marks a free
677
+ * operation: `withCredits` runs it without touching the ledger. Amounts that
678
+ * move credits stay strictly positive, so `reserveCredits` refuses a 0.
597
679
  */
598
680
  operationCosts: z.record(
599
681
  z.string().min(1),
600
- z.number().positive()
682
+ z.number().nonnegative()
601
683
  ),
602
684
  /**
603
685
  * Display labels for operation types (English canonical labels).
@@ -619,9 +701,9 @@ var creditSystemConfigSchema = z.object({
619
701
  */
620
702
  reservationExpiryMs: z.number().positive().default(5 * 60 * 1e3),
621
703
  /**
622
- * Default credits for new users (free tier)
704
+ * Default credits for new users (free tier). 0 means no signup grant.
623
705
  */
624
- defaultFreeCredits: z.number().positive().default(25),
706
+ defaultFreeCredits: z.number().min(0).default(25),
625
707
  /**
626
708
  * Grace period in days after subscription expires before downgrade
627
709
  */
@@ -1004,12 +1086,16 @@ export {
1004
1086
  assertRepresentableTierAmount,
1005
1087
  assertUnkeyedDirectReservation,
1006
1088
  assertUnreferencedDirectTransaction,
1089
+ assertValidBalanceTarget,
1007
1090
  assertValidCreditAmount,
1008
1091
  assertValidIdempotencyKey,
1009
1092
  assertValidStoredAmount,
1010
1093
  assertValidStoredAmountRaw,
1094
+ assertValidTierBalanceInput,
1095
+ attributeExpiredBacking,
1011
1096
  backedBalanceFloor,
1012
1097
  calculateAvailableCredits,
1098
+ cappedBalance,
1013
1099
  classifyDatabaseError,
1014
1100
  commitReservationWithJournal,
1015
1101
  createDeferredExecutor,
@@ -1025,6 +1111,7 @@ export {
1025
1111
  createUnsupportedOperationError,
1026
1112
  createUserNotFoundError,
1027
1113
  describePaymentMismatch,
1114
+ expiredHoldBackingPatch,
1028
1115
  genericDeferred,
1029
1116
  getSqlState,
1030
1117
  hasPlacedHold,
@@ -1046,9 +1133,14 @@ export {
1046
1133
  noopDeferred,
1047
1134
  normalizePaymentRef,
1048
1135
  numericToCents,
1136
+ planBalanceCap,
1137
+ planBalanceTarget,
1138
+ planHoldCommit,
1049
1139
  releaseReservationWithJournal,
1050
1140
  reserveCreditsForOperation,
1051
1141
  sameAmount,
1142
+ settleExpiredHoldBacking,
1143
+ spendableBalance,
1052
1144
  storedMonthlyLimit,
1053
1145
  sumAmounts,
1054
1146
  synchronousDeferred,