@delightstack/stripe 0.1.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 (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +30 -0
  3. package/dist/client/billing.client.svelte.d.ts +154 -0
  4. package/dist/client/billing.client.svelte.d.ts.map +1 -0
  5. package/dist/client/billing.client.svelte.js +279 -0
  6. package/dist/client/index.d.ts +2 -0
  7. package/dist/client/index.d.ts.map +1 -0
  8. package/dist/client/index.js +1 -0
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +1 -0
  12. package/dist/server/billing.config.d.ts +189 -0
  13. package/dist/server/billing.config.d.ts.map +1 -0
  14. package/dist/server/billing.config.js +37 -0
  15. package/dist/server/billing.handler.d.ts +46 -0
  16. package/dist/server/billing.handler.d.ts.map +1 -0
  17. package/dist/server/billing.handler.js +73 -0
  18. package/dist/server/billing.meter.d.ts +41 -0
  19. package/dist/server/billing.meter.d.ts.map +1 -0
  20. package/dist/server/billing.meter.js +55 -0
  21. package/dist/server/billing.products.d.ts +24 -0
  22. package/dist/server/billing.products.d.ts.map +1 -0
  23. package/dist/server/billing.products.js +124 -0
  24. package/dist/server/billing.routes.d.ts +8 -0
  25. package/dist/server/billing.routes.d.ts.map +1 -0
  26. package/dist/server/billing.routes.js +441 -0
  27. package/dist/server/billing.stripe.d.ts +34 -0
  28. package/dist/server/billing.stripe.d.ts.map +1 -0
  29. package/dist/server/billing.stripe.js +135 -0
  30. package/dist/server/billing.sync.d.ts +29 -0
  31. package/dist/server/billing.sync.d.ts.map +1 -0
  32. package/dist/server/billing.sync.js +122 -0
  33. package/dist/server/billing.webhook.d.ts +10 -0
  34. package/dist/server/billing.webhook.d.ts.map +1 -0
  35. package/dist/server/billing.webhook.js +222 -0
  36. package/dist/server/billing.webhook.register.d.ts +12 -0
  37. package/dist/server/billing.webhook.register.d.ts.map +1 -0
  38. package/dist/server/billing.webhook.register.js +57 -0
  39. package/dist/server/index.d.ts +9 -0
  40. package/dist/server/index.d.ts.map +1 -0
  41. package/dist/server/index.js +8 -0
  42. package/dist/sveltekit/guards.d.ts +29 -0
  43. package/dist/sveltekit/guards.d.ts.map +1 -0
  44. package/dist/sveltekit/guards.js +57 -0
  45. package/dist/sveltekit/index.d.ts +2 -0
  46. package/dist/sveltekit/index.d.ts.map +1 -0
  47. package/dist/sveltekit/index.js +1 -0
  48. package/dist/types/billing.type.d.ts +61 -0
  49. package/dist/types/billing.type.d.ts.map +1 -0
  50. package/dist/types/billing.type.js +1 -0
  51. package/dist/types/index.d.ts +3 -0
  52. package/dist/types/index.d.ts.map +1 -0
  53. package/dist/types/index.js +2 -0
  54. package/dist/types/webhook.type.d.ts +22 -0
  55. package/dist/types/webhook.type.d.ts.map +1 -0
  56. package/dist/types/webhook.type.js +2 -0
  57. package/package.json +91 -0
@@ -0,0 +1,122 @@
1
+ import { getStripe, stripeCall } from './billing.stripe';
2
+ /** Max pages fetched when listing a customer's subscriptions (100 per page) */
3
+ const MAX_SUBSCRIPTION_PAGES = 10;
4
+ /** Status priority used to pick the most relevant subscription */
5
+ const STATUS_PRIORITY = [
6
+ 'active',
7
+ 'trialing',
8
+ 'past_due',
9
+ 'canceled',
10
+ ];
11
+ /** Lists ALL subscriptions for a customer (paginated, not capped at one page) */
12
+ async function listAllSubscriptions(config, customer_id) {
13
+ const stripe = getStripe(config);
14
+ const all = [];
15
+ let starting_after;
16
+ for (let page = 0; page < MAX_SUBSCRIPTION_PAGES; page++) {
17
+ const result = await stripeCall(() => stripe.subscriptions.list({
18
+ customer: customer_id,
19
+ status: 'all',
20
+ limit: 100,
21
+ ...(starting_after ? { starting_after } : {}),
22
+ expand: ['data.items.data.price.product'],
23
+ }));
24
+ all.push(...result.data);
25
+ if (!result.has_more || result.data.length === 0)
26
+ break;
27
+ starting_after = result.data[result.data.length - 1].id;
28
+ }
29
+ return all;
30
+ }
31
+ /** Computes the SubscriptionState for the most relevant subscription (or null) */
32
+ function computeState(config, subscriptions) {
33
+ // Find the most relevant subscription (active > trialing > past_due > canceled)
34
+ const sorted = subscriptions
35
+ .filter((s) => STATUS_PRIORITY.includes(s.status))
36
+ .sort((a, b) => STATUS_PRIORITY.indexOf(a.status) - STATUS_PRIORITY.indexOf(b.status));
37
+ const subscription = sorted[0] ?? null;
38
+ if (!subscription)
39
+ return null;
40
+ // Extract plan IDs and entitlements from the subscription's products
41
+ const plan_ids = [];
42
+ const granted_entitlements = [];
43
+ for (const item of subscription.items.data) {
44
+ const product = item.price.product;
45
+ const plan_id = product.metadata?.plan_id;
46
+ if (plan_id) {
47
+ plan_ids.push(plan_id);
48
+ // Find the matching plan definition
49
+ const plan_def = config.plans?.find((p) => p.id === plan_id);
50
+ if (plan_def?.entitlements) {
51
+ granted_entitlements.push(...plan_def.entitlements);
52
+ }
53
+ }
54
+ }
55
+ return {
56
+ subscription_id: subscription.id,
57
+ status: subscription.status,
58
+ plan_ids,
59
+ entitlements: [...new Set(granted_entitlements)],
60
+ current_period_start: subscription.current_period_start * 1000,
61
+ current_period_end: subscription.current_period_end * 1000,
62
+ cancel_at: subscription.cancel_at ? subscription.cancel_at * 1000 : undefined,
63
+ canceled_at: subscription.canceled_at ? subscription.canceled_at * 1000 : undefined,
64
+ trial_start: subscription.trial_start ? subscription.trial_start * 1000 : undefined,
65
+ trial_end: subscription.trial_end ? subscription.trial_end * 1000 : undefined,
66
+ };
67
+ }
68
+ /** Whether a subscription state grants its plans/entitlements */
69
+ function isStateActive(state) {
70
+ return state?.status === 'active' || state?.status === 'trialing';
71
+ }
72
+ /**
73
+ * Lightweight read of the current subscription state from Stripe.
74
+ * Does NOT update auth entitlements or broadcast — use for GET endpoints.
75
+ */
76
+ export async function fetchSubscriptionState(config, customer_id) {
77
+ const subscriptions = await listAllSubscriptions(config, customer_id);
78
+ return computeState(config, subscriptions);
79
+ }
80
+ /** The plan ids that should be cached in org_state for `requirePlan()` */
81
+ export function activePlanIds(state) {
82
+ return isStateActive(state) ? state.plan_ids : [];
83
+ }
84
+ /**
85
+ * Fetches the latest subscription state from Stripe and syncs entitlements
86
+ * to the auth package. Returns the current subscription state.
87
+ */
88
+ export async function syncSubscription(ctx) {
89
+ const subscriptions = await listAllSubscriptions(ctx.config, ctx.customer_id);
90
+ const state = computeState(ctx.config, subscriptions);
91
+ // Update auth entitlements (cleared unless active/trialing)
92
+ await updateEntitlements(ctx, isStateActive(state) ? state.entitlements : []);
93
+ // Cache active plan ids in org_state so requirePlan() guards work
94
+ ctx.setOrgState?.({ billing_plan_ids: activePlanIds(state) });
95
+ broadcastChange(ctx, state);
96
+ return state;
97
+ }
98
+ /** Encode entitlement names to a bitwise integer and update the auth org */
99
+ async function updateEntitlements(ctx, entitlement_names) {
100
+ if (!ctx.auth || !ctx.org_id || !ctx.config.entitlements?.length)
101
+ return;
102
+ const entitlements_array = ctx.config.entitlements;
103
+ let encoded = 0;
104
+ for (const name of entitlement_names) {
105
+ const bit = entitlements_array.indexOf(name);
106
+ if (bit !== -1)
107
+ encoded |= 1 << bit;
108
+ }
109
+ await ctx.auth.updateOrg(ctx.org_id, { plan: encoded });
110
+ }
111
+ /** Broadcast subscription change via WebSocket */
112
+ function broadcastChange(ctx, state) {
113
+ if (!ctx.ws)
114
+ return;
115
+ ctx.ws.broadcast({
116
+ event: 'billing:subscription:changed',
117
+ subscription_id: state?.subscription_id ?? null,
118
+ status: state?.status ?? 'canceled',
119
+ plan_ids: state?.plan_ids ?? [],
120
+ entitlements: state?.entitlements ?? [],
121
+ });
122
+ }
@@ -0,0 +1,10 @@
1
+ import type { RequestEvent } from '@sveltejs/kit';
2
+ import type { ResolvedBillingConfig, AuthServerRpc, WebsocketRpc } from './billing.config';
3
+ export interface WebhookContext {
4
+ getAuthServer?: (event: RequestEvent) => AuthServerRpc | undefined;
5
+ getWebsocket?: (event: RequestEvent) => WebsocketRpc | undefined;
6
+ }
7
+ /** Resets the cached webhook secret (for tests) @internal */
8
+ export declare function resetWebhookSecretCache(): void;
9
+ export declare function handleWebhook(event: RequestEvent, config: ResolvedBillingConfig, ctx: WebhookContext): Promise<Response>;
10
+ //# sourceMappingURL=billing.webhook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"billing.webhook.d.ts","sourceRoot":"","sources":["../../src/server/billing.webhook.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD,OAAO,KAAK,EACX,qBAAqB,EACrB,aAAa,EACb,YAAY,EACZ,MAAM,kBAAkB,CAAC;AAM1B,MAAM,WAAW,cAAc;IAC9B,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,aAAa,GAAG,SAAS,CAAC;IACnE,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,YAAY,GAAG,SAAS,CAAC;CACjE;AASD,6DAA6D;AAC7D,wBAAgB,uBAAuB,IAAI,IAAI,CAE9C;AAuED,wBAAsB,aAAa,CAClC,KAAK,EAAE,YAAY,EACnB,MAAM,EAAE,qBAAqB,EAC7B,GAAG,EAAE,cAAc,GACjB,OAAO,CAAC,QAAQ,CAAC,CAwKnB"}
@@ -0,0 +1,222 @@
1
+ import { DelightError } from '@delightstack/utilities';
2
+ import { getStripe } from './billing.stripe';
3
+ import { syncSubscription } from './billing.sync';
4
+ import { ensureWebhookRegistered } from './billing.webhook.register';
5
+ /**
6
+ * In-flight/cached webhook secret registration (auto-registered).
7
+ * A single shared promise guards against two simultaneous cold-start webhooks
8
+ * both auto-registering a webhook endpoint.
9
+ */
10
+ let webhook_secret_promise = null;
11
+ /** Resets the cached webhook secret (for tests) @internal */
12
+ export function resetWebhookSecretCache() {
13
+ webhook_secret_promise = null;
14
+ }
15
+ /** Resolve the webhook signing secret, auto-registering if needed */
16
+ async function resolveWebhookSecret(event, config) {
17
+ if (config.webhook_secret)
18
+ return config.webhook_secret;
19
+ if (!webhook_secret_promise) {
20
+ const app_url = config.app_url ?? event.url.origin;
21
+ webhook_secret_promise = ensureWebhookRegistered(config, app_url).catch((error) => {
22
+ // Allow a retry on the next request instead of caching the failure
23
+ webhook_secret_promise = null;
24
+ throw error;
25
+ });
26
+ }
27
+ return webhook_secret_promise;
28
+ }
29
+ // ── Webhook idempotency ────────────────────────────────────────────
30
+ /** How long processed event IDs are remembered by the default store */
31
+ const EVENT_ID_TTL_MS = 24 * 60 * 60 * 1000;
32
+ /** Max processed event IDs kept by the default store */
33
+ const EVENT_ID_CAP = 5000;
34
+ /** Default in-memory idempotency store (per-isolate, TTL + cap) */
35
+ const processed_event_ids = new Map();
36
+ function defaultEventStoreHas(event_id) {
37
+ const now = Date.now();
38
+ // Prune expired entries (Map preserves insertion order — oldest first)
39
+ for (const [id, added_at] of processed_event_ids) {
40
+ if (now - added_at <= EVENT_ID_TTL_MS)
41
+ break;
42
+ processed_event_ids.delete(id);
43
+ }
44
+ return processed_event_ids.has(event_id);
45
+ }
46
+ function defaultEventStoreAdd(event_id) {
47
+ processed_event_ids.set(event_id, Date.now());
48
+ // Enforce the cap (evict oldest first)
49
+ while (processed_event_ids.size > EVENT_ID_CAP) {
50
+ const oldest = processed_event_ids.keys().next().value;
51
+ if (oldest === undefined)
52
+ break;
53
+ processed_event_ids.delete(oldest);
54
+ }
55
+ }
56
+ /** Resolve customer_id to org_id via Stripe customer metadata */
57
+ async function resolveOrgIdFromCustomer(stripe, customer_id) {
58
+ const customer = await stripe.customers.retrieve(customer_id);
59
+ if (customer.deleted)
60
+ return undefined;
61
+ return customer.metadata?.org_id ?? undefined;
62
+ }
63
+ /** Extract customer_id string from a Stripe object's customer field */
64
+ function extractCustomerId(customer) {
65
+ if (typeof customer === 'string')
66
+ return customer;
67
+ return customer?.id ?? '';
68
+ }
69
+ export async function handleWebhook(event, config, ctx) {
70
+ const stripe = getStripe(config);
71
+ const webhook_secret = await resolveWebhookSecret(event, config);
72
+ if (!webhook_secret) {
73
+ throw new DelightError({
74
+ message: 'Webhook secret not configured',
75
+ status: 500,
76
+ code: 'billing/webhook_secret_missing',
77
+ });
78
+ }
79
+ // Verify the webhook signature
80
+ const body = await event.request.text();
81
+ const sig = event.request.headers.get('stripe-signature');
82
+ if (!sig) {
83
+ throw DelightError.badRequest('Missing Stripe signature');
84
+ }
85
+ let stripe_event;
86
+ try {
87
+ stripe_event = stripe.webhooks.constructEvent(body, sig, webhook_secret);
88
+ }
89
+ catch {
90
+ throw DelightError.badRequest('Invalid webhook signature');
91
+ }
92
+ // Idempotency — skip events that were already fully processed so Stripe
93
+ // retries don't double-apply side effects
94
+ const event_store = config.webhook_event_store;
95
+ const already_processed = event_store
96
+ ? await event_store.has(stripe_event.id)
97
+ : defaultEventStoreHas(stripe_event.id);
98
+ if (already_processed) {
99
+ return new Response(JSON.stringify({ received: true, duplicate: true }), {
100
+ status: 200,
101
+ headers: { 'Content-Type': 'application/json' },
102
+ });
103
+ }
104
+ // Dispatch event
105
+ switch (stripe_event.type) {
106
+ case 'customer.subscription.created':
107
+ case 'customer.subscription.updated':
108
+ case 'customer.subscription.deleted': {
109
+ const subscription = stripe_event.data.object;
110
+ const customer_id = extractCustomerId(subscription.customer);
111
+ const org_id = await resolveOrgIdFromCustomer(stripe, customer_id);
112
+ const state = await syncSubscription({
113
+ config,
114
+ customer_id,
115
+ org_id,
116
+ auth: org_id ? ctx.getAuthServer?.(event) : undefined,
117
+ ws: org_id ? ctx.getWebsocket?.(event) : undefined,
118
+ });
119
+ if (config.hooks?.onSubscriptionChange) {
120
+ if (state) {
121
+ await config.hooks.onSubscriptionChange({
122
+ customer_id,
123
+ subscription_id: state.subscription_id,
124
+ status: state.status,
125
+ plan_id: state.plan_ids[0] ?? null,
126
+ entitlements: state.entitlements,
127
+ event,
128
+ });
129
+ }
130
+ else if (stripe_event.type === 'customer.subscription.deleted') {
131
+ // No remaining subscription — still fire the hook so apps can
132
+ // react to cancellations (the most important lifecycle event)
133
+ await config.hooks.onSubscriptionChange({
134
+ customer_id,
135
+ subscription_id: subscription.id,
136
+ status: 'canceled',
137
+ plan_id: null,
138
+ entitlements: [],
139
+ event,
140
+ });
141
+ }
142
+ }
143
+ break;
144
+ }
145
+ case 'invoice.paid': {
146
+ const invoice = stripe_event.data.object;
147
+ const customer_id = extractCustomerId(invoice.customer);
148
+ const org_id = await resolveOrgIdFromCustomer(stripe, customer_id);
149
+ const ws = org_id ? ctx.getWebsocket?.(event) : undefined;
150
+ // Amounts stay as integer cents end-to-end — no float division
151
+ if (ws) {
152
+ ws.broadcast({
153
+ event: 'billing:payment:succeeded',
154
+ invoice_id: invoice.id,
155
+ amount: invoice.amount_paid ?? 0,
156
+ currency: invoice.currency,
157
+ });
158
+ }
159
+ if (config.hooks?.onPaymentSuccess) {
160
+ await config.hooks.onPaymentSuccess({
161
+ customer_id,
162
+ amount: invoice.amount_paid ?? 0,
163
+ currency: invoice.currency,
164
+ invoice_id: invoice.id,
165
+ });
166
+ }
167
+ break;
168
+ }
169
+ case 'invoice.payment_failed': {
170
+ const invoice = stripe_event.data.object;
171
+ const customer_id = extractCustomerId(invoice.customer);
172
+ const org_id = await resolveOrgIdFromCustomer(stripe, customer_id);
173
+ const ws = org_id ? ctx.getWebsocket?.(event) : undefined;
174
+ // Amounts stay as integer cents end-to-end — no float division
175
+ if (ws) {
176
+ ws.broadcast({
177
+ event: 'billing:payment:failed',
178
+ invoice_id: invoice.id,
179
+ amount: invoice.amount_due ?? 0,
180
+ currency: invoice.currency,
181
+ });
182
+ }
183
+ if (config.hooks?.onPaymentFailed) {
184
+ await config.hooks.onPaymentFailed({
185
+ customer_id,
186
+ amount: invoice.amount_due ?? 0,
187
+ currency: invoice.currency,
188
+ invoice_id: invoice.id,
189
+ });
190
+ }
191
+ break;
192
+ }
193
+ case 'checkout.session.completed': {
194
+ const session = stripe_event.data.object;
195
+ if (session.mode === 'subscription' && session.customer) {
196
+ const customer_id = extractCustomerId(session.customer);
197
+ const org_id = await resolveOrgIdFromCustomer(stripe, customer_id);
198
+ await syncSubscription({
199
+ config,
200
+ customer_id,
201
+ org_id,
202
+ auth: org_id ? ctx.getAuthServer?.(event) : undefined,
203
+ ws: org_id ? ctx.getWebsocket?.(event) : undefined,
204
+ });
205
+ }
206
+ break;
207
+ }
208
+ default:
209
+ // Unhandled event type — no-op
210
+ break;
211
+ }
212
+ // Mark the event as processed AFTER all side effects succeeded so a
213
+ // failed handler is retried by Stripe (at-least-once with dedupe)
214
+ if (event_store)
215
+ await event_store.add(stripe_event.id);
216
+ else
217
+ defaultEventStoreAdd(stripe_event.id);
218
+ return new Response(JSON.stringify({ received: true }), {
219
+ status: 200,
220
+ headers: { 'Content-Type': 'application/json' },
221
+ });
222
+ }
@@ -0,0 +1,12 @@
1
+ import type { ResolvedBillingConfig } from './billing.config';
2
+ /**
3
+ * Ensures a webhook endpoint is registered in Stripe for this app.
4
+ * Called automatically on first webhook request if webhook_secret is not provided.
5
+ *
6
+ * In dev mode: deletes any existing webhook with the same URL and creates a fresh one.
7
+ * In production: warns that webhook_secret should be configured.
8
+ *
9
+ * Returns the webhook signing secret.
10
+ */
11
+ export declare function ensureWebhookRegistered(config: ResolvedBillingConfig, app_url: string): Promise<string>;
12
+ //# sourceMappingURL=billing.webhook.register.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"billing.webhook.register.d.ts","sourceRoot":"","sources":["../../src/server/billing.webhook.register.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAmB9D;;;;;;;;GAQG;AACH,wBAAsB,uBAAuB,CAC5C,MAAM,EAAE,qBAAqB,EAC7B,OAAO,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC,CAwCjB"}
@@ -0,0 +1,57 @@
1
+ import { DelightError } from '@delightstack/utilities';
2
+ import { getStripe, stripeCall } from './billing.stripe';
3
+ /** Webhook events the billing system needs to listen to */
4
+ const REQUIRED_EVENTS = [
5
+ 'customer.subscription.created',
6
+ 'customer.subscription.updated',
7
+ 'customer.subscription.deleted',
8
+ 'invoice.paid',
9
+ 'invoice.payment_failed',
10
+ 'invoice.finalized',
11
+ 'checkout.session.completed',
12
+ 'customer.created',
13
+ 'customer.updated',
14
+ 'payment_method.attached',
15
+ 'payment_method.detached',
16
+ ];
17
+ /**
18
+ * Ensures a webhook endpoint is registered in Stripe for this app.
19
+ * Called automatically on first webhook request if webhook_secret is not provided.
20
+ *
21
+ * In dev mode: deletes any existing webhook with the same URL and creates a fresh one.
22
+ * In production: warns that webhook_secret should be configured.
23
+ *
24
+ * Returns the webhook signing secret.
25
+ */
26
+ export async function ensureWebhookRegistered(config, app_url) {
27
+ const stripe = getStripe(config);
28
+ const webhook_url = `${app_url}${config.base_path}/webhook`;
29
+ // Check for existing webhook with this URL
30
+ const existing = await stripeCall(() => stripe.webhookEndpoints.list({ limit: 100 }));
31
+ const match = existing.data.find((wh) => wh.url === webhook_url && wh.status === 'enabled');
32
+ if (match) {
33
+ if (config.dev) {
34
+ // In dev mode, delete and recreate to get a fresh secret
35
+ await stripeCall(() => stripe.webhookEndpoints.del(match.id));
36
+ }
37
+ else {
38
+ // In production, the secret of an existing webhook can't be read back
39
+ // from Stripe — fail loudly with a clear setup error instead of an
40
+ // opaque "Webhook secret not configured" 500 later on
41
+ throw new DelightError({
42
+ message: 'A Stripe webhook endpoint already exists for this app but webhook_secret is not configured. ' +
43
+ 'Set webhook_secret in your billing config (from the Stripe dashboard) for production use.',
44
+ status: 500,
45
+ code: 'billing/webhook_secret_missing',
46
+ });
47
+ }
48
+ }
49
+ // Create new webhook endpoint
50
+ const webhook = await stripeCall(() => stripe.webhookEndpoints.create({
51
+ url: webhook_url,
52
+ enabled_events: REQUIRED_EVENTS,
53
+ description: '@delightstack/stripe auto-registered webhook',
54
+ metadata: { delightstack: 'true' },
55
+ }));
56
+ return webhook.secret;
57
+ }
@@ -0,0 +1,9 @@
1
+ export { defineBillingConfig, type BillingConfig, type ResolvedBillingConfig, type PlanDefinition, type MeterDefinition, type AuthServerRpc, type WebsocketRpc, type WebhookEventStore, } from './billing.config';
2
+ export { createBillingHandle, type BillingHandleOptions } from './billing.handler';
3
+ export { syncSubscription, fetchSubscriptionState, type SyncContext, } from './billing.sync';
4
+ export { syncProducts, syncMeters, syncAll } from './billing.products';
5
+ export { reportMeterEvent, createMeterReporter } from './billing.meter';
6
+ export { getStripe, stripeCall } from './billing.stripe';
7
+ export { handleWebhook } from './billing.webhook';
8
+ export { ensureWebhookRegistered } from './billing.webhook.register';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,mBAAmB,EACnB,KAAK,aAAa,EAClB,KAAK,qBAAqB,EAC1B,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,iBAAiB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,mBAAmB,EAAE,KAAK,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACnF,OAAO,EACN,gBAAgB,EAChB,sBAAsB,EACtB,KAAK,WAAW,GAChB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AACvE,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACxE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC"}
@@ -0,0 +1,8 @@
1
+ export { defineBillingConfig, } from './billing.config';
2
+ export { createBillingHandle } from './billing.handler';
3
+ export { syncSubscription, fetchSubscriptionState, } from './billing.sync';
4
+ export { syncProducts, syncMeters, syncAll } from './billing.products';
5
+ export { reportMeterEvent, createMeterReporter } from './billing.meter';
6
+ export { getStripe, stripeCall } from './billing.stripe';
7
+ export { handleWebhook } from './billing.webhook';
8
+ export { ensureWebhookRegistered } from './billing.webhook.register';
@@ -0,0 +1,29 @@
1
+ import type { ServerLoadEvent } from '@sveltejs/kit';
2
+ interface BillingGuardOptions {
3
+ /** URL to redirect unauthenticated users to @default '/signin' */
4
+ redirect_to?: string;
5
+ /** URL to redirect users without a subscription to @default '/pricing' */
6
+ subscription_redirect?: string;
7
+ }
8
+ /**
9
+ * Creates billing-specific guard functions for SvelteKit load functions.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * const { requireSubscription, requirePlan } = createBillingGuards({
14
+ * plans: ['starter-monthly', 'pro-monthly'] as const,
15
+ * });
16
+ *
17
+ * // In +page.server.ts:
18
+ * export const load = requireSubscription(({ locals }) => ({ ... }));
19
+ * export const load = requirePlan('pro-monthly', ({ locals }) => ({ ... }));
20
+ * ```
21
+ */
22
+ export declare function createBillingGuards<const P extends string>(_options: {
23
+ plans: readonly P[];
24
+ }): {
25
+ requireSubscription: <T>(loadFn: (event: ServerLoadEvent) => T | Promise<T>, guardOptions?: BillingGuardOptions) => (event: ServerLoadEvent) => Promise<T>;
26
+ requirePlan: <T>(plan_id: P, loadFn: (event: ServerLoadEvent) => T | Promise<T>, guardOptions?: BillingGuardOptions) => (event: ServerLoadEvent) => Promise<T>;
27
+ };
28
+ export {};
29
+ //# sourceMappingURL=guards.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../../src/sveltekit/guards.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAErD,UAAU,mBAAmB;IAC5B,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,CAAC,CAAC,SAAS,MAAM,EAAE,QAAQ,EAAE;IACrE,KAAK,EAAE,SAAS,CAAC,EAAE,CAAC;CACpB;0BAO6B,CAAC,UACrB,CAAC,KAAK,EAAE,eAAe,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,iBACnC,mBAAmB,KAChC,CAAC,KAAK,EAAE,eAAe,KAAK,OAAO,CAAC,CAAC,CAAC;kBAsBpB,CAAC,sBAEb,CAAC,KAAK,EAAE,eAAe,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,iBACnC,mBAAmB,KAChC,CAAC,KAAK,EAAE,eAAe,KAAK,OAAO,CAAC,CAAC,CAAC;EAoBzC"}
@@ -0,0 +1,57 @@
1
+ import { redirect } from '@sveltejs/kit';
2
+ /**
3
+ * Creates billing-specific guard functions for SvelteKit load functions.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * const { requireSubscription, requirePlan } = createBillingGuards({
8
+ * plans: ['starter-monthly', 'pro-monthly'] as const,
9
+ * });
10
+ *
11
+ * // In +page.server.ts:
12
+ * export const load = requireSubscription(({ locals }) => ({ ... }));
13
+ * export const load = requirePlan('pro-monthly', ({ locals }) => ({ ... }));
14
+ * ```
15
+ */
16
+ export function createBillingGuards(_options) {
17
+ /**
18
+ * Requires any active subscription (entitlements > 0 on the org).
19
+ * Redirects to /pricing if no subscription.
20
+ */
21
+ function requireSubscription(loadFn, guardOptions) {
22
+ return async (event) => {
23
+ const locals = event.locals;
24
+ if (!locals.session) {
25
+ const target = guardOptions?.redirect_to ?? '/signin';
26
+ const return_to = encodeURIComponent(event.url.pathname + event.url.search);
27
+ throw redirect(302, `${target}?redirect=${return_to}`);
28
+ }
29
+ const org = locals.org;
30
+ if (!org || org.entitlements == null || org.entitlements === 0) {
31
+ throw redirect(302, guardOptions?.subscription_redirect ?? '/pricing');
32
+ }
33
+ return loadFn(event);
34
+ };
35
+ }
36
+ /**
37
+ * Requires a specific plan. Checks the org_state cookie for cached billing_plan_ids.
38
+ * Redirects to /pricing if the plan is not active.
39
+ */
40
+ function requirePlan(plan_id, loadFn, guardOptions) {
41
+ return async (event) => {
42
+ const locals = event.locals;
43
+ if (!locals.session) {
44
+ const target = guardOptions?.redirect_to ?? '/signin';
45
+ const return_to = encodeURIComponent(event.url.pathname + event.url.search);
46
+ throw redirect(302, `${target}?redirect=${return_to}`);
47
+ }
48
+ const org_state = locals.org_state;
49
+ const plan_ids = org_state?.billing_plan_ids;
50
+ if (!plan_ids?.includes(plan_id)) {
51
+ throw redirect(302, guardOptions?.subscription_redirect ?? '/pricing');
52
+ }
53
+ return loadFn(event);
54
+ };
55
+ }
56
+ return { requireSubscription, requirePlan };
57
+ }
@@ -0,0 +1,2 @@
1
+ export { createBillingGuards } from './guards';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/sveltekit/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC"}
@@ -0,0 +1 @@
1
+ export { createBillingGuards } from './guards';
@@ -0,0 +1,61 @@
1
+ import type Stripe from 'stripe';
2
+ /** Current subscription state — returned from sync and stored reactively on the client */
3
+ export interface SubscriptionState {
4
+ subscription_id: string;
5
+ status: Stripe.Subscription.Status;
6
+ plan_ids: string[];
7
+ entitlements: string[];
8
+ current_period_start: number;
9
+ current_period_end: number;
10
+ cancel_at?: number;
11
+ canceled_at?: number;
12
+ trial_start?: number;
13
+ trial_end?: number;
14
+ }
15
+ /** Client-safe plan information */
16
+ export interface PlanInfo {
17
+ id: string;
18
+ name: string;
19
+ description?: string;
20
+ amount: number;
21
+ currency: string;
22
+ interval: 'month' | 'year' | 'week' | 'day';
23
+ interval_count: number;
24
+ entitlements: string[];
25
+ trial_days?: number;
26
+ }
27
+ /** Formatted payment method info for client display */
28
+ export interface PaymentMethodInfo {
29
+ id: string;
30
+ type: string;
31
+ brand?: string;
32
+ last4?: string;
33
+ exp_month?: number;
34
+ exp_year?: number;
35
+ display_name: string;
36
+ is_default: boolean;
37
+ }
38
+ /**
39
+ * Formatted invoice info for client display.
40
+ * All monetary amounts are integers in the smallest currency unit
41
+ * (e.g. cents — 499 = $4.99), matching `PlanDefinition.amount`.
42
+ */
43
+ export interface InvoiceInfo {
44
+ id: string;
45
+ number: string | null;
46
+ status: string | null;
47
+ /** Integer amount in the smallest currency unit (e.g. cents) */
48
+ amount_paid: number;
49
+ /** Integer amount in the smallest currency unit (e.g. cents) */
50
+ amount_due: number;
51
+ /** Integer amount in the smallest currency unit (e.g. cents) */
52
+ total: number;
53
+ currency: string;
54
+ created: number;
55
+ due_date: number | null;
56
+ period_start: number | null;
57
+ period_end: number | null;
58
+ pdf: string | null;
59
+ hosted_invoice_url: string | null;
60
+ }
61
+ //# sourceMappingURL=billing.type.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"billing.type.d.ts","sourceRoot":"","sources":["../../src/types/billing.type.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAEjC,0FAA0F;AAC1F,MAAM,WAAW,iBAAiB;IACjC,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC;IACnC,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,mCAAmC;AACnC,MAAM,WAAW,QAAQ;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,CAAC;IAC5C,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,uDAAuD;AACvD,MAAM,WAAW,iBAAiB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,OAAO,CAAC;CACpB;AAED;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,gEAAgE;IAChE,WAAW,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,UAAU,EAAE,MAAM,CAAC;IACnB,gEAAgE;IAChE,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ export * from './billing.type';
2
+ export * from './webhook.type';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export * from './billing.type';
2
+ export * from './webhook.type';