@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,189 @@
1
+ import type { RequestEvent } from '@sveltejs/kit';
2
+ /**
3
+ * A plan definition that maps to a Stripe Product + Price.
4
+ * Defined in code, synced to Stripe.
5
+ */
6
+ export interface PlanDefinition {
7
+ /** Unique plan identifier (used in code, stored as Stripe product metadata) */
8
+ id: string;
9
+ /** Human-readable name (synced to Stripe product name) */
10
+ name: string;
11
+ /** Plan description (synced to Stripe product description) */
12
+ description?: string;
13
+ /** Stripe lookup_key for the price. Allows price migration without code changes. */
14
+ lookup_key: string;
15
+ /** Price amount in smallest currency unit (e.g. 999 = $9.99) */
16
+ amount: number;
17
+ /** Currency code (lowercase) @default 'usd' */
18
+ currency?: string;
19
+ /** Billing interval */
20
+ interval: 'month' | 'year' | 'week' | 'day';
21
+ /** Number of intervals between billings @default 1 */
22
+ interval_count?: number;
23
+ /**
24
+ * Entitlement names this plan grants.
25
+ * Maps to the auth package's entitlements array.
26
+ * @example ['premium', 'video-uploads']
27
+ */
28
+ entitlements?: string[];
29
+ /** Trial period in days @default undefined (no trial) */
30
+ trial_days?: number;
31
+ /** Whether this plan is archived (hidden from new subscriptions) */
32
+ archived?: boolean;
33
+ /** Additional Stripe product metadata */
34
+ metadata?: Record<string, string>;
35
+ }
36
+ /**
37
+ * A usage meter definition for billing based on consumption.
38
+ * Maps to Stripe Billing Meters.
39
+ */
40
+ export interface MeterDefinition {
41
+ /** Unique meter identifier */
42
+ id: string;
43
+ /** Human-readable display name */
44
+ display_name: string;
45
+ /** Event name used in meter event API */
46
+ event_name: string;
47
+ /** Aggregation formula */
48
+ aggregation: 'sum' | 'count' | 'max' | 'last';
49
+ /** Value key in the event payload @default 'value' */
50
+ value_key?: string;
51
+ }
52
+ /** Minimal RPC interface for the auth server (avoids hard dependency) */
53
+ export interface AuthServerRpc {
54
+ updateOrg(id: string, data: {
55
+ plan?: number;
56
+ json?: string;
57
+ }): unknown;
58
+ /**
59
+ * Optional read of the org record. When provided, the billing package
60
+ * read-modify-writes the org's `json` metadata instead of overwriting it.
61
+ */
62
+ getOrg?(id: string): {
63
+ json?: string | null;
64
+ } | null | undefined | Promise<{
65
+ json?: string | null;
66
+ } | null | undefined>;
67
+ }
68
+ /**
69
+ * Store used to deduplicate Stripe webhook events by event ID.
70
+ * Provide a durable implementation (e.g. Durable Object storage) for
71
+ * multi-isolate deployments. Defaults to an in-memory store with a TTL/cap.
72
+ */
73
+ export interface WebhookEventStore {
74
+ /** Returns true if the given Stripe event ID was already processed */
75
+ has(event_id: string): boolean | Promise<boolean>;
76
+ /** Marks the given Stripe event ID as processed */
77
+ add(event_id: string): void | Promise<void>;
78
+ }
79
+ /** Minimal RPC interface for the websocket server (avoids hard dependency) */
80
+ export interface WebsocketRpc {
81
+ broadcast(message: Record<string, unknown>): void;
82
+ }
83
+ /**
84
+ * Configuration for the billing/stripe integration.
85
+ * Pass to `defineBillingConfig()` to fill in defaults.
86
+ */
87
+ export interface BillingConfig<E extends string = string> {
88
+ /** Stripe secret key (sk_live_... or sk_test_...) */
89
+ secret_key: string;
90
+ /** Stripe publishable key (pk_live_... or pk_test_...) */
91
+ publishable_key: string;
92
+ /**
93
+ * Stripe webhook signing secret. If omitted, webhooks are
94
+ * auto-registered and the secret is derived from the registration.
95
+ * Provide this to use a manually configured webhook.
96
+ */
97
+ webhook_secret?: string;
98
+ /** Whether the app is in dev mode @default false */
99
+ dev?: boolean;
100
+ /** Base path for billing API routes @default '/api/billing' */
101
+ base_path?: string;
102
+ /** Plan definitions — products and prices defined in code */
103
+ plans?: PlanDefinition[];
104
+ /** Usage meter definitions */
105
+ meters?: MeterDefinition[];
106
+ /**
107
+ * Entitlement names that correspond to the auth package's entitlements array.
108
+ * Array index = bit position. Must match the auth config's entitlements array.
109
+ * Used to map plan entitlements to the auth system's bitwise encoding.
110
+ * @example ['premium', 'video-uploads', 'extra-usage']
111
+ */
112
+ entitlements?: readonly E[];
113
+ /**
114
+ * Whether billing is scoped to orgs or users.
115
+ * - 'org': Stripe customer = org. Subscription managed by org owner.
116
+ * - 'user': Stripe customer = user. Each user manages their own subscription.
117
+ * @default 'org'
118
+ */
119
+ billing_scope?: 'org' | 'user';
120
+ /**
121
+ * The public URL of the app (used for webhook registration and Billing Portal return URL).
122
+ * If omitted, derived from the first request's origin.
123
+ */
124
+ app_url?: string;
125
+ /**
126
+ * Additional origins that a client-provided `return_url` may point to
127
+ * (for Billing Portal and Checkout). The app's own origin is always allowed.
128
+ * Any other origin is rejected to prevent open-redirect/phishing.
129
+ * @example ['https://app.example.com']
130
+ */
131
+ allowed_return_origins?: string[];
132
+ /**
133
+ * Store used to deduplicate Stripe webhook events (idempotency).
134
+ * Defaults to an in-memory store (per-isolate, 24h TTL, capped size).
135
+ * Provide a durable implementation for multi-isolate deployments.
136
+ */
137
+ webhook_event_store?: WebhookEventStore;
138
+ /**
139
+ * Billing Portal configuration.
140
+ * @default { enabled: true }
141
+ */
142
+ portal?: {
143
+ /** Enable the Stripe Billing Portal @default true */
144
+ enabled?: boolean;
145
+ };
146
+ /** Lifecycle hooks for billing events */
147
+ hooks?: {
148
+ /** Called after a subscription is created, updated, canceled, or deleted */
149
+ onSubscriptionChange?: (ctx: {
150
+ customer_id: string;
151
+ subscription_id: string;
152
+ status: string;
153
+ plan_id: string | null;
154
+ entitlements: string[];
155
+ event: RequestEvent;
156
+ }) => void | Promise<void>;
157
+ /** Called after a payment succeeds. `amount` is an integer in the smallest currency unit (cents). */
158
+ onPaymentSuccess?: (ctx: {
159
+ customer_id: string;
160
+ /** Integer amount in the smallest currency unit (e.g. cents) */
161
+ amount: number;
162
+ currency: string;
163
+ invoice_id: string;
164
+ }) => void | Promise<void>;
165
+ /** Called after a payment fails. `amount` is an integer in the smallest currency unit (cents). */
166
+ onPaymentFailed?: (ctx: {
167
+ customer_id: string;
168
+ /** Integer amount in the smallest currency unit (e.g. cents) */
169
+ amount: number;
170
+ currency: string;
171
+ invoice_id: string;
172
+ }) => void | Promise<void>;
173
+ /** Called when a customer is created in Stripe */
174
+ onCustomerCreated?: (ctx: {
175
+ customer_id: string;
176
+ org_id?: string;
177
+ user_id?: string;
178
+ }) => void | Promise<void>;
179
+ };
180
+ }
181
+ /** Resolved billing config with all defaults filled in */
182
+ export interface ResolvedBillingConfig<E extends string = string> extends BillingConfig<E> {
183
+ base_path: string;
184
+ billing_scope: 'org' | 'user';
185
+ portal: Required<NonNullable<BillingConfig['portal']>>;
186
+ }
187
+ /** Creates a billing config with sensible defaults */
188
+ export declare function defineBillingConfig<const E extends string>(config: BillingConfig<E>): ResolvedBillingConfig<E>;
189
+ //# sourceMappingURL=billing.config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"billing.config.d.ts","sourceRoot":"","sources":["../../src/server/billing.config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ /** Creates a billing config with sensible defaults */
2
+ export function defineBillingConfig(config) {
3
+ if (!config.secret_key?.startsWith('sk_')) {
4
+ throw new Error('Billing config: secret_key must be a valid Stripe secret key (sk_...)');
5
+ }
6
+ if (!config.publishable_key?.startsWith('pk_')) {
7
+ throw new Error('Billing config: publishable_key must be a valid Stripe publishable key (pk_...)');
8
+ }
9
+ // Validate plan definitions
10
+ if (config.plans) {
11
+ const ids = new Set();
12
+ const keys = new Set();
13
+ for (const plan of config.plans) {
14
+ if (ids.has(plan.id)) {
15
+ throw new Error(`Billing config: duplicate plan id '${plan.id}'`);
16
+ }
17
+ if (keys.has(plan.lookup_key)) {
18
+ throw new Error(`Billing config: duplicate lookup_key '${plan.lookup_key}'`);
19
+ }
20
+ ids.add(plan.id);
21
+ keys.add(plan.lookup_key);
22
+ }
23
+ }
24
+ // Validate entitlements limit
25
+ if (config.entitlements && config.entitlements.length > 32) {
26
+ throw new Error(`Billing config: entitlements array exceeds 32 entries (got ${config.entitlements.length}). ` +
27
+ 'Bitwise encoding uses a 32-bit integer.');
28
+ }
29
+ return {
30
+ ...config,
31
+ base_path: config.base_path ?? '/api/billing',
32
+ billing_scope: config.billing_scope ?? 'org',
33
+ portal: {
34
+ enabled: config.portal?.enabled ?? true,
35
+ },
36
+ };
37
+ }
@@ -0,0 +1,46 @@
1
+ import type { Handle, RequestEvent } from '@sveltejs/kit';
2
+ import type { BillingConfig, AuthServerRpc, WebsocketRpc } from './billing.config';
3
+ /** Options for `createBillingHandle()` */
4
+ export interface BillingHandleOptions<Config extends BillingConfig = BillingConfig> {
5
+ /** The billing configuration (pass result of `defineBillingConfig()` or raw config) */
6
+ config: Config;
7
+ /**
8
+ * Get the auth server for entitlement updates.
9
+ * Return undefined if not using auth integration.
10
+ */
11
+ getAuthServer?: (event: RequestEvent) => AuthServerRpc | undefined;
12
+ /**
13
+ * Get the WebSocket server for real-time billing events.
14
+ * Return undefined if not using WebSocket integration.
15
+ */
16
+ getWebsocket?: (event: RequestEvent) => WebsocketRpc | undefined;
17
+ /** Whether the app is building (static build step) @default false */
18
+ building?: boolean;
19
+ /**
20
+ * Sync product/price/meter definitions to Stripe on first request.
21
+ * @default false
22
+ */
23
+ sync_on_startup?: boolean;
24
+ }
25
+ /**
26
+ * Creates a SvelteKit Handle for billing.
27
+ * Composable with SvelteKit's `sequence()`.
28
+ *
29
+ * Routes handled:
30
+ * - POST /api/billing/webhook Stripe webhook endpoint
31
+ * - GET /api/billing/subscription Get current subscription
32
+ * - POST /api/billing/subscription Create/update subscription
33
+ * - DELETE /api/billing/subscription Cancel subscription
34
+ * - GET /api/billing/invoice List invoices
35
+ * - GET /api/billing/payment-method List payment methods
36
+ * - POST /api/billing/payment-method Add payment method (create setup session)
37
+ * - PATCH /api/billing/payment-method/:id Set default payment method
38
+ * - DELETE /api/billing/payment-method/:id Remove payment method
39
+ * - POST /api/billing/portal Create Billing Portal session
40
+ * - POST /api/billing/checkout Create Checkout session
41
+ * - GET /api/billing/plan List available plans
42
+ * - POST /api/billing/sync Force subscription sync
43
+ * - GET /api/billing/config Get client-safe config
44
+ */
45
+ export declare function createBillingHandle<Config extends BillingConfig>(options: BillingHandleOptions<Config>): Handle;
46
+ //# sourceMappingURL=billing.handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"billing.handler.d.ts","sourceRoot":"","sources":["../../src/server/billing.handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,KAAK,EACX,aAAa,EAEb,aAAa,EACb,YAAY,EACZ,MAAM,kBAAkB,CAAC;AAO1B,0CAA0C;AAC1C,MAAM,WAAW,oBAAoB,CAAC,MAAM,SAAS,aAAa,GAAG,aAAa;IACjF,uFAAuF;IACvF,MAAM,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,aAAa,GAAG,SAAS,CAAC;IAEnE;;;OAGG;IACH,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,YAAY,GAAG,SAAS,CAAC;IAEjE,qEAAqE;IACrE,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;OAGG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,SAAS,aAAa,EAC/D,OAAO,EAAE,oBAAoB,CAAC,MAAM,CAAC,GACnC,MAAM,CAqDR"}
@@ -0,0 +1,73 @@
1
+ import { DelightError } from '@delightstack/utilities';
2
+ import { defineBillingConfig } from './billing.config';
3
+ import { handleBillingRoute } from './billing.routes';
4
+ import { handleWebhook } from './billing.webhook';
5
+ import { syncAll } from './billing.products';
6
+ /**
7
+ * Creates a SvelteKit Handle for billing.
8
+ * Composable with SvelteKit's `sequence()`.
9
+ *
10
+ * Routes handled:
11
+ * - POST /api/billing/webhook Stripe webhook endpoint
12
+ * - GET /api/billing/subscription Get current subscription
13
+ * - POST /api/billing/subscription Create/update subscription
14
+ * - DELETE /api/billing/subscription Cancel subscription
15
+ * - GET /api/billing/invoice List invoices
16
+ * - GET /api/billing/payment-method List payment methods
17
+ * - POST /api/billing/payment-method Add payment method (create setup session)
18
+ * - PATCH /api/billing/payment-method/:id Set default payment method
19
+ * - DELETE /api/billing/payment-method/:id Remove payment method
20
+ * - POST /api/billing/portal Create Billing Portal session
21
+ * - POST /api/billing/checkout Create Checkout session
22
+ * - GET /api/billing/plan List available plans
23
+ * - POST /api/billing/sync Force subscription sync
24
+ * - GET /api/billing/config Get client-safe config
25
+ */
26
+ export function createBillingHandle(options) {
27
+ const config = defineBillingConfig(options.config);
28
+ let product_sync_started = false;
29
+ return async ({ event, resolve }) => {
30
+ if (options.building)
31
+ return resolve(event);
32
+ // Trigger product sync once on first request (non-blocking)
33
+ if (options.sync_on_startup && !product_sync_started) {
34
+ product_sync_started = true;
35
+ syncAll(config).catch((err) => {
36
+ console.error('[@delightstack/stripe] Product sync failed:', err);
37
+ });
38
+ }
39
+ const pathname = event.url.pathname;
40
+ const base_path = config.base_path;
41
+ if (!pathname.startsWith(base_path)) {
42
+ return resolve(event);
43
+ }
44
+ const route_path = pathname.slice(base_path.length) || '/';
45
+ const method = event.request.method;
46
+ // Webhook route does NOT require auth (Stripe sends it)
47
+ if (route_path === '/webhook' && method === 'POST') {
48
+ try {
49
+ return await handleWebhook(event, config, {
50
+ getAuthServer: options.getAuthServer,
51
+ getWebsocket: options.getWebsocket,
52
+ });
53
+ }
54
+ catch (error) {
55
+ return DelightError.from(error).toResponse();
56
+ }
57
+ }
58
+ // All other routes require authentication
59
+ const locals = event.locals;
60
+ if (!locals.session) {
61
+ return DelightError.unauthorized('Authentication required').toResponse();
62
+ }
63
+ try {
64
+ return await handleBillingRoute(event, config, route_path, method, {
65
+ getAuthServer: options.getAuthServer,
66
+ getWebsocket: options.getWebsocket,
67
+ });
68
+ }
69
+ catch (error) {
70
+ return DelightError.from(error).toResponse();
71
+ }
72
+ };
73
+ }
@@ -0,0 +1,41 @@
1
+ import type { ResolvedBillingConfig } from './billing.config';
2
+ /**
3
+ * Reports a usage event to a Stripe Billing Meter.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * await reportMeterEvent(config, {
8
+ * meter_id: 'ai-tokens',
9
+ * customer_id: 'cus_xxx',
10
+ * value: result.usage.total_tokens,
11
+ * });
12
+ * ```
13
+ */
14
+ export declare function reportMeterEvent(config: ResolvedBillingConfig, options: {
15
+ /** The meter ID (matches MeterDefinition.id from config) */
16
+ meter_id: string;
17
+ /** The Stripe customer ID to attribute the usage to */
18
+ customer_id: string;
19
+ /** The usage value to report */
20
+ value: number;
21
+ /** Unix timestamp in seconds @default now */
22
+ timestamp?: number;
23
+ /** Idempotency key to prevent duplicate events */
24
+ idempotency_key?: string;
25
+ }): Promise<void>;
26
+ /**
27
+ * Creates a helper function bound to a specific meter and config.
28
+ * Designed for integration with the AI package.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * // In your Durable Object:
33
+ * const reportTokenUsage = createMeterReporter(billingConfig, 'ai-tokens');
34
+ *
35
+ * // After each AI completion:
36
+ * const result = await ai.complete(options);
37
+ * await reportTokenUsage(customer_id, result.usage.total_tokens);
38
+ * ```
39
+ */
40
+ export declare function createMeterReporter(config: ResolvedBillingConfig, meter_id: string): (customer_id: string, value: number, idempotency_key?: string) => Promise<void>;
41
+ //# sourceMappingURL=billing.meter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"billing.meter.d.ts","sourceRoot":"","sources":["../../src/server/billing.meter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAI9D;;;;;;;;;;;GAWG;AACH,wBAAsB,gBAAgB,CACrC,MAAM,EAAE,qBAAqB,EAC7B,OAAO,EAAE;IACR,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,WAAW,EAAE,MAAM,CAAC;IACpB,gCAAgC;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,6CAA6C;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,eAAe,CAAC,EAAE,MAAM,CAAC;CACzB,GACC,OAAO,CAAC,IAAI,CAAC,CAqBf;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,qBAAqB,EAAE,QAAQ,EAAE,MAAM,IAEjF,aAAa,MAAM,EACnB,OAAO,MAAM,EACb,kBAAkB,MAAM,KACtB,OAAO,CAAC,IAAI,CAAC,CAShB"}
@@ -0,0 +1,55 @@
1
+ import { getStripe, stripeCall } from './billing.stripe';
2
+ import { DelightError } from '@delightstack/utilities';
3
+ /**
4
+ * Reports a usage event to a Stripe Billing Meter.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * await reportMeterEvent(config, {
9
+ * meter_id: 'ai-tokens',
10
+ * customer_id: 'cus_xxx',
11
+ * value: result.usage.total_tokens,
12
+ * });
13
+ * ```
14
+ */
15
+ export async function reportMeterEvent(config, options) {
16
+ const meter_def = config.meters?.find((m) => m.id === options.meter_id);
17
+ if (!meter_def) {
18
+ throw DelightError.badRequest(`Unknown meter: ${options.meter_id}`);
19
+ }
20
+ const stripe = getStripe(config);
21
+ await stripeCall(() => stripe.billing.meterEvents.create({
22
+ event_name: meter_def.event_name,
23
+ payload: {
24
+ stripe_customer_id: options.customer_id,
25
+ [meter_def.value_key ?? 'value']: String(options.value),
26
+ },
27
+ timestamp: options.timestamp ?? Math.floor(Date.now() / 1000),
28
+ }, options.idempotency_key ? { idempotencyKey: options.idempotency_key } : undefined));
29
+ }
30
+ /**
31
+ * Creates a helper function bound to a specific meter and config.
32
+ * Designed for integration with the AI package.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * // In your Durable Object:
37
+ * const reportTokenUsage = createMeterReporter(billingConfig, 'ai-tokens');
38
+ *
39
+ * // After each AI completion:
40
+ * const result = await ai.complete(options);
41
+ * await reportTokenUsage(customer_id, result.usage.total_tokens);
42
+ * ```
43
+ */
44
+ export function createMeterReporter(config, meter_id) {
45
+ return async (customer_id, value, idempotency_key) => {
46
+ if (value <= 0)
47
+ return; // Skip zero/negative usage
48
+ await reportMeterEvent(config, {
49
+ meter_id,
50
+ customer_id,
51
+ value,
52
+ idempotency_key,
53
+ });
54
+ };
55
+ }
@@ -0,0 +1,24 @@
1
+ import type { ResolvedBillingConfig } from './billing.config';
2
+ /**
3
+ * Syncs plan definitions to Stripe products and prices.
4
+ * Uses lookup_keys so that prices can be updated without changing code references.
5
+ * Idempotent — safe to call on every startup.
6
+ *
7
+ * Strategy:
8
+ * - Products are matched by metadata.plan_id
9
+ * - Prices are matched by lookup_key (Stripe's built-in mechanism)
10
+ * - Existing resources are updated; missing ones are created
11
+ * - Nothing is deleted (archived plans stay in Stripe)
12
+ */
13
+ export declare function syncProducts(config: ResolvedBillingConfig): Promise<void>;
14
+ /**
15
+ * Syncs meter definitions to Stripe Billing Meters.
16
+ * Idempotent — creates meters that do not exist.
17
+ */
18
+ export declare function syncMeters(config: ResolvedBillingConfig): Promise<void>;
19
+ /**
20
+ * Run a full sync of products, prices, and meters.
21
+ * Called automatically on first request if `sync_on_startup` is enabled.
22
+ */
23
+ export declare function syncAll(config: ResolvedBillingConfig): Promise<void>;
24
+ //# sourceMappingURL=billing.products.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"billing.products.d.ts","sourceRoot":"","sources":["../../src/server/billing.products.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAO9D;;;;;;;;;;GAUG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsF/E;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CA8B7E;AAED;;;GAGG;AACH,wBAAsB,OAAO,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAG1E"}
@@ -0,0 +1,124 @@
1
+ import { getStripe, stripeCall } from './billing.stripe';
2
+ /** Track whether sync has been performed this lifecycle */
3
+ let products_synced = false;
4
+ let meters_synced = false;
5
+ /**
6
+ * Syncs plan definitions to Stripe products and prices.
7
+ * Uses lookup_keys so that prices can be updated without changing code references.
8
+ * Idempotent — safe to call on every startup.
9
+ *
10
+ * Strategy:
11
+ * - Products are matched by metadata.plan_id
12
+ * - Prices are matched by lookup_key (Stripe's built-in mechanism)
13
+ * - Existing resources are updated; missing ones are created
14
+ * - Nothing is deleted (archived plans stay in Stripe)
15
+ */
16
+ export async function syncProducts(config) {
17
+ if (products_synced || !config.plans?.length)
18
+ return;
19
+ products_synced = true;
20
+ const stripe = getStripe(config);
21
+ for (const plan of config.plans) {
22
+ // 1. Ensure product exists
23
+ let product;
24
+ // Search by metadata.plan_id
25
+ const products = await stripeCall(() => stripe.products.search({
26
+ query: `metadata['plan_id']:'${plan.id}'`,
27
+ }));
28
+ product = products.data[0];
29
+ if (!product) {
30
+ // Create new product
31
+ product = await stripeCall(() => stripe.products.create({
32
+ name: plan.name,
33
+ description: plan.description,
34
+ metadata: { plan_id: plan.id, ...plan.metadata },
35
+ }));
36
+ }
37
+ else {
38
+ // Update existing product
39
+ await stripeCall(() => stripe.products.update(product.id, {
40
+ name: plan.name,
41
+ description: plan.description,
42
+ active: !plan.archived,
43
+ metadata: { plan_id: plan.id, ...plan.metadata },
44
+ }));
45
+ }
46
+ // 2. Ensure price exists with the correct lookup_key
47
+ const prices = await stripeCall(() => stripe.prices.list({ lookup_keys: [plan.lookup_key], limit: 1 }));
48
+ if (!prices.data.length) {
49
+ // Create price with lookup_key
50
+ await stripeCall(() => stripe.prices.create({
51
+ product: product.id,
52
+ unit_amount: plan.amount,
53
+ currency: plan.currency ?? 'usd',
54
+ recurring: {
55
+ interval: plan.interval,
56
+ interval_count: plan.interval_count ?? 1,
57
+ },
58
+ lookup_key: plan.lookup_key,
59
+ transfer_lookup_key: true,
60
+ metadata: { plan_id: plan.id },
61
+ }));
62
+ }
63
+ else {
64
+ // If price exists but amount/interval changed, create new price
65
+ // and transfer the lookup_key
66
+ const existing_price = prices.data[0];
67
+ if (existing_price.unit_amount !== plan.amount ||
68
+ existing_price.recurring?.interval !== plan.interval ||
69
+ (existing_price.recurring?.interval_count ?? 1) !== (plan.interval_count ?? 1)) {
70
+ await stripeCall(() => stripe.prices.create({
71
+ product: product.id,
72
+ unit_amount: plan.amount,
73
+ currency: plan.currency ?? 'usd',
74
+ recurring: {
75
+ interval: plan.interval,
76
+ interval_count: plan.interval_count ?? 1,
77
+ },
78
+ lookup_key: plan.lookup_key,
79
+ transfer_lookup_key: true,
80
+ metadata: { plan_id: plan.id },
81
+ }));
82
+ }
83
+ }
84
+ }
85
+ }
86
+ /**
87
+ * Syncs meter definitions to Stripe Billing Meters.
88
+ * Idempotent — creates meters that do not exist.
89
+ */
90
+ export async function syncMeters(config) {
91
+ if (meters_synced || !config.meters?.length)
92
+ return;
93
+ meters_synced = true;
94
+ const stripe = getStripe(config);
95
+ // List existing meters
96
+ const existing = await stripeCall(() => stripe.billing.meters.list({ limit: 100 }));
97
+ for (const meter of config.meters) {
98
+ const found = existing.data.find((m) => m.event_name === meter.event_name);
99
+ if (!found) {
100
+ await stripeCall(() => stripe.billing.meters.create({
101
+ display_name: meter.display_name,
102
+ event_name: meter.event_name,
103
+ default_aggregation: {
104
+ formula: meter.aggregation,
105
+ },
106
+ customer_mapping: {
107
+ type: 'by_id',
108
+ event_payload_key: 'stripe_customer_id',
109
+ },
110
+ value_settings: {
111
+ event_payload_key: meter.value_key ?? 'value',
112
+ },
113
+ }));
114
+ }
115
+ }
116
+ }
117
+ /**
118
+ * Run a full sync of products, prices, and meters.
119
+ * Called automatically on first request if `sync_on_startup` is enabled.
120
+ */
121
+ export async function syncAll(config) {
122
+ await syncProducts(config);
123
+ await syncMeters(config);
124
+ }
@@ -0,0 +1,8 @@
1
+ import type { RequestEvent } from '@sveltejs/kit';
2
+ import type { ResolvedBillingConfig, AuthServerRpc, WebsocketRpc } from './billing.config';
3
+ export interface RouteContext {
4
+ getAuthServer?: (event: RequestEvent) => AuthServerRpc | undefined;
5
+ getWebsocket?: (event: RequestEvent) => WebsocketRpc | undefined;
6
+ }
7
+ export declare function handleBillingRoute(event: RequestEvent, config: ResolvedBillingConfig, route_path: string, method: string, ctx: RouteContext): Promise<Response>;
8
+ //# sourceMappingURL=billing.routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"billing.routes.d.ts","sourceRoot":"","sources":["../../src/server/billing.routes.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;AAa1B,MAAM,WAAW,YAAY;IAC5B,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;AAoLD,wBAAsB,kBAAkB,CACvC,KAAK,EAAE,YAAY,EACnB,MAAM,EAAE,qBAAqB,EAC7B,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,YAAY,GACf,OAAO,CAAC,QAAQ,CAAC,CA+XnB"}