@lunora/payment 1.0.0-alpha.13 → 1.0.0-alpha.131

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 (62) hide show
  1. package/LICENSE.md +6 -0
  2. package/README.md +8 -4
  3. package/dist/index.d.mts +407 -530
  4. package/dist/index.d.ts +407 -530
  5. package/dist/index.mjs +1 -16
  6. package/dist/packem_shared/LunoraPaymentError-BSxyhWgu.mjs +1 -0
  7. package/dist/packem_shared/MemoryPaymentStore-CZbrrEb1.mjs +1 -0
  8. package/dist/packem_shared/PAYMENT_TERMINAL_STATES-C-ICZkun.mjs +1 -0
  9. package/dist/packem_shared/adapter.d-DytKm_IF.d.mts +513 -0
  10. package/dist/packem_shared/adapter.d-DytKm_IF.d.ts +513 -0
  11. package/dist/packem_shared/addMoney-B4ufWAnD.mjs +1 -0
  12. package/dist/packem_shared/applyWebhookAction-CvLpZjKP.mjs +1 -0
  13. package/dist/packem_shared/constantTimeEqual-ggrwGvRa.mjs +1 -0
  14. package/dist/packem_shared/createAdapterRegistry-DyCsry1Q.mjs +1 -0
  15. package/dist/packem_shared/createDatabasePaymentStore-BGcHX1uq.mjs +1 -0
  16. package/dist/packem_shared/createPayment-Dimez2zY.mjs +1 -0
  17. package/dist/packem_shared/entitlementsForReference-PMmhAOWa.mjs +1 -0
  18. package/dist/packem_shared/idempotencyKey-qQJLcEQX.mjs +1 -0
  19. package/dist/packem_shared/json-BJJPJVYj.mjs +1 -0
  20. package/dist/packem_shared/lunoraDatabaseToPaymentDatabase-CFPYSEu0.mjs +1 -0
  21. package/dist/packem_shared/not-supported-Cl0brzhn.mjs +1 -0
  22. package/dist/packem_shared/observability-BteZ2dJS.mjs +1 -0
  23. package/dist/packem_shared/paymentTables-Ve_lJxAt.mjs +1 -0
  24. package/dist/packem_shared/reconcile-fbxX8j9k.mjs +1 -0
  25. package/dist/packem_shared/subscription-event-C8GiQSFM.mjs +1 -0
  26. package/dist/providers/autumn-features.d.mts +129 -0
  27. package/dist/providers/autumn-features.d.ts +129 -0
  28. package/dist/providers/autumn-features.mjs +1 -0
  29. package/dist/providers/autumn.d.mts +28 -0
  30. package/dist/providers/autumn.d.ts +28 -0
  31. package/dist/providers/autumn.mjs +1 -0
  32. package/dist/providers/creem.d.mts +26 -0
  33. package/dist/providers/creem.d.ts +26 -0
  34. package/dist/providers/creem.mjs +1 -0
  35. package/dist/providers/dodopayments.d.mts +30 -0
  36. package/dist/providers/dodopayments.d.ts +30 -0
  37. package/dist/providers/dodopayments.mjs +1 -0
  38. package/dist/providers/polar.d.mts +31 -0
  39. package/dist/providers/polar.d.ts +31 -0
  40. package/dist/providers/polar.mjs +1 -0
  41. package/dist/providers/stripe.d.mts +34 -0
  42. package/dist/providers/stripe.d.ts +34 -0
  43. package/dist/providers/stripe.mjs +1 -0
  44. package/package.json +45 -4
  45. package/dist/packem_shared/LunoraPaymentError-B3hEzXSs.mjs +0 -22
  46. package/dist/packem_shared/MemoryPaymentStore-DvgdWa3C.mjs +0 -72
  47. package/dist/packem_shared/PAYMENT_TERMINAL_STATES-DrxV0clv.mjs +0 -26
  48. package/dist/packem_shared/addMoney-bCcs1nyw.mjs +0 -60
  49. package/dist/packem_shared/applyWebhookAction-DpAqf3Lw.mjs +0 -177
  50. package/dist/packem_shared/constantTimeEqual-CfY0jYcL.mjs +0 -95
  51. package/dist/packem_shared/createAdapterRegistry-BuDHFCBc.mjs +0 -24
  52. package/dist/packem_shared/createDatabasePaymentStore-bYB_HUE6.mjs +0 -172
  53. package/dist/packem_shared/createPayment-BccfPGyw.mjs +0 -190
  54. package/dist/packem_shared/createPolarAdapter-CgErk9zo.mjs +0 -220
  55. package/dist/packem_shared/createStripeAdapter-oDvMhpeX.mjs +0 -279
  56. package/dist/packem_shared/entitlementsForReference-CzZGXPoZ.mjs +0 -51
  57. package/dist/packem_shared/idempotencyKey-BFzDCA7g.mjs +0 -3
  58. package/dist/packem_shared/json-Db337f36.mjs +0 -6
  59. package/dist/packem_shared/lunoraDatabaseToPaymentDatabase-RlKX3Kcd.mjs +0 -29
  60. package/dist/packem_shared/observability-CvhJ205g.mjs +0 -11
  61. package/dist/packem_shared/paymentTables-DccHwWr_.mjs +0 -127
  62. package/dist/packem_shared/reconcile-CI1ukJF9.mjs +0 -73
package/dist/index.d.mts CHANGED
@@ -1,327 +1,84 @@
1
+ import { a as ProviderId, C as Customer, b as PaymentSession, S as Subscription, U as UsageEvent, W as WebhookActionType, A as ApplyResult, P as PaymentAdapter, c as AttachInput, d as CheckoutResult, e as CancelSubscriptionOptions, f as CaptureInput, g as CheckInput, h as CheckResult, i as CheckoutInput, F as FeatureBalance, R as RefundInput, T as TrackInput, j as TrackResult, k as CurrencyCode, M as Money, l as PaymentState, m as SubscriptionState, n as WebhookAction } from "./packem_shared/adapter.d-DytKm_IF.mjs";
2
+ export { type o as AdapterRegistry, type p as CustomerRef, type q as PortalInput, type r as ProviderCapabilities, type s as RefundAmountKind, type t as RefundResult, type u as ReportUsageInput, type v as SubscriptionPatch, type w as WebhookHeaders, type x as WebhookInput, y as createAdapterRegistry } from "./packem_shared/adapter.d-DytKm_IF.mjs";
3
+ import { LunoraError } from '@lunora/errors';
1
4
  import { TableDefinition } from '@lunora/server';
2
5
  /**
3
- * Core domain types for `@lunora/payment`.
4
- *
5
- * The provider is a stateless translator; the store owns all state. These types are the
6
- * provider-agnostic vocabulary every adapter normalizes onto.
7
- */
8
- /** ISO-4217 currency code (uppercase, 3 letters). Not enumerated — provider coverage varies. */
9
- type CurrencyCode = string;
10
- /**
11
- * Money as integer minor units + currency. Always carry the two together.
12
- *
13
- * `minorUnits` is a `bigint`, which is **not** JSON-serializable — cross the RPC/wire boundary
14
- * with the `toMoneyJSON` / `fromMoneyJSON` helpers (see `./money`).
15
- */
16
- interface Money {
17
- readonly currency: CurrencyCode;
18
- readonly minorUnits: bigint;
19
- }
20
- /** Stable provider identifier (Medusa-style). Scoped to what Convex ships: Stripe + Polar. */
21
- type ProviderId = "polar" | "stripe";
22
- /** What a provider can do — encoded in types so tax/UX assumptions aren't tribal knowledge. */
23
- interface ProviderCapabilities {
24
- /** True for Polar / Lemon Squeezy / Paddle; false for Stripe (PSP). Drives tax/invoice ownership. */
25
- readonly merchantOfRecord: boolean;
26
- /** Native hosted customer/billing portal. */
27
- readonly portal: boolean;
28
- /** Usage-based / metered billing. */
29
- readonly usageMetering: boolean;
30
- }
31
- /** Lifecycle state of a one-time payment session. */
32
- type PaymentState = "authorized" | "canceled" | "captured" | "failed" | "initiated" | "partially_refunded" | "refunded";
33
- /** Lifecycle state of a subscription. */
34
- type SubscriptionState = "active" | "canceled" | "past_due" | "paused" | "trialing";
35
- interface Customer {
36
- readonly createdAt: number;
37
- readonly email?: string;
38
- /** Provider-side customer id. */
39
- readonly id: string;
40
- readonly provider: ProviderId;
41
- /** App-side owner the customer belongs to (user / org / workspace). Opaque to this package. */
42
- readonly referenceId: string;
43
- }
44
- interface PaymentSession {
45
- readonly amount: Money;
46
- readonly capturedAmount: Money;
47
- readonly createdAt: number;
48
- /** Provider-side payment / intent / session id. */
49
- readonly id: string;
50
- readonly provider: ProviderId;
51
- readonly referenceId: string;
52
- readonly refundedAmount: Money;
53
- readonly state: PaymentState;
54
- readonly updatedAt: number;
55
- }
56
- interface Subscription {
57
- readonly cancelAtPeriodEnd: boolean;
58
- readonly createdAt: number;
59
- readonly currentPeriodEnd?: number;
60
- /** Start of the current billing period — the window `check` sums metered usage over. */
61
- readonly currentPeriodStart?: number;
62
- readonly id: string;
63
- readonly priceId: string;
64
- readonly provider: ProviderId;
65
- readonly quantity: number;
66
- readonly referenceId: string;
67
- readonly state: SubscriptionState;
68
- readonly updatedAt: number;
69
- }
70
- interface CustomerRef {
71
- readonly email?: string;
72
- readonly metadata?: Record<string, string>;
73
- readonly referenceId: string;
74
- }
75
- interface CheckoutInput {
76
- readonly cancelUrl: string;
77
- /** Existing provider customer id, if known. */
78
- readonly customerId?: string;
79
- /** Outbound idempotency key for the provider call; auto-derived when omitted. */
80
- readonly idempotencyKey?: string;
81
- readonly metadata?: Record<string, string>;
82
- readonly mode: "payment" | "subscription";
83
- readonly priceId: string;
84
- readonly quantity?: number;
85
- readonly referenceId: string;
86
- readonly successUrl: string;
87
- }
88
- interface CheckoutResult {
89
- readonly id: string;
90
- readonly provider: ProviderId;
91
- readonly url: string;
92
- }
93
- /**
94
- * `attach` input — subscribe a reference to a plan. A thin, plan-oriented skin over
95
- * {@link CheckoutInput}: `mode` defaults to `"subscription"` (the common case), so callers pass
96
- * just `{ referenceId, priceId, successUrl, cancelUrl }`.
97
- */
98
- interface AttachInput extends Omit<CheckoutInput, "mode"> {
99
- readonly mode?: CheckoutInput["mode"];
100
- }
101
- interface PortalInput {
102
- readonly customerId: string;
103
- readonly returnUrl: string;
104
- }
105
- /** A single durable usage record — one metered event for a `(referenceId, featureId)` pair. */
106
- interface UsageEvent {
107
- readonly createdAt: number;
108
- readonly featureId: string;
109
- /** Caller-stable dedupe key — recording the same key twice is a no-op (exactly-once `track`). */
110
- readonly idempotencyKey: string;
111
- readonly provider: ProviderId;
112
- readonly quantity: number;
113
- readonly referenceId: string;
114
- /** Whether the event was successfully forwarded to the provider's metering API. */
115
- readonly reportedToProvider: boolean;
116
- }
117
- /** `track` input — record metered usage for a reference's feature. */
118
- interface TrackInput {
119
- readonly featureId: string;
120
- /** Caller-supplied dedupe key; a fresh one is generated when omitted (so each call records). */
121
- readonly idempotencyKey?: string;
122
- /** `"add"` (default) increments usage by `quantity`; `"set"` reconciles the period total to `quantity`. */
123
- readonly mode?: "add" | "set";
124
- /** Usage amount to add, or the absolute period total when `mode` is `"set"` (defaults to `1`). */
125
- readonly quantity?: number;
126
- readonly referenceId: string;
127
- }
128
- /** Result of a `track` call. */
129
- interface TrackResult {
130
- /** True when this call inserted a new usage event; false when deduplicated by idempotency key. */
131
- readonly recorded: boolean;
132
- /** True when the event was forwarded to the provider's metering API. */
133
- readonly reportedToProvider: boolean;
134
- }
135
- /**
136
- * `check` input — is a reference allowed something right now? Pass `featureId` to check a feature
137
- * grant/allowance, or `priceId` to check active access to a product (one of the two is required).
138
- */
139
- interface CheckInput {
140
- /** Feature to check a grant/allowance for. Provide this **or** `priceId`. */
141
- readonly featureId?: string;
142
- /** Provider price/product id to check active access for. Provide this **or** `featureId`. */
143
- readonly priceId?: string;
144
- /** Units the caller intends to consume; the check passes only when this many remain (default `1`). */
145
- readonly quantity?: number;
146
- readonly referenceId: string;
147
- }
148
- /** Result of a `check` call. */
149
- interface CheckResult {
150
- /** Whether the reference may consume `quantity` units of the feature right now. */
151
- readonly allowed: boolean;
152
- /** Remaining units this period (`limit - used`), for metered features only. */
153
- readonly balance?: number;
154
- /** The plan-granted cap, for metered features only. */
155
- readonly limit?: number;
156
- /** True for a boolean feature granted without a numeric cap. */
157
- readonly unlimited: boolean;
158
- /** Usage consumed this period, for metered features only. */
159
- readonly used?: number;
160
- }
161
- /** One feature's resolved allowance for a reference — a {@link CheckResult} tagged with its feature. */
162
- interface FeatureBalance extends CheckResult {
163
- readonly featureId: string;
164
- }
165
- /** Input the adapter forwards to the provider's metering API (Stripe Meter Events / Polar ingestion). */
166
- interface ReportUsageInput {
167
- /** Provider customer id, when known (Stripe meter events key on it). */
168
- readonly customerId?: string;
169
- readonly featureId: string;
170
- readonly idempotencyKey: string;
171
- readonly quantity: number;
172
- readonly referenceId: string;
173
- /** Event time in epoch ms; defaults to now at the provider. */
174
- readonly timestamp?: number;
175
- }
176
- interface CaptureInput {
177
- /** Partial capture amount; full capture when omitted. */
178
- readonly amount?: Money;
179
- readonly idempotencyKey?: string;
180
- readonly sessionId: string;
181
- }
182
- interface RefundInput {
183
- /** Partial refund amount; full refund when omitted. */
184
- readonly amount?: Money;
185
- readonly idempotencyKey?: string;
186
- readonly reason?: string;
187
- readonly sessionId: string;
188
- }
189
- interface CancelSubscriptionOptions {
190
- /** Cancel at period end instead of immediately. */
191
- readonly atPeriodEnd?: boolean;
192
- readonly idempotencyKey?: string;
193
- }
194
- interface SubscriptionPatch {
195
- readonly priceId?: string;
196
- readonly quantity?: number;
197
- }
198
- /** Normalized webhook outcome — the *core state transition* a provider event implies. */
199
- type WebhookActionType = "payment.authorized" | "payment.captured" | "payment.failed" | "payment.refunded" | "subscription.active" | "subscription.canceled" | "subscription.past_due" | "subscription.paused" | "subscription.updated" | "unhandled";
200
- /**
201
- * How a refund action's {@link WebhookAction.amount} should be interpreted by the sync layer.
202
- *
203
- * `"delta"` is an incremental amount added to the running refunded total (Polar `refund.created`,
204
- * and the historical default), so multiple events accumulate. `"absolute"` is the provider's
205
- * cumulative refunded-to-date total (Stripe `charge.refunded` carries `amount_refunded`, which
206
- * already sums all prior partial refunds); the sync layer sets the refunded total to this value
207
- * rather than adding, so repeated partial-refund events do not over-count.
208
- *
209
- * Omitted means `"delta"`, preserving the original behavior for callers that predate this field.
210
- */
211
- type RefundAmountKind = "absolute" | "delta";
212
- interface WebhookAction {
213
- readonly amount?: Money;
214
- /**
215
- * Interpretation of {@link WebhookAction.amount} for refund actions (`payment.refunded`).
216
- * Defaults to `"delta"` when omitted. Ignored for non-refund actions.
217
- */
218
- readonly amountKind?: RefundAmountKind;
219
- readonly cancelAtPeriodEnd?: boolean;
220
- readonly currentPeriodEnd?: number;
221
- readonly currentPeriodStart?: number;
222
- readonly customerId?: string;
223
- /** Provider event id — the inbound idempotency key. */
224
- readonly eventId: string;
225
- readonly priceId?: string;
226
- readonly provider: ProviderId;
227
- readonly quantity?: number;
228
- /** Raw provider event, retained for the events log / debugging. */
229
- readonly raw?: unknown;
230
- readonly referenceId?: string;
231
- readonly sessionId?: string;
232
- readonly subscriptionId?: string;
233
- readonly type: WebhookActionType;
234
- }
235
- /** Result of applying a webhook action to the store. */
236
- interface ApplyResult {
237
- readonly applied: boolean;
238
- readonly reason?: "duplicate" | "illegal_transition" | "invalid_refund_amount" | "ok" | "unhandled";
239
- }
240
- /** A read-only header bag; the platform `Headers` object satisfies it. */
241
- interface WebhookHeaders {
242
- get: (name: string) => null | string;
243
- }
244
- interface WebhookInput {
245
- /** Request headers (signature schemes read provider-specific headers from here). */
246
- readonly headers: WebhookHeaders;
247
- /** Raw request body, exactly as received (required for signature verification). */
248
- readonly payload: string;
249
- }
250
- /**
251
- * A stateless translator between the provider API and Lunora's normalized vocabulary.
252
- *
253
- * Adapters never own state — they make provider calls and normalize provider events into a
254
- * `WebhookAction`. All durable state lives in the payment store.
255
- */
256
- interface PaymentAdapter {
257
- cancelPayment: (sessionId: string, options?: {
258
- idempotencyKey?: string;
259
- }) => Promise<PaymentSession>;
260
- cancelSubscription: (subscriptionId: string, options?: CancelSubscriptionOptions) => Promise<Subscription>;
261
- readonly capabilities: ProviderCapabilities;
262
- capturePayment: (input: CaptureInput) => Promise<PaymentSession>;
263
- createCheckout: (input: CheckoutInput) => Promise<CheckoutResult>;
264
- createPortalSession: (input: PortalInput) => Promise<{
265
- url: string;
266
- }>;
267
- getOrCreateCustomer: (ref: CustomerRef) => Promise<Customer>;
268
- /** Fetch the provider's current truth for a payment session — the basis for reconciliation. */
269
- getPaymentStatus: (sessionId: string) => Promise<PaymentSession>;
270
- /** Fetch the provider's current truth for a subscription — the basis for reconciliation. */
271
- getSubscriptionStatus: (subscriptionId: string) => Promise<Subscription>;
272
- /** Stable provider identifier (Medusa-style). */
273
- readonly identifier: ProviderId;
274
- /** Verify the signature over the raw body, then normalize the event. Throws on invalid signature. */
275
- parseWebhook: (input: WebhookInput) => Promise<WebhookAction>;
276
- refundPayment: (input: RefundInput) => Promise<PaymentSession>;
277
- /**
278
- * Forward metered usage to the provider's billing API. Optional — present only on providers
279
- * whose `capabilities.usageMetering` is `true` and that expose an ingestion endpoint. When
280
- * absent, `track` still records usage durably and `check` enforces limits locally.
281
- */
282
- reportUsage?: (input: ReportUsageInput) => Promise<void>;
283
- resumeSubscription: (subscriptionId: string) => Promise<Subscription>;
284
- updateSubscription: (subscriptionId: string, patch: SubscriptionPatch) => Promise<Subscription>;
285
- }
286
- /** Registry of adapters keyed by provider id — supports dual-register during provider migration. */
287
- interface AdapterRegistry {
288
- all: () => PaymentAdapter[];
289
- get: (provider: ProviderId) => PaymentAdapter;
290
- has: (provider: ProviderId) => boolean;
291
- }
292
- declare const createAdapterRegistry: (adapters: ReadonlyArray<PaymentAdapter>) => AdapterRegistry;
6
+ * `PaymentStore` is part of the experimental `@lunora/payment` API and may change without a major version bump.
7
+ * @experimental
8
+ */
293
9
  interface PaymentStore {
294
10
  getCustomerByReference: (provider: ProviderId, referenceId: string) => Promise<Customer | undefined>;
295
11
  getPaymentSession: (provider: ProviderId, id: string) => Promise<PaymentSession | undefined>;
296
12
  getSubscription: (provider: ProviderId, id: string) => Promise<Subscription | undefined>;
297
13
  listSubscriptionsByReference: (referenceId: string) => Promise<Subscription[]>;
298
14
  /**
299
- * Claims a provider event id for processing. Resolves `true` the first time an event is seen
300
- * and `false` for a duplicate — the inbound-idempotency primitive.
301
- */
302
- markEventProcessed: (provider: ProviderId, eventId: string) => Promise<boolean>;
15
+ * Usage events that still owe an upstream forward, oldest first, at most `limit`.
16
+ *
17
+ * "Owes a forward" is narrower than `reportedToProvider === false`: only an
18
+ * ADDITIVE event with a positive `quantity` qualifies. A `"set"` event's
19
+ * upstream delta was measured against the period total at the moment it was
20
+ * recorded and is not recoverable afterwards, and a non-positive quantity was
21
+ * never sent, so neither is a retry candidate — returning them would make the
22
+ * sweep re-send the same rows forever (or double-count on an additive meter).
23
+ *
24
+ * Read by `reconcile` to retry a forward the provider rejected transiently;
25
+ * without it a single 5xx loses that metered unit upstream for good, which for
26
+ * a provider that owns entitlements under-bills and over-entitles the customer.
27
+ */
28
+ listUnreportedUsage: (provider: ProviderId, limit: number) => Promise<UsageEvent[]>;
29
+ /**
30
+ * Claims a provider event id for processing. Resolves `true` the first time an event is seen
31
+ * and `false` for a duplicate — the inbound-idempotency primitive.
32
+ *
33
+ * `type` is recorded on the claim row so the `events` table is a readable audit log rather than
34
+ * bare ids and timestamps: a real delivery passes its `WebhookActionType`, and the
35
+ * internal claim markers (`sync.ts`'s orphan-retry bound, the local-refund ledger) pass a
36
+ * `marker.*` label so they are distinguishable from provider traffic in the studio.
37
+ */
38
+ markEventProcessed: (provider: ProviderId, eventId: string, type: string) => Promise<boolean>;
303
39
  /** Flag a recorded usage event as forwarded to the provider's metering API. */
304
40
  markUsageReported: (provider: ProviderId, idempotencyKey: string) => Promise<void>;
305
41
  /**
306
- * Append a usage event. Resolves `true` when newly recorded and `false` when its
307
- * `idempotencyKey` was already seen — the exactly-once primitive behind `track`.
308
- */
42
+ * Append a usage event. Resolves `true` when newly recorded and `false` when its
43
+ * `idempotencyKey` was already seen — the exactly-once primitive behind `track`.
44
+ */
309
45
  recordUsage: (event: UsageEvent) => Promise<boolean>;
310
46
  /**
311
- * Release a previously-claimed event id (see {@link PaymentStore.markEventProcessed}) so a
312
- * provider retry can re-process it. Called only when applying the claimed event *throws* (a
313
- * genuine store-write failure): the atomic insert-claim guards concurrent duplicates, but a
314
- * claim that outlives a failed apply would dedupe the retry and lose the effect — so the claim
315
- * is rolled back on failure. A no-op if the id was never claimed.
316
- */
47
+ * Release a previously-claimed event id (see {@link PaymentStore.markEventProcessed}) so a
48
+ * provider retry can re-process it. Called only when applying the claimed event *throws* (a
49
+ * genuine store-write failure): the atomic insert-claim guards concurrent duplicates, but a
50
+ * claim that outlives a failed apply would dedupe the retry and lose the effect — so the claim
51
+ * is rolled back on failure. A no-op if the id was never claimed.
52
+ */
317
53
  releaseEvent: (provider: ProviderId, eventId: string) => Promise<void>;
318
- /** Sum recorded usage `quantity` for a `(referenceId, featureId)` pair since `since` (epoch ms). */
54
+ /**
55
+ * The period total for a `(referenceId, featureId)` pair since `since` (epoch ms).
56
+ *
57
+ * NOT a plain sum: events are folded in `createdAt` order (ties broken by
58
+ * `idempotencyKey`, so every store agrees on the same order for events stamped
59
+ * in the same millisecond). An `"add"` event increments the running total; a
60
+ * `"set"` event RESETS it to that event's `quantity`, discarding everything
61
+ * earlier in the period. Use {@link foldUsage} so the two implementations
62
+ * cannot drift.
63
+ *
64
+ * This fold is what lets `track({ mode: "set" })` be append-only. Reconciling by
65
+ * writing `target - current` instead would be a read-modify-write across two
66
+ * un-transacted store calls: two interleaved `set`s would both read the same
67
+ * total, both append a delta, and leave the period over- or under-counted —
68
+ * inflating `balance = limit - used`. Here a concurrent pair simply resolves
69
+ * last-writer-wins, and a replayed `set` is idempotent by construction.
70
+ */
319
71
  sumUsage: (referenceId: string, featureId: string, since: number) => Promise<number>;
72
+ /** Period usage totals for many features in one read — the batch form of {@link PaymentStore.sumUsage}. */
73
+ sumUsageByFeature: (referenceId: string, featureIds: ReadonlyArray<string>, since: number) => Promise<ReadonlyMap<string, number>>;
320
74
  upsertCustomer: (customer: Customer) => Promise<void>;
321
75
  upsertPaymentSession: (session: PaymentSession) => Promise<void>;
322
76
  upsertSubscription: (subscription: Subscription) => Promise<void>;
323
77
  }
324
- /** In-memory {@link PaymentStore} for tests and local development. Not durable. */
78
+ /**
79
+ * In-memory {@link PaymentStore} for tests and local development. Not durable.
80
+ * @experimental
81
+ */
325
82
  declare class MemoryPaymentStore implements PaymentStore {
326
83
  private readonly customers;
327
84
  private readonly processedEvents;
@@ -332,15 +89,21 @@ declare class MemoryPaymentStore implements PaymentStore {
332
89
  getPaymentSession(provider: ProviderId, id: string): Promise<PaymentSession | undefined>;
333
90
  getSubscription(provider: ProviderId, id: string): Promise<Subscription | undefined>;
334
91
  listSubscriptionsByReference(referenceId: string): Promise<Subscription[]>;
335
- markEventProcessed(provider: ProviderId, eventId: string): Promise<boolean>;
92
+ listUnreportedUsage(provider: ProviderId, limit: number): Promise<UsageEvent[]>;
93
+ markEventProcessed(provider: ProviderId, eventId: string, _type: string): Promise<boolean>;
336
94
  releaseEvent(provider: ProviderId, eventId: string): Promise<void>;
337
95
  markUsageReported(provider: ProviderId, idempotencyKey: string): Promise<void>;
338
96
  recordUsage(event: UsageEvent): Promise<boolean>;
339
97
  sumUsage(referenceId: string, featureId: string, since: number): Promise<number>;
98
+ sumUsageByFeature(referenceId: string, featureIds: ReadonlyArray<string>, since: number): Promise<ReadonlyMap<string, number>>;
340
99
  upsertCustomer(customer: Customer): Promise<void>;
341
100
  upsertPaymentSession(session: PaymentSession): Promise<void>;
342
101
  upsertSubscription(subscription: Subscription): Promise<void>;
343
102
  }
103
+ /**
104
+ * `PlanDefinition` is part of the experimental `@lunora/payment` API and may change without a major version bump.
105
+ * @experimental
106
+ */
344
107
  interface PlanDefinition {
345
108
  /** Feature flags this plan grants. */
346
109
  readonly features?: ReadonlyArray<string>;
@@ -349,10 +112,18 @@ interface PlanDefinition {
349
112
  /** Provider price/product ids that grant this plan. */
350
113
  readonly priceIds: ReadonlyArray<string>;
351
114
  }
115
+ /**
116
+ * `EntitlementsConfig` is part of the experimental `@lunora/payment` API and may change without a major version bump.
117
+ * @experimental
118
+ */
352
119
  interface EntitlementsConfig {
353
120
  /** Plan name → definition. */
354
121
  readonly plans: Record<string, PlanDefinition>;
355
122
  }
123
+ /**
124
+ * `Entitlements` is part of the experimental `@lunora/payment` API and may change without a major version bump.
125
+ * @experimental
126
+ */
356
127
  interface Entitlements {
357
128
  readonly features: ReadonlySet<string>;
358
129
  /** True when an active subscription grants `feature`. */
@@ -363,19 +134,36 @@ interface Entitlements {
363
134
  readonly plans: ReadonlyArray<string>;
364
135
  }
365
136
  /**
366
- * Start of the window `check` sums metered usage over: the most recent billing-period start among
367
- * a reference's active subscriptions. `0` (count all-time) when no active subscription reports one
368
- * — limits still bind, they just never reset until the provider sends a period.
369
- */
137
+ * Start of the window `check` sums metered usage over: the most recent billing-period start among
138
+ * a reference's active subscriptions. `0` (count all-time) when no active subscription reports one
139
+ * — limits still bind, they just never reset until the provider sends a period.
140
+ * @experimental
141
+ */
370
142
  declare const usagePeriodStart: (subscriptions: ReadonlyArray<Subscription>) => number;
371
- /** Every feature name a config can grant — the union of `features` flags and `limits` keys across all plans, sorted. */
143
+ /**
144
+ * Every feature name a config can grant — the union of `features` flags and `limits` keys across all plans, sorted.
145
+ * @experimental
146
+ */
372
147
  declare const featureNames: (config: EntitlementsConfig) => string[];
373
- /** Whether the reference holds an entitling (active/trialing) subscription on `priceId` — the basis of a product `check`. */
148
+ /**
149
+ * Whether the reference holds an entitling (active/trialing) subscription on `priceId` — the basis of a product `check`.
150
+ * @experimental
151
+ */
374
152
  declare const hasActivePrice: (subscriptions: ReadonlyArray<Subscription>, priceId: string) => boolean;
375
- /** Derive {@link Entitlements} from a reference's subscriptions. Pure — the basis of `check`. */
153
+ /**
154
+ * Derive {@link Entitlements} from a reference's subscriptions. Pure — the basis of `check`.
155
+ * @experimental
156
+ */
376
157
  declare const resolveEntitlements: (config: EntitlementsConfig, subscriptions: ReadonlyArray<Subscription>) => Entitlements;
377
- /** Convenience: resolve entitlements straight from the store for a reference. */
158
+ /**
159
+ * Convenience: resolve entitlements straight from the store for a reference.
160
+ * @experimental
161
+ */
378
162
  declare const entitlementsForReference: (store: PaymentStore, config: EntitlementsConfig, referenceId: string) => Promise<Entitlements>;
163
+ /**
164
+ * `PaymentEvent` is part of the experimental `@lunora/payment` API and may change without a major version bump.
165
+ * @experimental
166
+ */
379
167
  type PaymentEvent = {
380
168
  action: WebhookActionType;
381
169
  eventId: string;
@@ -420,15 +208,50 @@ type PaymentEvent = {
420
208
  referenceId: string;
421
209
  type: "usage.report_failed";
422
210
  };
211
+ /**
212
+ * `PaymentObserver` is part of the experimental `@lunora/payment` API and may change without a major version bump.
213
+ * @experimental
214
+ */
423
215
  type PaymentObserver = (event: PaymentEvent) => void;
424
- /** Returns whether the current caller may act on `referenceId`. Throwing is also treated as denial. */
216
+ /**
217
+ * What a `processWebhook`-shaped internal action hands back to the HTTP route: the
218
+ * outcome plus the HTTP status the provider must actually see.
219
+ * @experimental
220
+ */
221
+ interface WebhookOutcome {
222
+ /** Whether the event advanced a row. A verified no-op/duplicate is `false`. */
223
+ applied: boolean;
224
+ /** The status {@link LunoraPayment.handleWebhook} answered — 500 for an orphaned event. */
225
+ status: number;
226
+ }
227
+ /**
228
+ * Turn a {@link WebhookOutcome} back into the HTTP response the provider must see.
229
+ *
230
+ * The webhook endpoint runs at the Worker edge (signature verification needs the raw
231
+ * body) and forwards into the shard via `ctx.runAction`, so `handleWebhook`'s own
232
+ * `Response` cannot cross the action boundary — only its JSON payload can. A route
233
+ * that answers `Response.json(result)` therefore collapses every outcome to `200`,
234
+ * including the deliberate `500` on an orphaned (out-of-order) event: the provider
235
+ * stops retrying and that event is lost for good. Call this from the route instead
236
+ * of building the response by hand.
237
+ * @experimental
238
+ */
239
+ declare const webhookResponse: (result: WebhookOutcome) => Response;
240
+ /**
241
+ * Returns whether the current caller may act on `referenceId`. Throwing is also treated as denial.
242
+ * @experimental
243
+ */
425
244
  type AuthorizeReference = (referenceId: string) => boolean | Promise<boolean>;
245
+ /**
246
+ * `CreatePaymentOptions` is part of the experimental `@lunora/payment` API and may change without a major version bump.
247
+ * @experimental
248
+ */
426
249
  interface CreatePaymentOptions {
427
250
  readonly adapter: PaymentAdapter;
428
251
  /**
429
- * Per-caller authorization for every mutation. Return `false` to reject with 403. Omit only
430
- * for trusted server-internal callers (e.g. the reconciliation sweep).
431
- */
252
+ * Per-caller authorization for every mutation. Return `false` to reject with 403. Omit only
253
+ * for trusted server-internal callers (e.g. the reconciliation sweep).
254
+ */
432
255
  readonly authorize?: AuthorizeReference;
433
256
  /** Plan → features/limits map. Required for `check`; omit if you don't gate features. */
434
257
  readonly entitlements?: EntitlementsConfig;
@@ -436,73 +259,162 @@ interface CreatePaymentOptions {
436
259
  readonly observability?: PaymentObserver;
437
260
  readonly store: PaymentStore;
438
261
  }
262
+ /**
263
+ * `LunoraPayment` is part of the experimental `@lunora/payment` API and may change without a major version bump.
264
+ * @experimental
265
+ */
439
266
  interface LunoraPayment {
440
267
  readonly adapter: PaymentAdapter;
441
268
  /**
442
- * Subscribe a reference to a plan — a plan-oriented alias of {@link LunoraPayment.createCheckout}
443
- * with `mode` defaulting to `"subscription"`. Returns a hosted-checkout URL to redirect to.
444
- */
269
+ * Subscribe a reference to a plan — a plan-oriented alias of {@link LunoraPayment.createCheckout}
270
+ * with `mode` defaulting to `"subscription"`. Returns a hosted-checkout URL to redirect to.
271
+ */
445
272
  attach: (input: AttachInput) => Promise<CheckoutResult>;
273
+ /** Cancel the caller's own uncaptured payment (authorized, derived idempotency key, store synced). */
274
+ cancelPayment: (sessionId: string, options?: {
275
+ idempotencyKey?: string;
276
+ }) => Promise<PaymentSession>;
446
277
  cancelSubscription: (subscriptionId: string, options?: CancelSubscriptionOptions) => Promise<Subscription>;
278
+ /** Capture the caller's own authorized payment (authorized, derived idempotency key, store synced). */
279
+ capturePayment: (input: CaptureInput) => Promise<PaymentSession>;
447
280
  /**
448
- * Is a reference allowed something right now? Pass `featureId` to check a grant/allowance (boolean
449
- * features check plan grants; metered features subtract usage tracked this period) or `priceId` to
450
- * check active access to a product. The feature path requires `entitlements` to be configured.
451
- */
281
+ * Is a reference allowed something right now? Pass `featureId` to check a grant/allowance (boolean
282
+ * features check plan grants; metered features subtract usage tracked this period) or `priceId` to
283
+ * check active access to a product. The feature path requires `entitlements` to be configured.
284
+ */
452
285
  check: (input: CheckInput) => Promise<CheckResult>;
453
286
  createCheckout: (input: CheckoutInput) => Promise<CheckoutResult>;
454
287
  /** Open the provider billing portal for the caller's own customer (derived from the store). */
455
288
  createPortalSession: (referenceId: string, returnUrl: string) => Promise<{
456
289
  url: string;
457
290
  }>;
458
- /** Verify + normalize + apply a provider webhook. Always 200 once verified, even on no-op. */
291
+ /**
292
+ * Verify + normalize + apply a provider webhook. 200 once verified, even on a no-op — except an
293
+ * event whose target row doesn't exist yet, which returns 500 so the provider redelivers it once.
294
+ */
459
295
  handleWebhook: (request: Request) => Promise<Response>;
460
296
  /** Resolve every configured feature's allowance for a reference in one call. Requires `entitlements`. */
461
297
  listBalances: (referenceId: string) => Promise<FeatureBalance[]>;
462
298
  listSubscriptions: (referenceId: string) => Promise<Subscription[]>;
299
+ /**
300
+ * Refund the caller's own captured payment (authorized, derived idempotency key, store synced).
301
+ *
302
+ * The derived key makes "same session, same amount, same reason" ONE operation, so a retried
303
+ * request cannot refund twice. Two *intentional* refunds of the same amount on one session are
304
+ * therefore indistinguishable from that retry: pass a distinct `RefundInput.idempotencyKey` for
305
+ * the second one, or the provider replays the first and the second moves no money.
306
+ */
307
+ refundPayment: (input: RefundInput) => Promise<PaymentSession>;
463
308
  readonly store: PaymentStore;
464
309
  /**
465
- * Record metered usage for a reference's feature — durably (exactly-once by idempotency key) and,
466
- * when the provider supports it, forwarded to its metering API. Best-effort upstream: a reporting
467
- * failure is observed, never thrown, and the local ledger that `check` reads is always updated.
468
- */
310
+ * Record metered usage for a reference's feature — durably (exactly-once by idempotency key) and,
311
+ * when the provider supports it, forwarded to its metering API. Best-effort upstream: a reporting
312
+ * failure is observed, never thrown, and the local ledger that `check` reads is always updated.
313
+ *
314
+ * `mode: "set"` is rejected with `VALIDATION_ERROR` on a provider that meters usage: its meter is
315
+ * additive, so a period total is not expressible on it and a lowering set would leave the provider
316
+ * billing more than the local ledger holds. Use `mode: "add"` there.
317
+ */
469
318
  track: (input: TrackInput) => Promise<TrackResult>;
470
319
  }
320
+ /**
321
+ * `createPayment` is part of the experimental `@lunora/payment` API and may change without a major version bump.
322
+ * @experimental
323
+ */
471
324
  declare const createPayment: (options: CreatePaymentOptions) => LunoraPayment;
472
- /** A stored row, carrying Lunora's document id. */
325
+ /**
326
+ * A stored row, carrying Lunora's document id.
327
+ * @experimental
328
+ */
473
329
  interface PaymentRow extends Record<string, unknown> {
474
330
  readonly _id: string;
475
331
  }
476
- /** Minimal write/read surface this store needs; `ctx.db` satisfies it structurally. */
332
+ /**
333
+ * Bounded-read arguments for {@link PaymentDatabase.findMany}, mirroring what
334
+ * Lunora's `ctx.db.findMany` already accepts. `where` stays equality-only — these
335
+ * are the knobs that let a caller bound the number of rows FETCHED rather than
336
+ * fetching everything and slicing afterwards.
337
+ * @experimental
338
+ */
339
+ interface PaymentPageArgs {
340
+ /** Keyset cursor from a previous page's {@link PaymentPage.cursor}. */
341
+ cursor?: string;
342
+ /** Maximum rows to FETCH. Omit to read every match. */
343
+ limit?: number;
344
+ /** Sort keys, pushed down to the store so the keyset cursor is well-defined. */
345
+ orderBy?: Record<string, "asc" | "desc">[];
346
+ }
347
+ /**
348
+ * One page of rows plus the cursor that continues it.
349
+ * @experimental
350
+ */
351
+ interface PaymentPage {
352
+ /** Cursor for the next page, or `undefined` when this was the last one. */
353
+ readonly cursor: string | undefined;
354
+ readonly rows: PaymentRow[];
355
+ }
356
+ /**
357
+ * Minimal write/read surface this store needs; `ctx.db` satisfies it structurally.
358
+ * @experimental
359
+ */
477
360
  interface PaymentDatabase {
478
361
  delete: (id: string) => Promise<void>;
479
362
  findFirst: (table: string, where: Record<string, unknown>) => Promise<PaymentRow | null>;
480
- findMany: (table: string, where: Record<string, unknown>) => Promise<PaymentRow[]>;
363
+ /**
364
+ * Equality-only `where`, with optional order/limit/cursor pushed DOWN to the
365
+ * store (see {@link PaymentPageArgs}). Omitting `page` reads every match — do
366
+ * that only where the match set is inherently small (one reference's rows).
367
+ */
368
+ findMany: (table: string, where: Record<string, unknown>, page?: PaymentPageArgs) => Promise<PaymentPage>;
481
369
  insert: (table: string, document: Record<string, unknown>) => Promise<string>;
482
370
  patch: (id: string, patch: Record<string, unknown>) => Promise<void>;
483
371
  }
372
+ /**
373
+ * `createDatabasePaymentStore` is part of the experimental `@lunora/payment` API and may change without a major version bump.
374
+ * @experimental
375
+ */
484
376
  declare const createDatabasePaymentStore: (database: PaymentDatabase) => PaymentStore;
485
- /** Structural subset of Lunora's `ctx.db` (the `findFirst`/`findMany(tableName, { where })` form). */
377
+ /**
378
+ * Structural subset of Lunora's `ctx.db` (the `findFirst`/`findMany(tableName, { where })` form).
379
+ *
380
+ * `findMany` models the order/limit/cursor knobs too — {@link PaymentDatabase}
381
+ * pushes them down so a sweep over a large match set reads bounded chunks
382
+ * instead of materialising the lot. `continueCursor` is REQUIRED here (`ctx.db`
383
+ * always returns it): a double that omitted it would page exactly once and then
384
+ * silently report the rest of the table as absent.
385
+ * @experimental
386
+ */
486
387
  interface LunoraDatabaseLike {
487
388
  delete: (id: string) => Promise<void>;
488
389
  findFirst: (table: string, args?: {
489
390
  where?: Record<string, unknown>;
490
391
  }) => Promise<Record<string, unknown> | null>;
491
392
  findMany: (table: string, args?: {
393
+ cursor?: string;
394
+ limit?: number;
395
+ orderBy?: Record<string, "asc" | "desc">[];
492
396
  where?: Record<string, unknown>;
493
397
  }) => Promise<{
398
+ continueCursor: null | string;
494
399
  page: Record<string, unknown>[];
495
400
  }>;
496
401
  insert: (table: string, document: Record<string, unknown>) => Promise<string>;
497
402
  patch: (id: string, patch: Record<string, unknown>) => Promise<void>;
498
403
  }
499
- /** Structural subset of a Lunora function context used to build payments. */
404
+ /**
405
+ * Structural subset of a Lunora function context used to build payments.
406
+ * @experimental
407
+ */
500
408
  interface PaymentContextLike {
501
409
  auth?: {
502
410
  userId?: null | string;
503
411
  };
504
412
  db: LunoraDatabaseLike;
505
413
  }
414
+ /**
415
+ * `PaymentsFromContextOptions` is part of the experimental `@lunora/payment` API and may change without a major version bump.
416
+ * @experimental
417
+ */
506
418
  interface PaymentsFromContextOptions {
507
419
  readonly adapter: PaymentAdapter;
508
420
  /** Override the default "caller owns the referenceId" authorization. */
@@ -512,190 +424,90 @@ interface PaymentsFromContextOptions {
512
424
  /** Optional telemetry sink, forwarded to the facade. */
513
425
  readonly observability?: PaymentObserver;
514
426
  }
515
- /** Adapt a Lunora `ctx.db` to the {@link PaymentDatabase} port the store writes through. */
427
+ /**
428
+ * Adapt a Lunora `ctx.db` to the {@link PaymentDatabase} port the store writes through.
429
+ * @experimental
430
+ */
516
431
  declare const lunoraDatabaseToPaymentDatabase: (database: LunoraDatabaseLike) => PaymentDatabase;
432
+ /**
433
+ * `paymentsFromContext` is part of the experimental `@lunora/payment` API and may change without a major version bump.
434
+ * @experimental
435
+ */
517
436
  declare const paymentsFromContext: (context: PaymentContextLike, options: PaymentsFromContextOptions) => LunoraPayment;
518
- type PaymentErrorCode = "CONFIG_INVALID" | "CURRENCY_MISMATCH" | "FORBIDDEN" | "INVALID_TRANSITION" | "NOT_FOUND" | "PROVIDER_ERROR" | "WEBHOOK_SIGNATURE_INVALID" | "WEBHOOK_TIMESTAMP_INVALID";
519
- /** Typed error for all `@lunora/payment` failures. `status` maps onto an HTTP response. */
520
- declare class LunoraPaymentError extends Error {
437
+ /**
438
+ * `PaymentErrorCode` is part of the experimental `@lunora/payment` API and may change without a major version bump.
439
+ * @experimental
440
+ */
441
+ type PaymentErrorCode = "CONFIG_INVALID" | "CURRENCY_MISMATCH" | "FORBIDDEN" | "INVALID_TRANSITION" | "NOT_FOUND" | "PROVIDER_ERROR" | "VALIDATION_ERROR" | "WEBHOOK_EVENT_ID_MISSING" | "WEBHOOK_SIGNATURE_INVALID" | "WEBHOOK_TIMESTAMP_INVALID";
442
+ /**
443
+ * Typed error for all `@lunora/payment` failures. A `LunoraError` subclass; `status` maps onto an HTTP response.
444
+ * @experimental
445
+ */
446
+ declare class LunoraPaymentError extends LunoraError {
521
447
  readonly code: PaymentErrorCode;
522
- readonly status: number;
523
448
  constructor(code: PaymentErrorCode, message: string);
524
449
  }
525
450
  /**
526
- * Outbound idempotency keys.
527
- *
528
- * Every mutating provider call carries a stable key derived from our own operation + inputs, so
529
- * a Worker retry can never double-charge. Distinct from inbound webhook dedupe (keyed on the
530
- * provider event id).
531
- */
532
- /** Build a deterministic idempotency key from an operation name and stable parts. */
451
+ * Build a deterministic idempotency key from an operation name and stable parts.
452
+ * @experimental
453
+ */
533
454
  declare const idempotencyKey: (operation: string, ...parts: ReadonlyArray<number | string>) => string;
534
- /** True when the currency has no minor unit (e.g. JPY). */
455
+ /**
456
+ * True when the currency has no minor unit (e.g. JPY).
457
+ */
535
458
  declare const isZeroDecimalCurrency: (currency: CurrencyCode) => boolean;
536
- /** Construct money. Currency is normalized to uppercase; never use floats for amounts. */
459
+ /**
460
+ * Construct money. Currency is normalized to uppercase; never use floats for amounts.
461
+ */
537
462
  declare const money: (minorUnits: bigint | number, currency: CurrencyCode) => Money;
463
+ /**
464
+ * Zero in the given currency.
465
+ */
538
466
  declare const zeroMoney: (currency: CurrencyCode) => Money;
539
- /** Localized currency string for display (e.g. `$19.99`). For UI only — never for arithmetic. */
467
+ /**
468
+ * Localized currency string for display (e.g. `$19.99`). For UI only — never for arithmetic.
469
+ */
540
470
  declare const formatMoney: (value: Money, locale?: string) => string;
471
+ /**
472
+ * Sum of two same-currency amounts. Throws `CURRENCY_MISMATCH` otherwise.
473
+ */
541
474
  declare const addMoney: (a: Money, b: Money) => Money;
475
+ /**
476
+ * Difference of two same-currency amounts. Throws `CURRENCY_MISMATCH` otherwise.
477
+ */
542
478
  declare const subtractMoney: (a: Money, b: Money) => Money;
543
- /** Compares two same-currency amounts, returning -1, 0, or 1. */
479
+ /**
480
+ * Compares two same-currency amounts, returning -1, 0, or 1.
481
+ */
544
482
  declare const compareMoney: (a: Money, b: Money) => -1 | 0 | 1;
545
483
  /**
546
- * Split an amount across integer ratios, distributing the remainder to the smallest unit so the
547
- * parts always sum back to the original. The basis for seat/proration math.
548
- */
484
+ * Split an amount across integer ratios, distributing the remainder to the smallest unit so the
485
+ * parts always sum back to the original. The basis for seat/proration math.
486
+ */
549
487
  declare const allocateMoney: (amount: Money, ratios: ReadonlyArray<bigint>) => Money[];
488
+ /**
489
+ * True when the amount is exactly zero minor units.
490
+ */
550
491
  declare const isZeroMoney: (a: Money) => boolean;
551
- /** JSON-safe wire form of money (bigint encoded as a decimal string). */
492
+ /**
493
+ * JSON-safe wire form of money (bigint encoded as a decimal string).
494
+ */
552
495
  interface MoneyJSON {
553
496
  readonly currency: CurrencyCode;
554
497
  readonly minorUnits: string;
555
498
  }
499
+ /**
500
+ * Encode money for the wire — `minorUnits` becomes a decimal string.
501
+ */
556
502
  declare const toMoneyJSON: (m: Money) => MoneyJSON;
503
+ /**
504
+ * Decode the wire form back to a `bigint`-backed {@link Money}.
505
+ */
557
506
  declare const fromMoneyJSON: (json: MoneyJSON) => Money;
558
- interface PolarSubscriptionLike {
559
- readonly cancelAtPeriodEnd?: boolean;
560
- readonly currentPeriodEnd?: null | string;
561
- readonly currentPeriodStart?: null | string;
562
- readonly customerId?: null | string;
563
- readonly id: string;
564
- readonly metadata?: Record<string, string>;
565
- readonly productId?: string;
566
- readonly status: string;
567
- }
568
- interface PolarOrderLike {
569
- readonly amount?: number;
570
- readonly currency?: string;
571
- readonly id: string;
572
- readonly status: string;
573
- readonly totalAmount?: number;
574
- }
575
- interface PolarClientLike {
576
- readonly checkouts: {
577
- create: (parameters: Record<string, unknown>) => Promise<{
578
- id: string;
579
- url: string;
580
- }>;
581
- };
582
- readonly customers: {
583
- create: (parameters: Record<string, unknown>) => Promise<{
584
- email: null | string;
585
- id: string;
586
- }>;
587
- };
588
- readonly customerSessions: {
589
- create: (parameters: Record<string, unknown>) => Promise<{
590
- customerPortalUrl: string;
591
- }>;
592
- };
593
- readonly events: {
594
- ingest: (parameters: Record<string, unknown>) => Promise<{
595
- inserted?: number;
596
- }>;
597
- };
598
- readonly orders: {
599
- get: (parameters: Record<string, unknown>) => Promise<PolarOrderLike>;
600
- };
601
- readonly refunds: {
602
- create: (parameters: Record<string, unknown>) => Promise<{
603
- id: string;
604
- }>;
605
- };
606
- readonly subscriptions: {
607
- get: (parameters: Record<string, unknown>) => Promise<PolarSubscriptionLike>;
608
- revoke: (parameters: Record<string, unknown>) => Promise<PolarSubscriptionLike>;
609
- update: (parameters: Record<string, unknown>) => Promise<PolarSubscriptionLike>;
610
- };
611
- }
612
- interface PolarAdapterOptions {
613
- readonly client: PolarClientLike;
614
- readonly webhookSecret: string;
615
- readonly webhookToleranceSeconds?: number;
616
- }
617
- declare const createPolarAdapter: (options: PolarAdapterOptions) => PaymentAdapter;
618
- interface StripeRequestOptions {
619
- readonly idempotencyKey?: string;
620
- }
621
- interface StripePaymentIntentLike {
622
- readonly amount: number;
623
- readonly amount_received?: number;
624
- readonly currency: string;
625
- readonly customer?: null | string;
626
- readonly id: string;
627
- readonly metadata?: Record<string, string>;
628
- readonly status: string;
629
- }
630
- interface StripeSubscriptionLike {
631
- readonly cancel_at_period_end?: boolean;
632
- readonly current_period_end?: number;
633
- readonly current_period_start?: number;
634
- readonly customer?: null | string;
635
- readonly id: string;
636
- readonly items?: {
637
- data: ReadonlyArray<{
638
- price?: {
639
- id?: string;
640
- };
641
- quantity?: number;
642
- }>;
643
- };
644
- readonly metadata?: Record<string, string>;
645
- readonly status: string;
646
- }
647
- /** The subset of the Stripe SDK surface this adapter calls. A real `Stripe` instance satisfies it. */
648
- interface StripeClientLike {
649
- readonly billing: {
650
- meterEvents: {
651
- create: (parameters: Record<string, unknown>, options?: StripeRequestOptions) => Promise<{
652
- identifier?: string;
653
- }>;
654
- };
655
- };
656
- readonly billingPortal: {
657
- sessions: {
658
- create: (parameters: Record<string, unknown>) => Promise<{
659
- url: string;
660
- }>;
661
- };
662
- };
663
- readonly checkout: {
664
- sessions: {
665
- create: (parameters: Record<string, unknown>, options?: StripeRequestOptions) => Promise<{
666
- id: string;
667
- url: null | string;
668
- }>;
669
- };
670
- };
671
- readonly customers: {
672
- create: (parameters: Record<string, unknown>, options?: StripeRequestOptions) => Promise<{
673
- email: null | string;
674
- id: string;
675
- }>;
676
- };
677
- readonly paymentIntents: {
678
- cancel: (id: string, parameters?: Record<string, unknown>, options?: StripeRequestOptions) => Promise<StripePaymentIntentLike>;
679
- capture: (id: string, parameters?: Record<string, unknown>, options?: StripeRequestOptions) => Promise<StripePaymentIntentLike>;
680
- retrieve: (id: string) => Promise<StripePaymentIntentLike>;
681
- };
682
- readonly refunds: {
683
- create: (parameters: Record<string, unknown>, options?: StripeRequestOptions) => Promise<{
684
- id: string;
685
- }>;
686
- };
687
- readonly subscriptions: {
688
- cancel: (id: string, parameters?: Record<string, unknown>, options?: StripeRequestOptions) => Promise<StripeSubscriptionLike>;
689
- retrieve: (id: string) => Promise<StripeSubscriptionLike>;
690
- update: (id: string, parameters: Record<string, unknown>, options?: StripeRequestOptions) => Promise<StripeSubscriptionLike>;
691
- };
692
- }
693
- interface StripeAdapterOptions {
694
- readonly client: StripeClientLike;
695
- readonly webhookSecret: string;
696
- readonly webhookToleranceSeconds?: number;
697
- }
698
- declare const createStripeAdapter: (options: StripeAdapterOptions) => PaymentAdapter;
507
+ /**
508
+ * `ReconcileInput` is part of the experimental `@lunora/payment` API and may change without a major version bump.
509
+ * @experimental
510
+ */
699
511
  interface ReconcileInput {
700
512
  readonly adapter: PaymentAdapter;
701
513
  /** Optional telemetry sink — fired per drifted row and once on completion. */
@@ -703,56 +515,107 @@ interface ReconcileInput {
703
515
  readonly paymentSessionIds?: ReadonlyArray<string>;
704
516
  readonly store: PaymentStore;
705
517
  readonly subscriptionIds?: ReadonlyArray<string>;
518
+ /**
519
+ * How many unreported usage events to retry forwarding upstream this sweep
520
+ * (oldest first). Default {@link DEFAULT_USAGE_REPORT_LIMIT}; `0` skips the
521
+ * usage sweep entirely. Ignored by an adapter that does not meter usage.
522
+ */
523
+ readonly usageReportLimit?: number;
706
524
  }
525
+ /**
526
+ * `ReconcileResult` is part of the experimental `@lunora/payment` API and may change without a major version bump.
527
+ * @experimental
528
+ */
707
529
  interface ReconcileResult {
708
530
  readonly checkedPayments: number;
709
531
  readonly checkedSubscriptions: number;
532
+ readonly checkedUsage: number;
710
533
  readonly failedPayments: number;
711
534
  readonly failedSubscriptions: number;
535
+ /** Usage events whose retried forward failed again — still pending for the next sweep. */
536
+ readonly failedUsage: number;
712
537
  readonly updatedPayments: number;
713
538
  readonly updatedSubscriptions: number;
539
+ /** Usage events successfully forwarded upstream on this sweep. */
540
+ readonly updatedUsage: number;
714
541
  }
542
+ /**
543
+ * `reconcile` is part of the experimental `@lunora/payment` API and may change without a major version bump.
544
+ * @experimental
545
+ */
715
546
  declare const reconcile: (input: ReconcileInput) => Promise<ReconcileResult>;
547
+ /**
548
+ * The canonical column reference for the payment tables — a value to READ (in a test, a migration
549
+ * check, or your editor), not one to spread. `defineSchema({ ...paymentTables })` does NOT work:
550
+ * codegen discovers tables by parsing your `lunora/schema.ts` AST and cannot resolve a
551
+ * cross-package spread, so declare the same columns inline there (see the module docstring).
552
+ *
553
+ * `paymentTables` is part of the experimental `@lunora/payment` API and may change without a major version bump.
554
+ * @experimental
555
+ */
716
556
  declare const paymentTables: Record<string, TableDefinition>;
717
- /** Action that may advance a payment session. */
557
+ /**
558
+ * Action that may advance a payment session.
559
+ * @experimental
560
+ */
718
561
  type PaymentAction = "authorize" | "cancel" | "capture" | "fail" | "partial_refund" | "refund";
719
- /** Action that may advance a subscription. */
562
+ /**
563
+ * Action that may advance a subscription.
564
+ * @experimental
565
+ */
720
566
  type SubscriptionAction = "activate" | "cancel" | "mark_past_due" | "pause" | "renew" | "resume";
567
+ /**
568
+ * `PAYMENT_TERMINAL_STATES` is part of the experimental `@lunora/payment` API and may change without a major version bump.
569
+ * @experimental
570
+ */
721
571
  declare const PAYMENT_TERMINAL_STATES: ReadonlySet<PaymentState>;
572
+ /**
573
+ * `SUBSCRIPTION_TERMINAL_STATES` is part of the experimental `@lunora/payment` API and may change without a major version bump.
574
+ * @experimental
575
+ */
722
576
  declare const SUBSCRIPTION_TERMINAL_STATES: ReadonlySet<SubscriptionState>;
723
- /** Next payment state for an action, or `undefined` if the transition is illegal from `from`. */
577
+ /**
578
+ * Next payment state for an action, or `undefined` if the transition is illegal from `from`.
579
+ * @experimental
580
+ */
724
581
  declare const nextPaymentState: (from: PaymentState, action: PaymentAction) => PaymentState | undefined;
582
+ /**
583
+ * `canTransitionPayment` is part of the experimental `@lunora/payment` API and may change without a major version bump.
584
+ * @experimental
585
+ */
725
586
  declare const canTransitionPayment: (from: PaymentState, action: PaymentAction) => boolean;
726
- /** Next subscription state for an action, or `undefined` if the transition is illegal from `from`. */
587
+ /**
588
+ * Next subscription state for an action, or `undefined` if the transition is illegal from `from`.
589
+ * @experimental
590
+ */
727
591
  declare const nextSubscriptionState: (from: SubscriptionState, action: SubscriptionAction) => SubscriptionState | undefined;
592
+ /**
593
+ * `canTransitionSubscription` is part of the experimental `@lunora/payment` API and may change without a major version bump.
594
+ * @experimental
595
+ */
728
596
  declare const canTransitionSubscription: (from: SubscriptionState, action: SubscriptionAction) => boolean;
597
+ /**
598
+ * `applyWebhookAction` is part of the experimental `@lunora/payment` API and may change without a major version bump.
599
+ * @experimental
600
+ */
729
601
  declare const applyWebhookAction: (store: PaymentStore, action: WebhookAction, observer?: PaymentObserver) => Promise<ApplyResult>;
730
- /** Constant-time string comparison to avoid leaking byte positions via timing. */
602
+ /**
603
+ * Constant-time string comparison to avoid leaking byte positions via timing.
604
+ *
605
+ * Re-exported from `shared/constant-time-equal` rather than defined here. The
606
+ * copy that used to live at this spot returned early on a length mismatch, so
607
+ * an attacker learned the expected signature's LENGTH from the response time —
608
+ * the exact divergence that shared file was created to stop, and which it
609
+ * records having already fixed once in the relay hub. It also compared by code
610
+ * POINT, making timing depend on surrogate boundaries; the canonical one
611
+ * compares per UTF-16 code unit.
612
+ */
731
613
  declare const constantTimeEqual: (a: string, b: string) => boolean;
732
- declare const hmacSha256Hex: (secret: string, payload: string) => Promise<string>;
733
- interface StripeSignatureParts {
734
- readonly signatures: string[];
735
- readonly timestamp: number;
736
- }
737
- /** Parse a Stripe-style `t=...,v1=...,v1=...` signature header. */
738
- declare const parseStripeSignatureHeader: (header: string) => StripeSignatureParts;
739
- interface VerifyStripeSignatureInput {
740
- /** Injectable clock (ms since epoch) for tests. */
741
- readonly now?: number;
742
- /** Raw request body, exactly as received. */
743
- readonly payload: string;
744
- readonly secret: string;
745
- /** The `Stripe-Signature` header value. */
746
- readonly signatureHeader: string;
747
- /** Whole-second tolerance for the signed timestamp (default 300). */
748
- readonly toleranceSeconds?: number;
749
- }
750
614
  /**
751
- * Verify a Stripe-scheme webhook signature: `HMAC_SHA256(secret, "{t}.{payload}")` compared
752
- * against the header's `v1` values, with a timestamp tolerance to reject replays. Throws a
753
- * {@link LunoraPaymentError} on any failure.
754
- */
755
- declare const verifyStripeSignature: (input: VerifyStripeSignatureInput) => Promise<void>;
615
+ * Hex-encoded `HMAC_SHA256(secret, payload)` — the primitive behind the hex-scheme
616
+ * webhook verifiers.
617
+ */
618
+ declare const hmacSha256Hex: (secret: string, payload: string) => Promise<string>;
756
619
  interface VerifyStandardWebhookInput {
757
620
  /** Injectable clock (ms since epoch) for tests. */
758
621
  readonly now?: number;
@@ -764,15 +627,29 @@ interface VerifyStandardWebhookInput {
764
627
  readonly toleranceSeconds?: number;
765
628
  /** `webhook-id` header. */
766
629
  readonly webhookId: string;
767
- /** `webhook-signature` header — space-separated `v1,&lt;base64>` entries. */
630
+ /** `webhook-signature` header — space-separated `v1,<base64>` entries. */
768
631
  readonly webhookSignature: string;
769
632
  /** `webhook-timestamp` header — unix seconds as a string. */
770
633
  readonly webhookTimestamp: string;
771
634
  }
772
635
  /**
773
- * Verify a Standard Webhooks signature (the scheme Polar and svix use):
774
- * `base64(HMAC_SHA256(key, "{id}.{timestamp}.{payload}"))` compared against the header's `v1`
775
- * entries, with a replay-window check. Throws a {@link LunoraPaymentError} on any failure.
776
- */
636
+ * Verify a Standard Webhooks signature (the scheme Polar and svix use):
637
+ * `base64(HMAC_SHA256(key, "{id}.{timestamp}.{payload}"))` compared against the header's `v1`
638
+ * entries, with a replay-window check. Throws a {@link LunoraPaymentError} on any failure.
639
+ */
777
640
  declare const verifyStandardWebhook: (input: VerifyStandardWebhookInput) => Promise<void>;
778
- export { type AdapterRegistry, type ApplyResult, type AttachInput, type AuthorizeReference, type CancelSubscriptionOptions, type CaptureInput, type CheckInput, type CheckResult, type CheckoutInput, type CheckoutResult, type CreatePaymentOptions, type CurrencyCode, type Customer, type CustomerRef, type Entitlements, type EntitlementsConfig, type FeatureBalance, type LunoraDatabaseLike, type LunoraPayment, LunoraPaymentError, MemoryPaymentStore, type Money, type MoneyJSON, PAYMENT_TERMINAL_STATES, type PaymentAction, type PaymentAdapter, type PaymentContextLike, type PaymentDatabase, type PaymentErrorCode, type PaymentEvent, type PaymentObserver, type PaymentRow, type PaymentSession, type PaymentState, type PaymentStore, type PaymentsFromContextOptions, type PlanDefinition, type PolarAdapterOptions, type PolarClientLike, type PortalInput, type ProviderCapabilities, type ProviderId, type ReconcileInput, type ReconcileResult, type RefundAmountKind, type RefundInput, type ReportUsageInput, SUBSCRIPTION_TERMINAL_STATES, type StripeAdapterOptions, type StripeClientLike, type Subscription, type SubscriptionAction, type SubscriptionPatch, type SubscriptionState, type TrackInput, type TrackResult, type UsageEvent, type WebhookAction, type WebhookActionType, type WebhookHeaders, type WebhookInput, addMoney, allocateMoney, applyWebhookAction, canTransitionPayment, canTransitionSubscription, compareMoney, constantTimeEqual, createAdapterRegistry, createDatabasePaymentStore, createPayment, createPolarAdapter, createStripeAdapter, entitlementsForReference, featureNames, formatMoney, fromMoneyJSON, hasActivePrice, hmacSha256Hex, idempotencyKey, isZeroDecimalCurrency, isZeroMoney, lunoraDatabaseToPaymentDatabase, money, nextPaymentState, nextSubscriptionState, parseStripeSignatureHeader, paymentTables, paymentsFromContext, reconcile, resolveEntitlements, subtractMoney, toMoneyJSON, usagePeriodStart, verifyStandardWebhook, verifyStripeSignature, zeroMoney };
641
+ interface VerifyCreemSignatureInput {
642
+ /** Raw request body, exactly as received. */
643
+ readonly payload: string;
644
+ /** Creem webhook signing secret. */
645
+ readonly secret: string;
646
+ /** The `creem-signature` header value. */
647
+ readonly signature: string;
648
+ }
649
+ /**
650
+ * Verify a Creem webhook signature: `hex(HMAC_SHA256(secret, rawBody))` compared against the
651
+ * `creem-signature` header. Creem's scheme signs the raw body with no timestamp, so there is no
652
+ * replay-window check. Throws a {@link LunoraPaymentError} on any failure.
653
+ */
654
+ declare const verifyCreemSignature: (input: VerifyCreemSignatureInput) => Promise<void>;
655
+ export { type ApplyResult, type AttachInput, type AuthorizeReference, type CancelSubscriptionOptions, type CaptureInput, type CheckInput, type CheckResult, type CheckoutInput, type CheckoutResult, type CreatePaymentOptions, type CurrencyCode, type Customer, type Entitlements, type EntitlementsConfig, type FeatureBalance, type LunoraDatabaseLike, type LunoraPayment, LunoraPaymentError, MemoryPaymentStore, type Money, type MoneyJSON, PAYMENT_TERMINAL_STATES, type PaymentAction, type PaymentAdapter, type PaymentContextLike, type PaymentDatabase, type PaymentErrorCode, type PaymentEvent, type PaymentObserver, type PaymentPage, type PaymentPageArgs, type PaymentRow, type PaymentSession, type PaymentState, type PaymentStore, type PaymentsFromContextOptions, type PlanDefinition, type ProviderId, type ReconcileInput, type ReconcileResult, type RefundInput, SUBSCRIPTION_TERMINAL_STATES, type Subscription, type SubscriptionAction, type SubscriptionState, type TrackInput, type TrackResult, type UsageEvent, type VerifyCreemSignatureInput, type VerifyStandardWebhookInput, type WebhookAction, type WebhookActionType, type WebhookOutcome, addMoney, allocateMoney, applyWebhookAction, canTransitionPayment, canTransitionSubscription, compareMoney, constantTimeEqual, createDatabasePaymentStore, createPayment, entitlementsForReference, featureNames, formatMoney, fromMoneyJSON, hasActivePrice, hmacSha256Hex, idempotencyKey, isZeroDecimalCurrency, isZeroMoney, lunoraDatabaseToPaymentDatabase, money, nextPaymentState, nextSubscriptionState, paymentTables, paymentsFromContext, reconcile, resolveEntitlements, subtractMoney, toMoneyJSON, usagePeriodStart, verifyCreemSignature, verifyStandardWebhook, webhookResponse, zeroMoney };