@basaltkit/subscriptions 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +374 -0
- package/dist/index.d.ts +476 -0
- package/dist/index.js +718 -0
- package/package.json +54 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
import * as _basaltkit_core from '@basaltkit/core';
|
|
2
|
+
import { DurationInput, BasaltError, HookBus } from '@basaltkit/core';
|
|
3
|
+
import { BasaltRoute } from '@basaltkit/fastify';
|
|
4
|
+
|
|
5
|
+
declare class UnknownPlanError extends BasaltError {
|
|
6
|
+
constructor(plan: string);
|
|
7
|
+
}
|
|
8
|
+
/** Metered feature: the limit resets every billing month. */
|
|
9
|
+
interface Meter {
|
|
10
|
+
readonly meter: true;
|
|
11
|
+
readonly limit: number;
|
|
12
|
+
}
|
|
13
|
+
/** `'api.requests': meter(100_000)` — consumed monthly. */
|
|
14
|
+
declare function meter(limit: number): Meter;
|
|
15
|
+
/**
|
|
16
|
+
* boolean → on/off flag · number → consumable balance (lifetime)
|
|
17
|
+
* meter(n) → monthly-reset quota · Infinity → unlimited
|
|
18
|
+
*/
|
|
19
|
+
type FeatureValue = boolean | number | Meter;
|
|
20
|
+
type BillingPeriod = 'monthly' | 'yearly';
|
|
21
|
+
interface PlanDefinition {
|
|
22
|
+
/** 0 = free · number = same price both periods · object = per period · 'custom' = sales-led */
|
|
23
|
+
price: number | {
|
|
24
|
+
monthly: number;
|
|
25
|
+
yearly: number;
|
|
26
|
+
} | 'custom';
|
|
27
|
+
trial?: DurationInput;
|
|
28
|
+
features: Record<string, FeatureValue>;
|
|
29
|
+
}
|
|
30
|
+
type Plans = Record<string, PlanDefinition>;
|
|
31
|
+
declare function definePlans<T extends Plans>(plans: T): T;
|
|
32
|
+
declare function planPrice(plan: PlanDefinition, period: BillingPeriod): number | 'custom';
|
|
33
|
+
/** Normalized limit of a feature: false→0, true→Infinity. */
|
|
34
|
+
declare function featureLimit(value: FeatureValue | undefined): number;
|
|
35
|
+
declare function isMeter(value: FeatureValue | undefined): value is Meter;
|
|
36
|
+
|
|
37
|
+
type SubscriptionStatus = 'active' | 'trialing' | 'past_due' | 'canceled' | 'incomplete';
|
|
38
|
+
interface SubscriptionRecord {
|
|
39
|
+
/** The billable entity — the tenant id by convention. */
|
|
40
|
+
billableId: string;
|
|
41
|
+
plan: string;
|
|
42
|
+
period: BillingPeriod;
|
|
43
|
+
status: SubscriptionStatus;
|
|
44
|
+
trialEndsAt?: number;
|
|
45
|
+
cancelAtPeriodEnd?: boolean;
|
|
46
|
+
canceledAt?: number;
|
|
47
|
+
/** Reference in the payment gateway (e.g. Stripe subscription id). */
|
|
48
|
+
gatewayRef?: string;
|
|
49
|
+
}
|
|
50
|
+
interface SubscriptionStore {
|
|
51
|
+
get(billableId: string): Promise<SubscriptionRecord | null>;
|
|
52
|
+
save(record: SubscriptionRecord): Promise<void>;
|
|
53
|
+
all(): Promise<SubscriptionRecord[]>;
|
|
54
|
+
}
|
|
55
|
+
declare class MemorySubscriptionStore implements SubscriptionStore {
|
|
56
|
+
private readonly records;
|
|
57
|
+
get(billableId: string): Promise<SubscriptionRecord | null>;
|
|
58
|
+
save(record: SubscriptionRecord): Promise<void>;
|
|
59
|
+
all(): Promise<SubscriptionRecord[]>;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Deduplicates webhook processing across restarts and instances. `markProcessed`
|
|
63
|
+
* claims an event id (true = new, process it; false = already seen, skip);
|
|
64
|
+
* `release` frees the claim so a failed apply can be retried by the gateway.
|
|
65
|
+
*/
|
|
66
|
+
interface WebhookStore {
|
|
67
|
+
markProcessed(id: string): Promise<boolean>;
|
|
68
|
+
release(id: string): Promise<void>;
|
|
69
|
+
}
|
|
70
|
+
declare class MemoryWebhookStore implements WebhookStore {
|
|
71
|
+
private readonly seen;
|
|
72
|
+
markProcessed(id: string): Promise<boolean>;
|
|
73
|
+
release(id: string): Promise<void>;
|
|
74
|
+
}
|
|
75
|
+
interface UsageConsumeResult {
|
|
76
|
+
/** Whether the increment was applied (false when it would exceed the limit). */
|
|
77
|
+
applied: boolean;
|
|
78
|
+
/** Usage after the operation (unchanged when not applied). */
|
|
79
|
+
used: number;
|
|
80
|
+
}
|
|
81
|
+
/** Usage counters: `periodKey` is 'lifetime' or 'YYYY-MM' for meters. */
|
|
82
|
+
interface UsageStore {
|
|
83
|
+
get(billableId: string, feature: string, periodKey: string): Promise<number>;
|
|
84
|
+
/** Unconditional increment — used for unlimited features. Returns the new total. */
|
|
85
|
+
increment(billableId: string, feature: string, periodKey: string, amount: number): Promise<number>;
|
|
86
|
+
/**
|
|
87
|
+
* Atomically increments by `amount` only if the result stays within `limit`.
|
|
88
|
+
* Must be atomic under concurrency so a quota is never overshot.
|
|
89
|
+
*/
|
|
90
|
+
consume(billableId: string, feature: string, periodKey: string, amount: number, limit: number): Promise<UsageConsumeResult>;
|
|
91
|
+
}
|
|
92
|
+
declare class MemoryUsageStore implements UsageStore {
|
|
93
|
+
private readonly counters;
|
|
94
|
+
private key;
|
|
95
|
+
get(billableId: string, feature: string, periodKey: string): Promise<number>;
|
|
96
|
+
increment(billableId: string, feature: string, periodKey: string, amount: number): Promise<number>;
|
|
97
|
+
consume(billableId: string, feature: string, periodKey: string, amount: number, limit: number): Promise<UsageConsumeResult>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Minimal ioredis-compatible surface — inject your client, no hard dependency. */
|
|
101
|
+
interface RedisLike {
|
|
102
|
+
get(key: string): Promise<string | null>;
|
|
103
|
+
eval(script: string, numKeys: number, ...args: (string | number)[]): Promise<unknown>;
|
|
104
|
+
}
|
|
105
|
+
interface RedisUsageStoreOptions {
|
|
106
|
+
/** Key prefix. Default: 'basalt:usage'. */
|
|
107
|
+
prefix?: string;
|
|
108
|
+
/**
|
|
109
|
+
* TTL in seconds for periodic (monthly) counters, so old buckets are
|
|
110
|
+
* reclaimed. Lifetime counters never expire. Default: 60 days.
|
|
111
|
+
*/
|
|
112
|
+
ttlSeconds?: number;
|
|
113
|
+
}
|
|
114
|
+
declare class RedisUsageStore implements UsageStore {
|
|
115
|
+
private readonly redis;
|
|
116
|
+
private readonly prefix;
|
|
117
|
+
private readonly ttlSeconds;
|
|
118
|
+
constructor(redis: RedisLike, options?: RedisUsageStoreOptions);
|
|
119
|
+
private key;
|
|
120
|
+
private ttlFor;
|
|
121
|
+
get(billableId: string, feature: string, periodKey: string): Promise<number>;
|
|
122
|
+
increment(billableId: string, feature: string, periodKey: string, amount: number): Promise<number>;
|
|
123
|
+
consume(billableId: string, feature: string, periodKey: string, amount: number, limit: number): Promise<UsageConsumeResult>;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Minimal ioredis-compatible surface for webhook dedupe. */
|
|
127
|
+
interface RedisWebhookClient {
|
|
128
|
+
set(key: string, value: string, ex: 'EX', seconds: number, nx: 'NX'): Promise<'OK' | null>;
|
|
129
|
+
del(key: string): Promise<number>;
|
|
130
|
+
}
|
|
131
|
+
interface RedisWebhookStoreOptions {
|
|
132
|
+
/** Key prefix. Default: 'basalt:webhook'. */
|
|
133
|
+
prefix?: string;
|
|
134
|
+
/**
|
|
135
|
+
* How long a processed id stays deduplicated, in seconds. Cover the
|
|
136
|
+
* gateway's retry window with margin. Default: 7 days.
|
|
137
|
+
*/
|
|
138
|
+
ttlSeconds?: number;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Durable webhook dedupe via Redis `SET key value NX EX` — the atomic claim
|
|
142
|
+
* returns 'OK' only for the first caller, so idempotency holds across process
|
|
143
|
+
* restarts and multiple instances.
|
|
144
|
+
*/
|
|
145
|
+
declare class RedisWebhookStore implements WebhookStore {
|
|
146
|
+
private readonly redis;
|
|
147
|
+
private readonly prefix;
|
|
148
|
+
private readonly ttlSeconds;
|
|
149
|
+
constructor(redis: RedisWebhookClient, options?: RedisWebhookStoreOptions);
|
|
150
|
+
private key;
|
|
151
|
+
markProcessed(id: string): Promise<boolean>;
|
|
152
|
+
release(id: string): Promise<void>;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
declare class WebhookInvalidError extends BasaltError {
|
|
156
|
+
readonly status = 400;
|
|
157
|
+
constructor();
|
|
158
|
+
}
|
|
159
|
+
/** Gateway-agnostic webhook event, already translated to domain terms. */
|
|
160
|
+
interface WebhookEvent {
|
|
161
|
+
/** Unique id at the gateway — used for idempotent processing. */
|
|
162
|
+
id: string;
|
|
163
|
+
type: 'subscription.canceled' | 'payment.failed' | 'payment.succeeded';
|
|
164
|
+
billableId: string;
|
|
165
|
+
/** Gateway subscription id, when the event carries one (e.g. after Checkout). */
|
|
166
|
+
gatewayRef?: string;
|
|
167
|
+
}
|
|
168
|
+
interface CreateSubscriptionInput {
|
|
169
|
+
billableId: string;
|
|
170
|
+
plan: string;
|
|
171
|
+
period: BillingPeriod;
|
|
172
|
+
price: number;
|
|
173
|
+
/**
|
|
174
|
+
* Trial length in days. When set, the gateway runs the trial and charges at
|
|
175
|
+
* its end, driving the trial→active/past_due transition via webhook.
|
|
176
|
+
*/
|
|
177
|
+
trialDays?: number;
|
|
178
|
+
}
|
|
179
|
+
/** Input for a hosted Checkout session (the customer enters payment there). */
|
|
180
|
+
interface CheckoutInput {
|
|
181
|
+
billableId: string;
|
|
182
|
+
plan: string;
|
|
183
|
+
period: BillingPeriod;
|
|
184
|
+
successUrl: string;
|
|
185
|
+
cancelUrl: string;
|
|
186
|
+
trialDays?: number;
|
|
187
|
+
}
|
|
188
|
+
/** Input for a Customer Portal session (self-service card/cancel management). */
|
|
189
|
+
interface PortalInput {
|
|
190
|
+
billableId: string;
|
|
191
|
+
returnUrl: string;
|
|
192
|
+
}
|
|
193
|
+
/** Input for changing a subscription's plan mid-cycle with proration. */
|
|
194
|
+
interface SwapInput {
|
|
195
|
+
plan: string;
|
|
196
|
+
period: BillingPeriod;
|
|
197
|
+
/** How the gateway settles the mid-cycle difference. Default create_prorations. */
|
|
198
|
+
prorationBehavior?: 'create_prorations' | 'none' | 'always_invoice';
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Payment gateway driver contract. The app talks to Basalt; only drivers
|
|
202
|
+
* talk to Stripe/Paddle/Lemon Squeezy. A driver translates raw webhook
|
|
203
|
+
* payloads into WebhookEvent — app code never sees gateway payloads.
|
|
204
|
+
*/
|
|
205
|
+
interface BillingGateway {
|
|
206
|
+
readonly name: string;
|
|
207
|
+
createSubscription(input: CreateSubscriptionInput): Promise<{
|
|
208
|
+
gatewayRef: string;
|
|
209
|
+
}>;
|
|
210
|
+
cancelSubscription(gatewayRef: string, options: {
|
|
211
|
+
atPeriodEnd: boolean;
|
|
212
|
+
}): Promise<void>;
|
|
213
|
+
/**
|
|
214
|
+
* Verifies the signature and translates the payload. Throws
|
|
215
|
+
* WebhookInvalidError on a bad signature. Returns null for a verified event
|
|
216
|
+
* the gateway doesn't map to a domain event (gateways emit many event types
|
|
217
|
+
* we don't act on).
|
|
218
|
+
*/
|
|
219
|
+
verifyWebhook(rawBody: string, signature: string | undefined): WebhookEvent | null;
|
|
220
|
+
/** Hosted Checkout session — returns a URL to redirect the customer to. */
|
|
221
|
+
createCheckoutSession?(input: CheckoutInput): Promise<{
|
|
222
|
+
url: string;
|
|
223
|
+
id: string;
|
|
224
|
+
}>;
|
|
225
|
+
/** Customer Portal session — returns a URL for self-service billing. */
|
|
226
|
+
createPortalSession?(input: PortalInput): Promise<{
|
|
227
|
+
url: string;
|
|
228
|
+
}>;
|
|
229
|
+
/** Changes the plan on an existing subscription, applying proration. */
|
|
230
|
+
swapSubscription?(gatewayRef: string, input: SwapInput): Promise<void>;
|
|
231
|
+
}
|
|
232
|
+
/** Controllable in-process gateway — the test/dev driver. */
|
|
233
|
+
declare class FakeBillingGateway implements BillingGateway {
|
|
234
|
+
readonly name = "fake";
|
|
235
|
+
readonly created: CreateSubscriptionInput[];
|
|
236
|
+
readonly canceled: {
|
|
237
|
+
gatewayRef: string;
|
|
238
|
+
atPeriodEnd: boolean;
|
|
239
|
+
}[];
|
|
240
|
+
readonly checkouts: CheckoutInput[];
|
|
241
|
+
readonly portals: PortalInput[];
|
|
242
|
+
readonly swaps: {
|
|
243
|
+
gatewayRef: string;
|
|
244
|
+
input: SwapInput;
|
|
245
|
+
}[];
|
|
246
|
+
private counter;
|
|
247
|
+
createSubscription(input: CreateSubscriptionInput): Promise<{
|
|
248
|
+
gatewayRef: string;
|
|
249
|
+
}>;
|
|
250
|
+
cancelSubscription(gatewayRef: string, options: {
|
|
251
|
+
atPeriodEnd: boolean;
|
|
252
|
+
}): Promise<void>;
|
|
253
|
+
createCheckoutSession(input: CheckoutInput): Promise<{
|
|
254
|
+
url: string;
|
|
255
|
+
id: string;
|
|
256
|
+
}>;
|
|
257
|
+
createPortalSession(input: PortalInput): Promise<{
|
|
258
|
+
url: string;
|
|
259
|
+
}>;
|
|
260
|
+
swapSubscription(gatewayRef: string, input: SwapInput): Promise<void>;
|
|
261
|
+
verifyWebhook(rawBody: string, signature: string | undefined): WebhookEvent;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
declare class StripeRequestError extends BasaltError {
|
|
265
|
+
readonly httpStatus: number;
|
|
266
|
+
constructor(httpStatus: number, message: string);
|
|
267
|
+
}
|
|
268
|
+
interface StripeGatewayOptions {
|
|
269
|
+
secretKey: string;
|
|
270
|
+
/** The endpoint signing secret (`whsec_...`) used to verify webhooks. */
|
|
271
|
+
webhookSecret: string;
|
|
272
|
+
/** Resolves the Stripe Price ID for a plan + billing period. */
|
|
273
|
+
priceId: (plan: string, period: BillingPeriod) => string;
|
|
274
|
+
/** Resolves (or ensures) the Stripe Customer ID for a billable entity. */
|
|
275
|
+
customerId: (billableId: string) => string | Promise<string>;
|
|
276
|
+
/**
|
|
277
|
+
* Extracts the billable id from a verified event. Default: reads
|
|
278
|
+
* `data.object.metadata.billableId` — which createSubscription sets on the
|
|
279
|
+
* subscription. Override for events whose object carries it elsewhere.
|
|
280
|
+
*/
|
|
281
|
+
resolveBillableId?: (event: unknown) => string | undefined;
|
|
282
|
+
/** Webhook timestamp tolerance in seconds. Default: 300 (5 minutes). */
|
|
283
|
+
tolerance?: number;
|
|
284
|
+
/** Injected fetch (tests). Default: global fetch. */
|
|
285
|
+
fetch?: typeof fetch;
|
|
286
|
+
/** Clock in ms (tests). Default: Date.now. */
|
|
287
|
+
now?: () => number;
|
|
288
|
+
/** API base, for tests/mocks. Default: https://api.stripe.com */
|
|
289
|
+
apiBase?: string;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Stripe billing gateway targeting the Stripe REST API directly — no `stripe`
|
|
293
|
+
* SDK dependency. HTTP goes through an injectable fetch; webhook signatures are
|
|
294
|
+
* verified with node:crypto using Stripe's documented scheme.
|
|
295
|
+
*/
|
|
296
|
+
declare class StripeBillingGateway implements BillingGateway {
|
|
297
|
+
private readonly options;
|
|
298
|
+
readonly name = "stripe";
|
|
299
|
+
private readonly fetch;
|
|
300
|
+
private readonly now;
|
|
301
|
+
private readonly tolerance;
|
|
302
|
+
private readonly apiBase;
|
|
303
|
+
private readonly resolveBillableId;
|
|
304
|
+
constructor(options: StripeGatewayOptions);
|
|
305
|
+
createSubscription(input: CreateSubscriptionInput): Promise<{
|
|
306
|
+
gatewayRef: string;
|
|
307
|
+
}>;
|
|
308
|
+
cancelSubscription(gatewayRef: string, options: {
|
|
309
|
+
atPeriodEnd: boolean;
|
|
310
|
+
}): Promise<void>;
|
|
311
|
+
createCheckoutSession(input: CheckoutInput): Promise<{
|
|
312
|
+
url: string;
|
|
313
|
+
id: string;
|
|
314
|
+
}>;
|
|
315
|
+
createPortalSession(input: PortalInput): Promise<{
|
|
316
|
+
url: string;
|
|
317
|
+
}>;
|
|
318
|
+
swapSubscription(gatewayRef: string, input: SwapInput): Promise<void>;
|
|
319
|
+
verifyWebhook(rawBody: string, signature: string | undefined): WebhookEvent | null;
|
|
320
|
+
private request;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
declare class NotSubscribedError extends BasaltError {
|
|
324
|
+
readonly status = 402;
|
|
325
|
+
constructor();
|
|
326
|
+
}
|
|
327
|
+
declare class FeatureUnavailableError extends BasaltError {
|
|
328
|
+
readonly status = 403;
|
|
329
|
+
constructor(feature: string);
|
|
330
|
+
}
|
|
331
|
+
declare class QuotaExceededError extends BasaltError {
|
|
332
|
+
readonly status = 402;
|
|
333
|
+
constructor(feature: string, remaining: number);
|
|
334
|
+
}
|
|
335
|
+
/** A billing action needs a gateway (or gateway capability) that isn't configured. */
|
|
336
|
+
declare class GatewayUnsupportedError extends BasaltError {
|
|
337
|
+
readonly status = 501;
|
|
338
|
+
constructor(capability: string);
|
|
339
|
+
}
|
|
340
|
+
interface SubscriptionsOptions {
|
|
341
|
+
plans: Plans;
|
|
342
|
+
store?: SubscriptionStore;
|
|
343
|
+
usage?: UsageStore;
|
|
344
|
+
gateway?: BillingGateway;
|
|
345
|
+
/** Webhook dedupe store. Default: in-memory (per-process). */
|
|
346
|
+
webhooks?: WebhookStore;
|
|
347
|
+
/** Plan applied to billables without a subscription (e.g. 'free'). */
|
|
348
|
+
fallbackPlan?: string;
|
|
349
|
+
hooks?: HookBus;
|
|
350
|
+
}
|
|
351
|
+
declare class Subscriptions {
|
|
352
|
+
private readonly plans;
|
|
353
|
+
private readonly store;
|
|
354
|
+
private readonly usage;
|
|
355
|
+
private readonly gateway;
|
|
356
|
+
private readonly fallbackPlan;
|
|
357
|
+
private readonly hooks;
|
|
358
|
+
private readonly webhooks;
|
|
359
|
+
constructor(options: SubscriptionsOptions);
|
|
360
|
+
plan(name: string): PlanDefinition;
|
|
361
|
+
subscribe(billableId: string, planName: string, options?: {
|
|
362
|
+
period?: BillingPeriod;
|
|
363
|
+
}): Promise<SubscriptionRecord>;
|
|
364
|
+
/**
|
|
365
|
+
* Starts a hosted Checkout flow for a paid plan. Records the intended
|
|
366
|
+
* subscription locally as `incomplete` — it becomes `active` when the
|
|
367
|
+
* gateway confirms payment via webhook (`payment.succeeded`). Returns the
|
|
368
|
+
* URL to redirect the customer to.
|
|
369
|
+
*/
|
|
370
|
+
checkout(billableId: string, planName: string, options: {
|
|
371
|
+
period?: BillingPeriod;
|
|
372
|
+
successUrl: string;
|
|
373
|
+
cancelUrl: string;
|
|
374
|
+
}): Promise<{
|
|
375
|
+
url: string;
|
|
376
|
+
}>;
|
|
377
|
+
/**
|
|
378
|
+
* Opens a Customer Portal session for self-service billing (update card,
|
|
379
|
+
* change plan, cancel). Returns the URL to redirect the customer to.
|
|
380
|
+
*/
|
|
381
|
+
portal(billableId: string, options: {
|
|
382
|
+
returnUrl: string;
|
|
383
|
+
}): Promise<{
|
|
384
|
+
url: string;
|
|
385
|
+
}>;
|
|
386
|
+
get(billableId: string): Promise<SubscriptionRecord | null>;
|
|
387
|
+
/** Active = status active, or trialing with the trial still running. */
|
|
388
|
+
subscribed(billableId: string, plan?: string): Promise<boolean>;
|
|
389
|
+
onTrial(billableId: string): Promise<boolean>;
|
|
390
|
+
/**
|
|
391
|
+
* Changes the plan on an active subscription. When the subscription is
|
|
392
|
+
* gateway-backed, the change is pushed to the gateway with proration so the
|
|
393
|
+
* customer is credited/charged the mid-cycle difference (pass
|
|
394
|
+
* `{ prorate: false }` to switch at the next renewal with no immediate
|
|
395
|
+
* settlement).
|
|
396
|
+
*/
|
|
397
|
+
swap(billableId: string, planName: string, options?: {
|
|
398
|
+
prorate?: boolean;
|
|
399
|
+
}): Promise<SubscriptionRecord>;
|
|
400
|
+
cancel(billableId: string, options?: {
|
|
401
|
+
atPeriodEnd?: boolean;
|
|
402
|
+
}): Promise<SubscriptionRecord>;
|
|
403
|
+
resume(billableId: string): Promise<SubscriptionRecord>;
|
|
404
|
+
/** Feature checks and consumption, Soulbscription-style. */
|
|
405
|
+
features(billableId: string): {
|
|
406
|
+
can: (feature: string) => Promise<boolean>;
|
|
407
|
+
limit: (feature: string) => Promise<number>;
|
|
408
|
+
usage: (feature: string) => Promise<number>;
|
|
409
|
+
remaining: (feature: string) => Promise<number>;
|
|
410
|
+
consume: (feature: string, amount?: number) => Promise<number>;
|
|
411
|
+
};
|
|
412
|
+
/**
|
|
413
|
+
* Applies a gateway webhook: idempotent by event id, updates local state
|
|
414
|
+
* and emits domain hooks. Local state is the read model — feature checks
|
|
415
|
+
* never call the gateway.
|
|
416
|
+
*/
|
|
417
|
+
handleWebhook(event: WebhookEvent): Promise<boolean>;
|
|
418
|
+
/**
|
|
419
|
+
* Maintenance (run from the scheduler): settles expired local trials.
|
|
420
|
+
* Gateway-backed trials are settled by the gateway's webhook, not here.
|
|
421
|
+
*/
|
|
422
|
+
expireTrials(): Promise<SubscriptionRecord[]>;
|
|
423
|
+
private isActive;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
declare module '@basaltkit/core' {
|
|
427
|
+
interface BasaltHooks {
|
|
428
|
+
'billing:subscribed': {
|
|
429
|
+
subscription: SubscriptionRecord;
|
|
430
|
+
};
|
|
431
|
+
'billing:swapped': {
|
|
432
|
+
subscription: SubscriptionRecord;
|
|
433
|
+
from: string;
|
|
434
|
+
};
|
|
435
|
+
'billing:canceled': {
|
|
436
|
+
subscription: SubscriptionRecord;
|
|
437
|
+
};
|
|
438
|
+
'billing:trial_expired': {
|
|
439
|
+
subscription: SubscriptionRecord;
|
|
440
|
+
};
|
|
441
|
+
'billing:webhook': {
|
|
442
|
+
event: WebhookEvent;
|
|
443
|
+
};
|
|
444
|
+
'billing:checkout_started': {
|
|
445
|
+
billableId: string;
|
|
446
|
+
plan: string;
|
|
447
|
+
url: string;
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
declare const SUBSCRIPTIONS: _basaltkit_core.Token<Subscriptions>;
|
|
452
|
+
type SubscriptionsPluginOptions = Omit<SubscriptionsOptions, 'hooks'>;
|
|
453
|
+
declare function subscriptionsPlugin(options: SubscriptionsPluginOptions): _basaltkit_core.BasaltPlugin<unknown>;
|
|
454
|
+
interface BillingRoutesOptions {
|
|
455
|
+
/** Where Stripe returns the customer after Checkout. */
|
|
456
|
+
successUrl: string;
|
|
457
|
+
cancelUrl: string;
|
|
458
|
+
/** Where the Customer Portal returns the customer. Default: successUrl. */
|
|
459
|
+
portalReturnUrl?: string;
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Hosted billing routes for the current tenant: `POST /billing/checkout`
|
|
463
|
+
* (subscribe via the gateway's hosted page) and `POST /billing/portal`
|
|
464
|
+
* (self-service management). Both return `{ url }` to redirect to. The
|
|
465
|
+
* success/cancel/return URLs are configured here; the request body may
|
|
466
|
+
* override them per call.
|
|
467
|
+
*/
|
|
468
|
+
declare function billingRoutes(options: BillingRoutesOptions): BasaltRoute[];
|
|
469
|
+
/**
|
|
470
|
+
* Webhook endpoint: POST /billing/webhook — signature verified by the
|
|
471
|
+
* gateway driver, processing idempotent by event id. Returns 200 with
|
|
472
|
+
* { received, duplicate } so gateways stop retrying.
|
|
473
|
+
*/
|
|
474
|
+
declare function billingWebhookRoute(gateway: BillingGateway): BasaltRoute;
|
|
475
|
+
|
|
476
|
+
export { type BillingGateway, type BillingPeriod, type BillingRoutesOptions, type CheckoutInput, type CreateSubscriptionInput, FakeBillingGateway, FeatureUnavailableError, type FeatureValue, GatewayUnsupportedError, MemorySubscriptionStore, MemoryUsageStore, MemoryWebhookStore, type Meter, NotSubscribedError, type PlanDefinition, type Plans, type PortalInput, QuotaExceededError, type RedisLike, RedisUsageStore, type RedisUsageStoreOptions, type RedisWebhookClient, RedisWebhookStore, type RedisWebhookStoreOptions, SUBSCRIPTIONS, StripeBillingGateway, type StripeGatewayOptions, StripeRequestError, type SubscriptionRecord, type SubscriptionStatus, type SubscriptionStore, Subscriptions, type SubscriptionsOptions, type SubscriptionsPluginOptions, type SwapInput, UnknownPlanError, type UsageConsumeResult, type UsageStore, type WebhookEvent, WebhookInvalidError, type WebhookStore, billingRoutes, billingWebhookRoute, definePlans, featureLimit, isMeter, meter, planPrice, subscriptionsPlugin };
|