@shware/purchase 5.2.2 → 7.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/README.md CHANGED
@@ -12,18 +12,18 @@ signatures and calling store APIs stays in your app, with the official SDKs.
12
12
  ```sh
13
13
  pnpm add @shware/purchase zod
14
14
  # optional peers, only for the subpaths you use
15
- pnpm add stripe # @shware/purchase/stripe (^22.6.2 || ^23.0.0)
15
+ pnpm add stripe # @shware/purchase/stripe (^23.0.0)
16
16
  pnpm add @apple/app-store-server-library # @shware/purchase/app-store (^3.1.0)
17
17
  ```
18
18
 
19
19
  ## Entry points
20
20
 
21
- | Import path | Contents |
22
- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
23
- | `@shware/purchase` | Subscription model (`PLATFORMS`, `SUBSCRIPTION_STATUSES`, `AVAILABLE_STATUSES`, cancellation types) and the three configs |
24
- | `@shware/purchase/stripe` | `StripeEventHandler`, Stripe mappers, checkout schemas, `METADATA_KEYS`, analytics helpers, browser pending-checkout helpers |
25
- | `@shware/purchase/app-store` | `AppStoreNotificationHandler` for App Store Server Notifications V2, `mapCancellationDetails` |
26
- | `@shware/purchase/google-play` | Real-time developer notification (RTDN) types and enums, subscription state and cancellation mappers |
21
+ | Import path | Contents |
22
+ | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
23
+ | `@shware/purchase` | Subscription model (`PLATFORMS`, `SUBSCRIPTION_STATUSES`, `AVAILABLE_STATUSES`, cancellation types) and the three configs |
24
+ | `@shware/purchase/stripe` | `StripeThinEventHandler`, `StripeEventHandler`, Stripe mappers, checkout schemas, `METADATA_KEYS`, analytics helpers, browser pending-checkout helpers |
25
+ | `@shware/purchase/app-store` | `AppStoreNotificationHandler` for App Store Server Notifications V2, `mapCancellationDetails` |
26
+ | `@shware/purchase/google-play` | Real-time developer notification (RTDN) types and enums, subscription state and cancellation mappers |
27
27
 
28
28
  ## Subscription model
29
29
 
@@ -131,14 +131,38 @@ so existing subscribers keep resolving.
131
131
 
132
132
  ### Webhooks
133
133
 
134
- Verify the signature with the Stripe SDK, then dispatch with `StripeEventHandler`. `on` is typed
135
- per event, so each handler receives the right `event.data.object`; `process` throws for an event
136
- type with no handler.
134
+ Two dispatchers, one per Stripe [event format](https://docs.stripe.com/events/how-events-work#event-formats).
135
+ Both are typed per event type, run every handler registered for a type, and throw from `process`
136
+ for a type with no handler.
137
+
138
+ | Handler | Event format | Verify with | Handlers receive |
139
+ | ------------------------ | ------------ | ------------------------------------------ | ---------------------------------------- |
140
+ | `StripeThinEventHandler` | thin | `stripe.parseEventNotificationAsync(...)` | the typed notification (`v1.*` types) |
141
+ | `StripeEventHandler` | snapshot | `stripe.webhooks.constructEventAsync(...)` | `event.data.object` for the event's type |
142
+
143
+ Which to use: Stripe recommends thin events for new integrations, and so do we. Use snapshot
144
+ events when you need the full payload as it was when the event fired, or
145
+ `data.previous_attributes`.
146
+
147
+ #### Thin events (recommended)
148
+
149
+ A thin event only says which object changed (`type`, `id`, `created`, `reason`, `related_object`),
150
+ so each handler reads current state from Stripe with `notification.fetchRelatedObject()` (or the
151
+ full event with `fetchEvent()`). The SDK never pre-fetches: some handlers only need
152
+ `related_object.id`. `on` is keyed by thin event type (`v1.customer.subscription.updated`, ...),
153
+ so `fetchRelatedObject()` resolves to the right Stripe object. Delivery order carries no meaning:
154
+ re-read state instead of applying payloads in order, and dedupe one-off facts (credits, payments,
155
+ refunds) with idempotency keys. `StripeThinEventNotificationOf<T>` types a handler shared across
156
+ several types.
157
+
158
+ Create the event destination with the thin payload style and the `v1.*` types you handle. A
159
+ Cloudflare Workers route with Hono (`parseEventNotificationAsync` uses Web Crypto there):
137
160
 
138
161
  ```ts
139
162
  import {
140
163
  METADATA_KEYS,
141
- StripeEventHandler,
164
+ StripeThinEventHandler,
165
+ type StripeThinEventNotificationOf,
142
166
  mapCancellationDetails,
143
167
  mapSubscriptionStatus,
144
168
  mapTime,
@@ -147,23 +171,31 @@ import Stripe from 'stripe';
147
171
 
148
172
  const stripe = new Stripe(env.STRIPE_SECRET_KEY);
149
173
 
150
- export async function processStripeEvent(signature: string, body: string) {
151
- const event = await stripe.webhooks.constructEventAsync(
152
- body,
153
- signature,
174
+ const handler = StripeThinEventHandler.create()
175
+ .on('v1.checkout.session.completed', handleCheckoutSessionCompleted)
176
+ .on('v1.invoice.paid', handleInvoicePaid)
177
+ .on('v1.customer.subscription.created', handleSubscriptionEvent)
178
+ .on('v1.customer.subscription.updated', handleSubscriptionEvent)
179
+ .on('v1.customer.subscription.deleted', handleSubscriptionEvent);
180
+
181
+ app.post('/webhooks/stripe', async (c) => {
182
+ const notification = await stripe.parseEventNotificationAsync(
183
+ await c.req.text(),
184
+ c.req.header('stripe-signature') ?? '',
154
185
  env.STRIPE_WEBHOOK_SECRET
155
186
  );
156
-
157
- await StripeEventHandler.create()
158
- .on('checkout.session.completed', handleCheckoutSessionCompleted)
159
- .on('invoice.payment_succeeded', handleInvoicePaymentSucceeded)
160
- .on('customer.subscription.created', handleSubscriptionEvent)
161
- .on('customer.subscription.updated', handleSubscriptionEvent)
162
- .on('customer.subscription.deleted', handleSubscriptionEvent)
163
- .process(event);
164
- }
165
-
166
- async function handleSubscriptionEvent(sub: Stripe.Subscription) {
187
+ await handler.process(notification);
188
+ return c.body(null, 204);
189
+ });
190
+
191
+ async function handleSubscriptionEvent(
192
+ notification: StripeThinEventNotificationOf<
193
+ | 'v1.customer.subscription.created'
194
+ | 'v1.customer.subscription.updated'
195
+ | 'v1.customer.subscription.deleted'
196
+ >
197
+ ) {
198
+ const sub = await notification.fetchRelatedObject(); // Stripe.Subscription, current state
167
199
  const userId = sub.metadata[METADATA_KEYS.USER_ID];
168
200
  const productId = sub.metadata[METADATA_KEYS.PRODUCT_ID];
169
201
  await saveSubscription({
@@ -178,6 +210,41 @@ async function handleSubscriptionEvent(sub: Stripe.Subscription) {
178
210
  }
179
211
  ```
180
212
 
213
+ #### Snapshot events
214
+
215
+ `on` is keyed by snapshot event type (`customer.subscription.updated`, ...) and each handler
216
+ receives that event's `data.object`, typed.
217
+
218
+ ```ts
219
+ import { StripeEventHandler } from '@shware/purchase/stripe';
220
+
221
+ export async function processStripeEvent(signature: string, body: string) {
222
+ const event = await stripe.webhooks.constructEventAsync(
223
+ body,
224
+ signature,
225
+ env.STRIPE_WEBHOOK_SECRET
226
+ );
227
+
228
+ await StripeEventHandler.create()
229
+ .on('invoice.payment_succeeded', handleInvoicePaymentSucceeded) // (invoice: Stripe.Invoice)
230
+ .on('customer.subscription.updated', handleSubscription) // (sub: Stripe.Subscription)
231
+ .process(event);
232
+ }
233
+ ```
234
+
235
+ #### Moving from snapshot to thin events
236
+
237
+ - Switch the event destination to the thin payload style; `parseEventNotificationAsync` rejects
238
+ snapshot payloads.
239
+ - Replace `constructEventAsync` with `parseEventNotificationAsync` and `StripeEventHandler` with
240
+ `StripeThinEventHandler`.
241
+ - Prefix every `on` type with `v1.` (`invoice.paid` becomes `v1.invoice.paid`).
242
+ - Handlers receive the notification instead of `event.data.object`: call
243
+ `await notification.fetchRelatedObject()`, or use `notification.related_object.id` when the id
244
+ is enough. The fetched object is current state, so handlers must not depend on delivery order.
245
+ - The mappers (`mapSubscriptionStatus`, `mapInvoice`, `mapCancellationDetails`, ...) take the
246
+ fetched objects unchanged.
247
+
181
248
  ### Checkout
182
249
 
183
250
  `createCheckoutSessionSchema(productIds)` is a `zod/mini` schema for the create-checkout request
@@ -2,12 +2,14 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_stripe_config = require("./config.cjs");
3
3
  const require_stripe_handler = require("./handler.cjs");
4
4
  const require_stripe_mapper = require("./mapper.cjs");
5
+ const require_stripe_thin_handler = require("./thin-handler.cjs");
5
6
  const require_stripe_schema = require("./schema.cjs");
6
7
  const require_stripe_metadata = require("./metadata.cjs");
7
8
  const require_stripe_pending_checkout = require("./pending-checkout.cjs");
8
9
  exports.METADATA_KEYS = require_stripe_metadata.METADATA_KEYS;
9
10
  exports.StripeConfig = require_stripe_config.StripeConfig;
10
11
  exports.StripeEventHandler = require_stripe_handler.StripeEventHandler;
12
+ exports.StripeThinEventHandler = require_stripe_thin_handler.StripeThinEventHandler;
11
13
  exports.addPendingCheckout = require_stripe_pending_checkout.addPendingCheckout;
12
14
  exports.cancellationDetailsSchema = require_stripe_schema.cancellationDetailsSchema;
13
15
  exports.createCheckoutSessionSchema = require_stripe_schema.createCheckoutSessionSchema;
@@ -26,3 +28,4 @@ exports.mapTime = require_stripe_mapper.mapTime;
26
28
  exports.minorUnits = require_stripe_mapper.minorUnits;
27
29
  exports.price = require_stripe_mapper.price;
28
30
  exports.removePendingCheckout = require_stripe_pending_checkout.removePendingCheckout;
31
+ exports.returnUrlSchema = require_stripe_schema.returnUrlSchema;
@@ -2,8 +2,9 @@ import { CancellationDetails } from "../subscription/index.cjs";
2
2
  import { PriceId, StripeConfig, StripeMode } from "./config.cjs";
3
3
  import { StripeEventHandler } from "./handler.cjs";
4
4
  import { BeginCheckoutProperties, CheckoutSession, LineItem, PaymentStatus, Price, ProductPrice, PurchaseProperties, getBeginCheckoutProperties, getPurchaseProperties, mapCancellationDetails, mapCharge, mapCheckoutSession, mapInvoice, mapLineItem, mapPaymentIntent, mapPrice, mapSubscriptionStatus, mapTime, minorUnits, price } from "./mapper.cjs";
5
- import { CreateCheckoutSessionDTO, cancellationDetailsSchema, createCheckoutSessionSchema } from "./schema.cjs";
5
+ import { StripeThinEventHandler, StripeThinEventNotificationOf, StripeThinEventType } from "./thin-handler.cjs";
6
+ import { AllowedOrigins, CreateCheckoutSessionDTO, CreateCheckoutSessionSchemaOptions, cancellationDetailsSchema, createCheckoutSessionSchema, returnUrlSchema } from "./schema.cjs";
6
7
  import { METADATA_KEYS } from "./metadata.cjs";
7
8
  import { addPendingCheckout, getPendingCheckouts, removePendingCheckout } from "./pending-checkout.cjs";
8
9
  import { Stripe } from "stripe";
9
- export { type BeginCheckoutProperties, type CancellationDetails, type CheckoutSession, type CreateCheckoutSessionDTO, type LineItem, METADATA_KEYS, type PaymentStatus, type Price, type PriceId, type ProductPrice, type PurchaseProperties, type Stripe, StripeConfig, StripeEventHandler, type StripeMode, addPendingCheckout, cancellationDetailsSchema, createCheckoutSessionSchema, getBeginCheckoutProperties, getPendingCheckouts, getPurchaseProperties, mapCancellationDetails, mapCharge, mapCheckoutSession, mapInvoice, mapLineItem, mapPaymentIntent, mapPrice, mapSubscriptionStatus, mapTime, minorUnits, price, removePendingCheckout };
10
+ export { type AllowedOrigins, type BeginCheckoutProperties, type CancellationDetails, type CheckoutSession, type CreateCheckoutSessionDTO, type CreateCheckoutSessionSchemaOptions, type LineItem, METADATA_KEYS, type PaymentStatus, type Price, type PriceId, type ProductPrice, type PurchaseProperties, type Stripe, StripeConfig, StripeEventHandler, type StripeMode, StripeThinEventHandler, type StripeThinEventNotificationOf, type StripeThinEventType, addPendingCheckout, cancellationDetailsSchema, createCheckoutSessionSchema, getBeginCheckoutProperties, getPendingCheckouts, getPurchaseProperties, mapCancellationDetails, mapCharge, mapCheckoutSession, mapInvoice, mapLineItem, mapPaymentIntent, mapPrice, mapSubscriptionStatus, mapTime, minorUnits, price, removePendingCheckout, returnUrlSchema };
@@ -2,8 +2,9 @@ import { CancellationDetails } from "../subscription/index.mjs";
2
2
  import { PriceId, StripeConfig, StripeMode } from "./config.mjs";
3
3
  import { StripeEventHandler } from "./handler.mjs";
4
4
  import { BeginCheckoutProperties, CheckoutSession, LineItem, PaymentStatus, Price, ProductPrice, PurchaseProperties, getBeginCheckoutProperties, getPurchaseProperties, mapCancellationDetails, mapCharge, mapCheckoutSession, mapInvoice, mapLineItem, mapPaymentIntent, mapPrice, mapSubscriptionStatus, mapTime, minorUnits, price } from "./mapper.mjs";
5
- import { CreateCheckoutSessionDTO, cancellationDetailsSchema, createCheckoutSessionSchema } from "./schema.mjs";
5
+ import { StripeThinEventHandler, StripeThinEventNotificationOf, StripeThinEventType } from "./thin-handler.mjs";
6
+ import { AllowedOrigins, CreateCheckoutSessionDTO, CreateCheckoutSessionSchemaOptions, cancellationDetailsSchema, createCheckoutSessionSchema, returnUrlSchema } from "./schema.mjs";
6
7
  import { METADATA_KEYS } from "./metadata.mjs";
7
8
  import { addPendingCheckout, getPendingCheckouts, removePendingCheckout } from "./pending-checkout.mjs";
8
9
  import { Stripe } from "stripe";
9
- export { type BeginCheckoutProperties, type CancellationDetails, type CheckoutSession, type CreateCheckoutSessionDTO, type LineItem, METADATA_KEYS, type PaymentStatus, type Price, type PriceId, type ProductPrice, type PurchaseProperties, type Stripe, StripeConfig, StripeEventHandler, type StripeMode, addPendingCheckout, cancellationDetailsSchema, createCheckoutSessionSchema, getBeginCheckoutProperties, getPendingCheckouts, getPurchaseProperties, mapCancellationDetails, mapCharge, mapCheckoutSession, mapInvoice, mapLineItem, mapPaymentIntent, mapPrice, mapSubscriptionStatus, mapTime, minorUnits, price, removePendingCheckout };
10
+ export { type AllowedOrigins, type BeginCheckoutProperties, type CancellationDetails, type CheckoutSession, type CreateCheckoutSessionDTO, type CreateCheckoutSessionSchemaOptions, type LineItem, METADATA_KEYS, type PaymentStatus, type Price, type PriceId, type ProductPrice, type PurchaseProperties, type Stripe, StripeConfig, StripeEventHandler, type StripeMode, StripeThinEventHandler, type StripeThinEventNotificationOf, type StripeThinEventType, addPendingCheckout, cancellationDetailsSchema, createCheckoutSessionSchema, getBeginCheckoutProperties, getPendingCheckouts, getPurchaseProperties, mapCancellationDetails, mapCharge, mapCheckoutSession, mapInvoice, mapLineItem, mapPaymentIntent, mapPrice, mapSubscriptionStatus, mapTime, minorUnits, price, removePendingCheckout, returnUrlSchema };
@@ -1,7 +1,8 @@
1
1
  import { StripeConfig } from "./config.mjs";
2
2
  import { StripeEventHandler } from "./handler.mjs";
3
3
  import { getBeginCheckoutProperties, getPurchaseProperties, mapCancellationDetails, mapCharge, mapCheckoutSession, mapInvoice, mapLineItem, mapPaymentIntent, mapPrice, mapSubscriptionStatus, mapTime, minorUnits, price } from "./mapper.mjs";
4
- import { cancellationDetailsSchema, createCheckoutSessionSchema } from "./schema.mjs";
4
+ import { StripeThinEventHandler } from "./thin-handler.mjs";
5
+ import { cancellationDetailsSchema, createCheckoutSessionSchema, returnUrlSchema } from "./schema.mjs";
5
6
  import { METADATA_KEYS } from "./metadata.mjs";
6
7
  import { addPendingCheckout, getPendingCheckouts, removePendingCheckout } from "./pending-checkout.mjs";
7
- export { METADATA_KEYS, StripeConfig, StripeEventHandler, addPendingCheckout, cancellationDetailsSchema, createCheckoutSessionSchema, getBeginCheckoutProperties, getPendingCheckouts, getPurchaseProperties, mapCancellationDetails, mapCharge, mapCheckoutSession, mapInvoice, mapLineItem, mapPaymentIntent, mapPrice, mapSubscriptionStatus, mapTime, minorUnits, price, removePendingCheckout };
8
+ export { METADATA_KEYS, StripeConfig, StripeEventHandler, StripeThinEventHandler, addPendingCheckout, cancellationDetailsSchema, createCheckoutSessionSchema, getBeginCheckoutProperties, getPendingCheckouts, getPurchaseProperties, mapCancellationDetails, mapCharge, mapCheckoutSession, mapInvoice, mapLineItem, mapPaymentIntent, mapPrice, mapSubscriptionStatus, mapTime, minorUnits, price, removePendingCheckout, returnUrlSchema };
@@ -8,16 +8,56 @@ const cancellationDetailsSchema = (0, zod_mini.object)({
8
8
  feedback: (0, zod_mini.optional)((0, zod_mini.nullable)((0, zod_mini.enum)(require_subscription_index.CANCELLATION_FEEDBACKS))),
9
9
  resubscribeIntent: (0, zod_mini.optional)((0, zod_mini.nullable)((0, zod_mini.enum)(require_subscription_index.RESUBSCRIBE_INTENTS)))
10
10
  });
11
- function createCheckoutSessionSchema(productIds) {
11
+ /**
12
+ * Normalizes each allowed origin with `new URL()`, so `https://example.com/` and
13
+ * `https://example.com` are the same entry. Throws on anything that is not an http(s) origin: a
14
+ * misconfigured list must fail at startup, not let every return URL through or refuse them all.
15
+ */
16
+ function normalizeOrigins(origins) {
17
+ return origins.map((origin) => {
18
+ const parsed = new URL(origin);
19
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") throw new Error(`returnUrl: ${origin} is not an http(s) origin`);
20
+ return parsed.origin;
21
+ });
22
+ }
23
+ /**
24
+ * Whether `value` may be handed to Stripe as a URL its hosted pages (Checkout, the billing portal)
25
+ * send the browser back to. Origins are compared as parsed by `new URL()`, never by string prefix,
26
+ * so a lookalike host (`https://example.com.evil.com`), userinfo (`https://example.com@evil.com`),
27
+ * protocol-relative (`//evil.com`) and `javascript:` values are all refused. The scheme is part of
28
+ * the origin, so an https-only list refuses `http:` too.
29
+ */
30
+ function isAllowedReturnUrl(value, allowedOrigins) {
31
+ let parsed;
32
+ try {
33
+ parsed = new URL(value);
34
+ } catch {
35
+ return false;
36
+ }
37
+ if (parsed.username !== "" || parsed.password !== "") return false;
38
+ return allowedOrigins.includes(parsed.origin);
39
+ }
40
+ /**
41
+ * A client-supplied return / success / cancel URL. It goes straight to Stripe, so without an
42
+ * origin check anyone could create a session with their own account and hand out a
43
+ * `checkout.stripe.com` / `billing.stripe.com` link that ends on a page of their choosing: an open
44
+ * redirect behind a trusted domain. `allowedOrigins` is usually just the web app's origin.
45
+ */
46
+ function returnUrlSchema(allowedOrigins) {
47
+ const origins = normalizeOrigins(allowedOrigins);
48
+ return (0, zod_mini.url)().check((0, zod_mini.refine)((value) => isAllowedReturnUrl(value, origins), { message: `URL must be on ${origins.join(", ")}` }));
49
+ }
50
+ function createCheckoutSessionSchema(productIds, { allowedOrigins }) {
12
51
  return (0, zod_mini.object)({
13
52
  quantity: (0, zod_mini._default)((0, zod_mini.int)().check((0, zod_mini.gte)(1)), 1),
14
53
  productId: (0, zod_mini.enum)(productIds),
15
- cancelUrl: (0, zod_mini.optional)((0, zod_mini.url)()),
16
- successUrl: (0, zod_mini.optional)((0, zod_mini.url)())
54
+ cancelUrl: (0, zod_mini.optional)(returnUrlSchema(allowedOrigins)),
55
+ successUrl: (0, zod_mini.optional)(returnUrlSchema(allowedOrigins))
17
56
  });
18
57
  }
19
58
  //#endregion
20
59
  exports.cancellationDetailsSchema = cancellationDetailsSchema;
21
60
  exports.createCheckoutSessionSchema = createCheckoutSessionSchema;
61
+ exports.returnUrlSchema = returnUrlSchema;
22
62
 
23
63
  //# sourceMappingURL=schema.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema.cjs","names":["object","optional","nullable","string","maxLength","_enum","CANCELLATION_FEEDBACKS","RESUBSCRIBE_INTENTS","_default","int","gte","url"],"sources":["../../src/stripe/schema.ts"],"sourcesContent":["import {\n _default,\n enum as _enum,\n gte,\n int,\n maxLength,\n nullable,\n object,\n optional,\n string,\n url,\n} from 'zod/mini';\nimport { CANCELLATION_FEEDBACKS, RESUBSCRIBE_INTENTS } from '../subscription/index';\n\n/** What our own cancel dialog may send along with a cancellation (see `CancellationDetails`). */\nexport const cancellationDetailsSchema = object({\n comment: optional(nullable(string().check(maxLength(1024)))),\n feedback: optional(nullable(_enum(CANCELLATION_FEEDBACKS))),\n resubscribeIntent: optional(nullable(_enum(RESUBSCRIBE_INTENTS))),\n});\n\nexport function createCheckoutSessionSchema(productIds: string[]) {\n return object({\n quantity: _default(int().check(gte(1)), 1),\n productId: _enum(productIds),\n cancelUrl: optional(url()),\n successUrl: optional(url()),\n });\n}\n\n// Kept under the stripe entry for callers that imported it from here; the type is platform\n// neutral and lives with the subscription statuses.\nexport type { CancellationDetails } from '../subscription/index';\n\nexport interface CreateCheckoutSessionDTO {\n productId: string;\n cancelUrl?: string;\n successUrl?: `${string}session_id={CHECKOUT_SESSION_ID}${string}`;\n}\n"],"mappings":";;;;;AAeA,MAAa,6BAAA,GAA4BA,SAAAA,OAAAA,CAAO;CAC9C,UAAA,GAASC,SAAAA,SAAAA,EAAAA,GAASC,SAAAA,SAAAA,EAAAA,GAASC,SAAAA,OAAAA,CAAO,CAAC,CAAC,OAAA,GAAMC,SAAAA,UAAAA,CAAU,IAAI,CAAC,CAAC,CAAC;CAC3D,WAAA,GAAUH,SAAAA,SAAAA,EAAAA,GAASC,SAAAA,SAAAA,EAAAA,GAASG,SAAAA,KAAAA,CAAMC,2BAAAA,sBAAsB,CAAC,CAAC;CAC1D,oBAAA,GAAmBL,SAAAA,SAAAA,EAAAA,GAASC,SAAAA,SAAAA,EAAAA,GAASG,SAAAA,KAAAA,CAAME,2BAAAA,mBAAmB,CAAC,CAAC;AAClE,CAAC;AAED,SAAgB,4BAA4B,YAAsB;CAChE,QAAA,GAAOP,SAAAA,OAAAA,CAAO;EACZ,WAAA,GAAUQ,SAAAA,SAAAA,EAAAA,GAASC,SAAAA,IAAAA,CAAI,CAAC,CAAC,OAAA,GAAMC,SAAAA,IAAAA,CAAI,CAAC,CAAC,GAAG,CAAC;EACzC,YAAA,GAAWL,SAAAA,KAAAA,CAAM,UAAU;EAC3B,YAAA,GAAWJ,SAAAA,SAAAA,EAAAA,GAASU,SAAAA,IAAAA,CAAI,CAAC;EACzB,aAAA,GAAYV,SAAAA,SAAAA,EAAAA,GAASU,SAAAA,IAAAA,CAAI,CAAC;CAC5B,CAAC;AACH"}
1
+ {"version":3,"file":"schema.cjs","names":["object","optional","nullable","string","maxLength","_enum","CANCELLATION_FEEDBACKS","RESUBSCRIBE_INTENTS","url","refine","_default","int","gte"],"sources":["../../src/stripe/schema.ts"],"sourcesContent":["import {\n _default,\n enum as _enum,\n gte,\n int,\n maxLength,\n nullable,\n object,\n optional,\n refine,\n string,\n url,\n} from 'zod/mini';\nimport { CANCELLATION_FEEDBACKS, RESUBSCRIBE_INTENTS } from '../subscription/index';\n\n/** What our own cancel dialog may send along with a cancellation (see `CancellationDetails`). */\nexport const cancellationDetailsSchema = object({\n comment: optional(nullable(string().check(maxLength(1024)))),\n feedback: optional(nullable(_enum(CANCELLATION_FEEDBACKS))),\n resubscribeIntent: optional(nullable(_enum(RESUBSCRIBE_INTENTS))),\n});\n\n/** At least one origin: an empty list would refuse every return URL. */\nexport type AllowedOrigins = readonly [string, ...string[]];\n\n/**\n * Normalizes each allowed origin with `new URL()`, so `https://example.com/` and\n * `https://example.com` are the same entry. Throws on anything that is not an http(s) origin: a\n * misconfigured list must fail at startup, not let every return URL through or refuse them all.\n */\nfunction normalizeOrigins(origins: AllowedOrigins): string[] {\n return origins.map((origin) => {\n const parsed = new URL(origin);\n if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {\n throw new Error(`returnUrl: ${origin} is not an http(s) origin`);\n }\n return parsed.origin;\n });\n}\n\n/**\n * Whether `value` may be handed to Stripe as a URL its hosted pages (Checkout, the billing portal)\n * send the browser back to. Origins are compared as parsed by `new URL()`, never by string prefix,\n * so a lookalike host (`https://example.com.evil.com`), userinfo (`https://example.com@evil.com`),\n * protocol-relative (`//evil.com`) and `javascript:` values are all refused. The scheme is part of\n * the origin, so an https-only list refuses `http:` too.\n */\nfunction isAllowedReturnUrl(value: string, allowedOrigins: readonly string[]): boolean {\n let parsed: URL;\n try {\n parsed = new URL(value);\n } catch {\n return false;\n }\n if (parsed.username !== '' || parsed.password !== '') return false;\n return allowedOrigins.includes(parsed.origin);\n}\n\n/**\n * A client-supplied return / success / cancel URL. It goes straight to Stripe, so without an\n * origin check anyone could create a session with their own account and hand out a\n * `checkout.stripe.com` / `billing.stripe.com` link that ends on a page of their choosing: an open\n * redirect behind a trusted domain. `allowedOrigins` is usually just the web app's origin.\n */\nexport function returnUrlSchema(allowedOrigins: AllowedOrigins) {\n const origins = normalizeOrigins(allowedOrigins);\n return url().check(\n refine((value) => isAllowedReturnUrl(value, origins), {\n message: `URL must be on ${origins.join(', ')}`,\n })\n );\n}\n\nexport interface CreateCheckoutSessionSchemaOptions {\n /** Origins (`https://example.com`) `cancelUrl` and `successUrl` may point at; see `returnUrlSchema`. */\n allowedOrigins: AllowedOrigins;\n}\n\nexport function createCheckoutSessionSchema(\n productIds: string[],\n { allowedOrigins }: CreateCheckoutSessionSchemaOptions\n) {\n return object({\n quantity: _default(int().check(gte(1)), 1),\n productId: _enum(productIds),\n cancelUrl: optional(returnUrlSchema(allowedOrigins)),\n successUrl: optional(returnUrlSchema(allowedOrigins)),\n });\n}\n\n// Kept under the stripe entry for callers that imported it from here; the type is platform\n// neutral and lives with the subscription statuses.\nexport type { CancellationDetails } from '../subscription/index';\n\nexport interface CreateCheckoutSessionDTO {\n productId: string;\n cancelUrl?: string;\n successUrl?: `${string}session_id={CHECKOUT_SESSION_ID}${string}`;\n}\n"],"mappings":";;;;;AAgBA,MAAa,6BAAA,GAA4BA,SAAAA,OAAAA,CAAO;CAC9C,UAAA,GAASC,SAAAA,SAAAA,EAAAA,GAASC,SAAAA,SAAAA,EAAAA,GAASC,SAAAA,OAAAA,CAAO,CAAC,CAAC,OAAA,GAAMC,SAAAA,UAAAA,CAAU,IAAI,CAAC,CAAC,CAAC;CAC3D,WAAA,GAAUH,SAAAA,SAAAA,EAAAA,GAASC,SAAAA,SAAAA,EAAAA,GAASG,SAAAA,KAAAA,CAAMC,2BAAAA,sBAAsB,CAAC,CAAC;CAC1D,oBAAA,GAAmBL,SAAAA,SAAAA,EAAAA,GAASC,SAAAA,SAAAA,EAAAA,GAASG,SAAAA,KAAAA,CAAME,2BAAAA,mBAAmB,CAAC,CAAC;AAClE,CAAC;;;;;;AAUD,SAAS,iBAAiB,SAAmC;CAC3D,OAAO,QAAQ,KAAK,WAAW;EAC7B,MAAM,SAAS,IAAI,IAAI,MAAM;EAC7B,IAAI,OAAO,aAAa,YAAY,OAAO,aAAa,SACtD,MAAM,IAAI,MAAM,cAAc,OAAO,0BAA0B;EAEjE,OAAO,OAAO;CAChB,CAAC;AACH;;;;;;;;AASA,SAAS,mBAAmB,OAAe,gBAA4C;CACrF,IAAI;CACJ,IAAI;EACF,SAAS,IAAI,IAAI,KAAK;CACxB,QAAQ;EACN,OAAO;CACT;CACA,IAAI,OAAO,aAAa,MAAM,OAAO,aAAa,IAAI,OAAO;CAC7D,OAAO,eAAe,SAAS,OAAO,MAAM;AAC9C;;;;;;;AAQA,SAAgB,gBAAgB,gBAAgC;CAC9D,MAAM,UAAU,iBAAiB,cAAc;CAC/C,QAAA,GAAOC,SAAAA,IAAAA,CAAI,CAAC,CAAC,OAAA,GACXC,SAAAA,OAAAA,EAAQ,UAAU,mBAAmB,OAAO,OAAO,GAAG,EACpD,SAAS,kBAAkB,QAAQ,KAAK,IAAI,IAC9C,CAAC,CACH;AACF;AAOA,SAAgB,4BACd,YACA,EAAE,kBACF;CACA,QAAA,GAAOT,SAAAA,OAAAA,CAAO;EACZ,WAAA,GAAUU,SAAAA,SAAAA,EAAAA,GAASC,SAAAA,IAAAA,CAAI,CAAC,CAAC,OAAA,GAAMC,SAAAA,IAAAA,CAAI,CAAC,CAAC,GAAG,CAAC;EACzC,YAAA,GAAWP,SAAAA,KAAAA,CAAM,UAAU;EAC3B,YAAA,GAAWJ,SAAAA,SAAAA,CAAS,gBAAgB,cAAc,CAAC;EACnD,aAAA,GAAYA,SAAAA,SAAAA,CAAS,gBAAgB,cAAc,CAAC;CACtD,CAAC;AACH"}
@@ -19,7 +19,20 @@ export declare const cancellationDetailsSchema: import("zod/mini").ZodMiniObject
19
19
  yes: "yes";
20
20
  }>>>;
21
21
  }, import("zod/v4/core").$strip>;
22
- export declare function createCheckoutSessionSchema(productIds: string[]): import("zod/mini").ZodMiniObject<{
22
+ /** At least one origin: an empty list would refuse every return URL. */
23
+ export type AllowedOrigins = readonly [string, ...string[]];
24
+ /**
25
+ * A client-supplied return / success / cancel URL. It goes straight to Stripe, so without an
26
+ * origin check anyone could create a session with their own account and hand out a
27
+ * `checkout.stripe.com` / `billing.stripe.com` link that ends on a page of their choosing: an open
28
+ * redirect behind a trusted domain. `allowedOrigins` is usually just the web app's origin.
29
+ */
30
+ export declare function returnUrlSchema(allowedOrigins: AllowedOrigins): import("zod/mini").ZodMiniURL;
31
+ export interface CreateCheckoutSessionSchemaOptions {
32
+ /** Origins (`https://example.com`) `cancelUrl` and `successUrl` may point at; see `returnUrlSchema`. */
33
+ allowedOrigins: AllowedOrigins;
34
+ }
35
+ export declare function createCheckoutSessionSchema(productIds: string[], { allowedOrigins }: CreateCheckoutSessionSchemaOptions): import("zod/mini").ZodMiniObject<{
23
36
  quantity: import("zod/mini").ZodMiniDefault<import("zod/mini").ZodMiniInt>;
24
37
  productId: import("zod/mini").ZodMiniEnum<{
25
38
  [x: string]: string;
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.cts","names":[],"sources":["../../src/stripe/schema.ts"],"mappings":";;;qBAea,8CAAyB;;;;;;;;;;;;;;;;;yBAIpC;wBAEc,4BAA4B,0CAAoB;;;;;;;yBAO/D;iBAMgB;EACf;EACA;EACA"}
1
+ {"version":3,"file":"schema.d.cts","names":[],"sources":["../../src/stripe/schema.ts"],"mappings":";;;qBAgBa,8CAAyB;;;;;;;;;;;;;;;;;yBAIpC;;YAGU;;;;;;;wBAyCI,gBAAgB,gBAAgB,oCAAc;iBAS7C;;EAEf,gBAAgB;;wBAGF,4BACd,wBACE,kBAAkB,wDAAkC;;;;;;;yBAQvD;iBAMgB;EACf;EACA;EACA"}
@@ -19,7 +19,20 @@ export declare const cancellationDetailsSchema: import("zod/mini").ZodMiniObject
19
19
  yes: "yes";
20
20
  }>>>;
21
21
  }, import("zod/v4/core").$strip>;
22
- export declare function createCheckoutSessionSchema(productIds: string[]): import("zod/mini").ZodMiniObject<{
22
+ /** At least one origin: an empty list would refuse every return URL. */
23
+ export type AllowedOrigins = readonly [string, ...string[]];
24
+ /**
25
+ * A client-supplied return / success / cancel URL. It goes straight to Stripe, so without an
26
+ * origin check anyone could create a session with their own account and hand out a
27
+ * `checkout.stripe.com` / `billing.stripe.com` link that ends on a page of their choosing: an open
28
+ * redirect behind a trusted domain. `allowedOrigins` is usually just the web app's origin.
29
+ */
30
+ export declare function returnUrlSchema(allowedOrigins: AllowedOrigins): import("zod/mini").ZodMiniURL;
31
+ export interface CreateCheckoutSessionSchemaOptions {
32
+ /** Origins (`https://example.com`) `cancelUrl` and `successUrl` may point at; see `returnUrlSchema`. */
33
+ allowedOrigins: AllowedOrigins;
34
+ }
35
+ export declare function createCheckoutSessionSchema(productIds: string[], { allowedOrigins }: CreateCheckoutSessionSchemaOptions): import("zod/mini").ZodMiniObject<{
23
36
  quantity: import("zod/mini").ZodMiniDefault<import("zod/mini").ZodMiniInt>;
24
37
  productId: import("zod/mini").ZodMiniEnum<{
25
38
  [x: string]: string;
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.mts","names":[],"sources":["../../src/stripe/schema.ts"],"mappings":";;;qBAea,8CAAyB;;;;;;;;;;;;;;;;;yBAIpC;wBAEc,4BAA4B,0CAAoB;;;;;;;yBAO/D;iBAMgB;EACf;EACA;EACA"}
1
+ {"version":3,"file":"schema.d.mts","names":[],"sources":["../../src/stripe/schema.ts"],"mappings":";;;qBAgBa,8CAAyB;;;;;;;;;;;;;;;;;yBAIpC;;YAGU;;;;;;;wBAyCI,gBAAgB,gBAAgB,oCAAc;iBAS7C;;EAEf,gBAAgB;;wBAGF,4BACd,wBACE,kBAAkB,wDAAkC;;;;;;;yBAQvD;iBAMgB;EACf;EACA;EACA"}
@@ -1,5 +1,5 @@
1
1
  import { CANCELLATION_FEEDBACKS, RESUBSCRIBE_INTENTS } from "../subscription/index.mjs";
2
- import { _default, enum as enum$1, gte, int, maxLength, nullable, object, optional, string, url } from "zod/mini";
2
+ import { _default, enum as enum$1, gte, int, maxLength, nullable, object, optional, refine, string, url } from "zod/mini";
3
3
  //#region src/stripe/schema.ts
4
4
  /** What our own cancel dialog may send along with a cancellation (see `CancellationDetails`). */
5
5
  const cancellationDetailsSchema = object({
@@ -7,15 +7,54 @@ const cancellationDetailsSchema = object({
7
7
  feedback: optional(nullable(enum$1(CANCELLATION_FEEDBACKS))),
8
8
  resubscribeIntent: optional(nullable(enum$1(RESUBSCRIBE_INTENTS)))
9
9
  });
10
- function createCheckoutSessionSchema(productIds) {
10
+ /**
11
+ * Normalizes each allowed origin with `new URL()`, so `https://example.com/` and
12
+ * `https://example.com` are the same entry. Throws on anything that is not an http(s) origin: a
13
+ * misconfigured list must fail at startup, not let every return URL through or refuse them all.
14
+ */
15
+ function normalizeOrigins(origins) {
16
+ return origins.map((origin) => {
17
+ const parsed = new URL(origin);
18
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") throw new Error(`returnUrl: ${origin} is not an http(s) origin`);
19
+ return parsed.origin;
20
+ });
21
+ }
22
+ /**
23
+ * Whether `value` may be handed to Stripe as a URL its hosted pages (Checkout, the billing portal)
24
+ * send the browser back to. Origins are compared as parsed by `new URL()`, never by string prefix,
25
+ * so a lookalike host (`https://example.com.evil.com`), userinfo (`https://example.com@evil.com`),
26
+ * protocol-relative (`//evil.com`) and `javascript:` values are all refused. The scheme is part of
27
+ * the origin, so an https-only list refuses `http:` too.
28
+ */
29
+ function isAllowedReturnUrl(value, allowedOrigins) {
30
+ let parsed;
31
+ try {
32
+ parsed = new URL(value);
33
+ } catch {
34
+ return false;
35
+ }
36
+ if (parsed.username !== "" || parsed.password !== "") return false;
37
+ return allowedOrigins.includes(parsed.origin);
38
+ }
39
+ /**
40
+ * A client-supplied return / success / cancel URL. It goes straight to Stripe, so without an
41
+ * origin check anyone could create a session with their own account and hand out a
42
+ * `checkout.stripe.com` / `billing.stripe.com` link that ends on a page of their choosing: an open
43
+ * redirect behind a trusted domain. `allowedOrigins` is usually just the web app's origin.
44
+ */
45
+ function returnUrlSchema(allowedOrigins) {
46
+ const origins = normalizeOrigins(allowedOrigins);
47
+ return url().check(refine((value) => isAllowedReturnUrl(value, origins), { message: `URL must be on ${origins.join(", ")}` }));
48
+ }
49
+ function createCheckoutSessionSchema(productIds, { allowedOrigins }) {
11
50
  return object({
12
51
  quantity: _default(int().check(gte(1)), 1),
13
52
  productId: enum$1(productIds),
14
- cancelUrl: optional(url()),
15
- successUrl: optional(url())
53
+ cancelUrl: optional(returnUrlSchema(allowedOrigins)),
54
+ successUrl: optional(returnUrlSchema(allowedOrigins))
16
55
  });
17
56
  }
18
57
  //#endregion
19
- export { cancellationDetailsSchema, createCheckoutSessionSchema };
58
+ export { cancellationDetailsSchema, createCheckoutSessionSchema, returnUrlSchema };
20
59
 
21
60
  //# sourceMappingURL=schema.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema.mjs","names":["_enum"],"sources":["../../src/stripe/schema.ts"],"sourcesContent":["import {\n _default,\n enum as _enum,\n gte,\n int,\n maxLength,\n nullable,\n object,\n optional,\n string,\n url,\n} from 'zod/mini';\nimport { CANCELLATION_FEEDBACKS, RESUBSCRIBE_INTENTS } from '../subscription/index';\n\n/** What our own cancel dialog may send along with a cancellation (see `CancellationDetails`). */\nexport const cancellationDetailsSchema = object({\n comment: optional(nullable(string().check(maxLength(1024)))),\n feedback: optional(nullable(_enum(CANCELLATION_FEEDBACKS))),\n resubscribeIntent: optional(nullable(_enum(RESUBSCRIBE_INTENTS))),\n});\n\nexport function createCheckoutSessionSchema(productIds: string[]) {\n return object({\n quantity: _default(int().check(gte(1)), 1),\n productId: _enum(productIds),\n cancelUrl: optional(url()),\n successUrl: optional(url()),\n });\n}\n\n// Kept under the stripe entry for callers that imported it from here; the type is platform\n// neutral and lives with the subscription statuses.\nexport type { CancellationDetails } from '../subscription/index';\n\nexport interface CreateCheckoutSessionDTO {\n productId: string;\n cancelUrl?: string;\n successUrl?: `${string}session_id={CHECKOUT_SESSION_ID}${string}`;\n}\n"],"mappings":";;;;AAeA,MAAa,4BAA4B,OAAO;CAC9C,SAAS,SAAS,SAAS,OAAO,CAAC,CAAC,MAAM,UAAU,IAAI,CAAC,CAAC,CAAC;CAC3D,UAAU,SAAS,SAASA,OAAM,sBAAsB,CAAC,CAAC;CAC1D,mBAAmB,SAAS,SAASA,OAAM,mBAAmB,CAAC,CAAC;AAClE,CAAC;AAED,SAAgB,4BAA4B,YAAsB;CAChE,OAAO,OAAO;EACZ,UAAU,SAAS,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,GAAG,CAAC;EACzC,WAAWA,OAAM,UAAU;EAC3B,WAAW,SAAS,IAAI,CAAC;EACzB,YAAY,SAAS,IAAI,CAAC;CAC5B,CAAC;AACH"}
1
+ {"version":3,"file":"schema.mjs","names":["_enum"],"sources":["../../src/stripe/schema.ts"],"sourcesContent":["import {\n _default,\n enum as _enum,\n gte,\n int,\n maxLength,\n nullable,\n object,\n optional,\n refine,\n string,\n url,\n} from 'zod/mini';\nimport { CANCELLATION_FEEDBACKS, RESUBSCRIBE_INTENTS } from '../subscription/index';\n\n/** What our own cancel dialog may send along with a cancellation (see `CancellationDetails`). */\nexport const cancellationDetailsSchema = object({\n comment: optional(nullable(string().check(maxLength(1024)))),\n feedback: optional(nullable(_enum(CANCELLATION_FEEDBACKS))),\n resubscribeIntent: optional(nullable(_enum(RESUBSCRIBE_INTENTS))),\n});\n\n/** At least one origin: an empty list would refuse every return URL. */\nexport type AllowedOrigins = readonly [string, ...string[]];\n\n/**\n * Normalizes each allowed origin with `new URL()`, so `https://example.com/` and\n * `https://example.com` are the same entry. Throws on anything that is not an http(s) origin: a\n * misconfigured list must fail at startup, not let every return URL through or refuse them all.\n */\nfunction normalizeOrigins(origins: AllowedOrigins): string[] {\n return origins.map((origin) => {\n const parsed = new URL(origin);\n if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {\n throw new Error(`returnUrl: ${origin} is not an http(s) origin`);\n }\n return parsed.origin;\n });\n}\n\n/**\n * Whether `value` may be handed to Stripe as a URL its hosted pages (Checkout, the billing portal)\n * send the browser back to. Origins are compared as parsed by `new URL()`, never by string prefix,\n * so a lookalike host (`https://example.com.evil.com`), userinfo (`https://example.com@evil.com`),\n * protocol-relative (`//evil.com`) and `javascript:` values are all refused. The scheme is part of\n * the origin, so an https-only list refuses `http:` too.\n */\nfunction isAllowedReturnUrl(value: string, allowedOrigins: readonly string[]): boolean {\n let parsed: URL;\n try {\n parsed = new URL(value);\n } catch {\n return false;\n }\n if (parsed.username !== '' || parsed.password !== '') return false;\n return allowedOrigins.includes(parsed.origin);\n}\n\n/**\n * A client-supplied return / success / cancel URL. It goes straight to Stripe, so without an\n * origin check anyone could create a session with their own account and hand out a\n * `checkout.stripe.com` / `billing.stripe.com` link that ends on a page of their choosing: an open\n * redirect behind a trusted domain. `allowedOrigins` is usually just the web app's origin.\n */\nexport function returnUrlSchema(allowedOrigins: AllowedOrigins) {\n const origins = normalizeOrigins(allowedOrigins);\n return url().check(\n refine((value) => isAllowedReturnUrl(value, origins), {\n message: `URL must be on ${origins.join(', ')}`,\n })\n );\n}\n\nexport interface CreateCheckoutSessionSchemaOptions {\n /** Origins (`https://example.com`) `cancelUrl` and `successUrl` may point at; see `returnUrlSchema`. */\n allowedOrigins: AllowedOrigins;\n}\n\nexport function createCheckoutSessionSchema(\n productIds: string[],\n { allowedOrigins }: CreateCheckoutSessionSchemaOptions\n) {\n return object({\n quantity: _default(int().check(gte(1)), 1),\n productId: _enum(productIds),\n cancelUrl: optional(returnUrlSchema(allowedOrigins)),\n successUrl: optional(returnUrlSchema(allowedOrigins)),\n });\n}\n\n// Kept under the stripe entry for callers that imported it from here; the type is platform\n// neutral and lives with the subscription statuses.\nexport type { CancellationDetails } from '../subscription/index';\n\nexport interface CreateCheckoutSessionDTO {\n productId: string;\n cancelUrl?: string;\n successUrl?: `${string}session_id={CHECKOUT_SESSION_ID}${string}`;\n}\n"],"mappings":";;;;AAgBA,MAAa,4BAA4B,OAAO;CAC9C,SAAS,SAAS,SAAS,OAAO,CAAC,CAAC,MAAM,UAAU,IAAI,CAAC,CAAC,CAAC;CAC3D,UAAU,SAAS,SAASA,OAAM,sBAAsB,CAAC,CAAC;CAC1D,mBAAmB,SAAS,SAASA,OAAM,mBAAmB,CAAC,CAAC;AAClE,CAAC;;;;;;AAUD,SAAS,iBAAiB,SAAmC;CAC3D,OAAO,QAAQ,KAAK,WAAW;EAC7B,MAAM,SAAS,IAAI,IAAI,MAAM;EAC7B,IAAI,OAAO,aAAa,YAAY,OAAO,aAAa,SACtD,MAAM,IAAI,MAAM,cAAc,OAAO,0BAA0B;EAEjE,OAAO,OAAO;CAChB,CAAC;AACH;;;;;;;;AASA,SAAS,mBAAmB,OAAe,gBAA4C;CACrF,IAAI;CACJ,IAAI;EACF,SAAS,IAAI,IAAI,KAAK;CACxB,QAAQ;EACN,OAAO;CACT;CACA,IAAI,OAAO,aAAa,MAAM,OAAO,aAAa,IAAI,OAAO;CAC7D,OAAO,eAAe,SAAS,OAAO,MAAM;AAC9C;;;;;;;AAQA,SAAgB,gBAAgB,gBAAgC;CAC9D,MAAM,UAAU,iBAAiB,cAAc;CAC/C,OAAO,IAAI,CAAC,CAAC,MACX,QAAQ,UAAU,mBAAmB,OAAO,OAAO,GAAG,EACpD,SAAS,kBAAkB,QAAQ,KAAK,IAAI,IAC9C,CAAC,CACH;AACF;AAOA,SAAgB,4BACd,YACA,EAAE,kBACF;CACA,OAAO,OAAO;EACZ,UAAU,SAAS,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,GAAG,CAAC;EACzC,WAAWA,OAAM,UAAU;EAC3B,WAAW,SAAS,gBAAgB,cAAc,CAAC;EACnD,YAAY,SAAS,gBAAgB,cAAc,CAAC;CACtD,CAAC;AACH"}
@@ -0,0 +1,40 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/stripe/thin-handler.ts
3
+ /**
4
+ * Stripe thin event dispatcher keyed by event type (`v1.customer.subscription.updated`, ...). The
5
+ * caller verifies and parses the payload with `stripe.parseEventNotificationAsync` first and passes
6
+ * the notification in. A thin event only says which object changed, so each handler reads current
7
+ * state itself with `notification.fetchRelatedObject()` (or `fetchEvent()`). Nothing is pre-fetched
8
+ * here: some handlers only need `related_object.id`, and fetching inside the handler keeps the API
9
+ * call next to the code that uses its result. Delivery order carries no meaning: handlers re-read
10
+ * state instead of applying a payload, and dedupe one-off facts with idempotency keys.
11
+ *
12
+ * The thin-event counterpart of StripeEventHandler, which dispatches snapshot events. Thin event
13
+ * types and `parseEventNotificationAsync` need stripe-node 23+.
14
+ */
15
+ var StripeThinEventHandler = class StripeThinEventHandler {
16
+ handlers;
17
+ constructor() {
18
+ this.handlers = /* @__PURE__ */ new Map();
19
+ }
20
+ static create() {
21
+ return new StripeThinEventHandler();
22
+ }
23
+ on = (type, handler) => {
24
+ if (!this.handlers.has(type)) this.handlers.set(type, []);
25
+ this.handlers.get(type)?.push(handler);
26
+ return this;
27
+ };
28
+ process = async (notification) => {
29
+ const eventHandlers = this.handlers.get(notification.type);
30
+ if (eventHandlers) await Promise.all(eventHandlers.map((handler) => handler(notification)));
31
+ else {
32
+ console.error(`No stripe event handler found for event type: ${notification.type}`);
33
+ throw new Error(`No stripe event handler found for event type: ${notification.type}`);
34
+ }
35
+ };
36
+ };
37
+ //#endregion
38
+ exports.StripeThinEventHandler = StripeThinEventHandler;
39
+
40
+ //# sourceMappingURL=thin-handler.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thin-handler.cjs","names":[],"sources":["../../src/stripe/thin-handler.ts"],"sourcesContent":["import type { Stripe } from 'stripe';\n\ntype StripeEventNotification = Stripe.V2.Core.EventNotification;\n\nexport type StripeThinEventType = StripeEventNotification['type'];\n\nexport type StripeThinEventNotificationOf<T extends StripeThinEventType> = Extract<\n StripeEventNotification,\n { type: T }\n>;\n\ntype EventHandlerFn<T extends StripeThinEventType = StripeThinEventType> = (\n notification: StripeThinEventNotificationOf<T>\n) => Promise<void>;\n\n/**\n * Stripe thin event dispatcher keyed by event type (`v1.customer.subscription.updated`, ...). The\n * caller verifies and parses the payload with `stripe.parseEventNotificationAsync` first and passes\n * the notification in. A thin event only says which object changed, so each handler reads current\n * state itself with `notification.fetchRelatedObject()` (or `fetchEvent()`). Nothing is pre-fetched\n * here: some handlers only need `related_object.id`, and fetching inside the handler keeps the API\n * call next to the code that uses its result. Delivery order carries no meaning: handlers re-read\n * state instead of applying a payload, and dedupe one-off facts with idempotency keys.\n *\n * The thin-event counterpart of StripeEventHandler, which dispatches snapshot events. Thin event\n * types and `parseEventNotificationAsync` need stripe-node 23+.\n */\nexport class StripeThinEventHandler {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private readonly handlers: Map<StripeThinEventType, Array<EventHandlerFn<any>>>;\n\n private constructor() {\n this.handlers = new Map();\n }\n\n static create() {\n return new StripeThinEventHandler();\n }\n\n on = <T extends StripeThinEventType>(type: T, handler: EventHandlerFn<T>) => {\n if (!this.handlers.has(type)) {\n this.handlers.set(type, []);\n }\n\n this.handlers.get(type)?.push(handler);\n return this;\n };\n\n process = async (notification: StripeEventNotification): Promise<void> => {\n const eventHandlers = this.handlers.get(notification.type);\n if (eventHandlers) {\n await Promise.all(eventHandlers.map((handler) => handler(notification)));\n } else {\n console.error(`No stripe event handler found for event type: ${notification.type}`);\n throw new Error(`No stripe event handler found for event type: ${notification.type}`);\n }\n };\n}\n"],"mappings":";;;;;;;;;;;;;;AA2BA,IAAa,yBAAb,MAAa,uBAAuB;CAElC;CAEA,cAAsB;EACpB,KAAK,2BAAW,IAAI,IAAI;CAC1B;CAEA,OAAO,SAAS;EACd,OAAO,IAAI,uBAAuB;CACpC;CAEA,MAAqC,MAAS,YAA+B;EAC3E,IAAI,CAAC,KAAK,SAAS,IAAI,IAAI,GACzB,KAAK,SAAS,IAAI,MAAM,CAAC,CAAC;EAG5B,KAAK,SAAS,IAAI,IAAI,CAAC,EAAE,KAAK,OAAO;EACrC,OAAO;CACT;CAEA,UAAU,OAAO,iBAAyD;EACxE,MAAM,gBAAgB,KAAK,SAAS,IAAI,aAAa,IAAI;EACzD,IAAI,eACF,MAAM,QAAQ,IAAI,cAAc,KAAK,YAAY,QAAQ,YAAY,CAAC,CAAC;OAClE;GACL,QAAQ,MAAM,iDAAiD,aAAa,MAAM;GAClF,MAAM,IAAI,MAAM,iDAAiD,aAAa,MAAM;EACtF;CACF;AACF"}
@@ -0,0 +1,30 @@
1
+ import { Stripe } from "stripe";
2
+ //#region src/stripe/thin-handler.d.ts
3
+ type StripeEventNotification = Stripe.V2.Core.EventNotification;
4
+ export type StripeThinEventType = StripeEventNotification['type'];
5
+ export type StripeThinEventNotificationOf<T extends StripeThinEventType> = Extract<StripeEventNotification, {
6
+ type: T;
7
+ }>;
8
+ type EventHandlerFn<T extends StripeThinEventType = StripeThinEventType> = (notification: StripeThinEventNotificationOf<T>) => Promise<void>;
9
+ /**
10
+ * Stripe thin event dispatcher keyed by event type (`v1.customer.subscription.updated`, ...). The
11
+ * caller verifies and parses the payload with `stripe.parseEventNotificationAsync` first and passes
12
+ * the notification in. A thin event only says which object changed, so each handler reads current
13
+ * state itself with `notification.fetchRelatedObject()` (or `fetchEvent()`). Nothing is pre-fetched
14
+ * here: some handlers only need `related_object.id`, and fetching inside the handler keeps the API
15
+ * call next to the code that uses its result. Delivery order carries no meaning: handlers re-read
16
+ * state instead of applying a payload, and dedupe one-off facts with idempotency keys.
17
+ *
18
+ * The thin-event counterpart of StripeEventHandler, which dispatches snapshot events. Thin event
19
+ * types and `parseEventNotificationAsync` need stripe-node 23+.
20
+ */
21
+ export declare class StripeThinEventHandler {
22
+ private readonly handlers;
23
+ private constructor();
24
+ static create(): StripeThinEventHandler;
25
+ on: <T extends StripeThinEventType>(type: T, handler: EventHandlerFn<T>) => this;
26
+ process: (notification: StripeEventNotification) => Promise<void>;
27
+ }
28
+ //#endregion
29
+ export {};
30
+ //# sourceMappingURL=thin-handler.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thin-handler.d.cts","names":[],"sources":["../../src/stripe/thin-handler.ts"],"mappings":";;KAEK,0BAA0B,OAAO,GAAG,KAAK;YAElC,sBAAsB;YAEtB,8BAA8B,UAAU,uBAAuB,QACzE;EACE,MAAM;;KAGL,eAAe,UAAU,sBAAsB,wBAClD,cAAc,8BAA8B,OACzC;;;;;;;;;;;;;qBAcQ;mBAEM;UAEV;SAIA,UAAM;EAIb,KAAM,UAAU,qBAAmB,MAAQ,GAAC,SAAW,eAAe;EAStE,UAAO,cAAwB,4BAA0B"}
@@ -0,0 +1,30 @@
1
+ import { Stripe } from "stripe";
2
+ //#region src/stripe/thin-handler.d.ts
3
+ type StripeEventNotification = Stripe.V2.Core.EventNotification;
4
+ export type StripeThinEventType = StripeEventNotification['type'];
5
+ export type StripeThinEventNotificationOf<T extends StripeThinEventType> = Extract<StripeEventNotification, {
6
+ type: T;
7
+ }>;
8
+ type EventHandlerFn<T extends StripeThinEventType = StripeThinEventType> = (notification: StripeThinEventNotificationOf<T>) => Promise<void>;
9
+ /**
10
+ * Stripe thin event dispatcher keyed by event type (`v1.customer.subscription.updated`, ...). The
11
+ * caller verifies and parses the payload with `stripe.parseEventNotificationAsync` first and passes
12
+ * the notification in. A thin event only says which object changed, so each handler reads current
13
+ * state itself with `notification.fetchRelatedObject()` (or `fetchEvent()`). Nothing is pre-fetched
14
+ * here: some handlers only need `related_object.id`, and fetching inside the handler keeps the API
15
+ * call next to the code that uses its result. Delivery order carries no meaning: handlers re-read
16
+ * state instead of applying a payload, and dedupe one-off facts with idempotency keys.
17
+ *
18
+ * The thin-event counterpart of StripeEventHandler, which dispatches snapshot events. Thin event
19
+ * types and `parseEventNotificationAsync` need stripe-node 23+.
20
+ */
21
+ export declare class StripeThinEventHandler {
22
+ private readonly handlers;
23
+ private constructor();
24
+ static create(): StripeThinEventHandler;
25
+ on: <T extends StripeThinEventType>(type: T, handler: EventHandlerFn<T>) => this;
26
+ process: (notification: StripeEventNotification) => Promise<void>;
27
+ }
28
+ //#endregion
29
+ export {};
30
+ //# sourceMappingURL=thin-handler.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thin-handler.d.mts","names":[],"sources":["../../src/stripe/thin-handler.ts"],"mappings":";;KAEK,0BAA0B,OAAO,GAAG,KAAK;YAElC,sBAAsB;YAEtB,8BAA8B,UAAU,uBAAuB,QACzE;EACE,MAAM;;KAGL,eAAe,UAAU,sBAAsB,wBAClD,cAAc,8BAA8B,OACzC;;;;;;;;;;;;;qBAcQ;mBAEM;UAEV;SAIA,UAAM;EAIb,KAAM,UAAU,qBAAmB,MAAQ,GAAC,SAAW,eAAe;EAStE,UAAO,cAAwB,4BAA0B"}
@@ -0,0 +1,39 @@
1
+ //#region src/stripe/thin-handler.ts
2
+ /**
3
+ * Stripe thin event dispatcher keyed by event type (`v1.customer.subscription.updated`, ...). The
4
+ * caller verifies and parses the payload with `stripe.parseEventNotificationAsync` first and passes
5
+ * the notification in. A thin event only says which object changed, so each handler reads current
6
+ * state itself with `notification.fetchRelatedObject()` (or `fetchEvent()`). Nothing is pre-fetched
7
+ * here: some handlers only need `related_object.id`, and fetching inside the handler keeps the API
8
+ * call next to the code that uses its result. Delivery order carries no meaning: handlers re-read
9
+ * state instead of applying a payload, and dedupe one-off facts with idempotency keys.
10
+ *
11
+ * The thin-event counterpart of StripeEventHandler, which dispatches snapshot events. Thin event
12
+ * types and `parseEventNotificationAsync` need stripe-node 23+.
13
+ */
14
+ var StripeThinEventHandler = class StripeThinEventHandler {
15
+ handlers;
16
+ constructor() {
17
+ this.handlers = /* @__PURE__ */ new Map();
18
+ }
19
+ static create() {
20
+ return new StripeThinEventHandler();
21
+ }
22
+ on = (type, handler) => {
23
+ if (!this.handlers.has(type)) this.handlers.set(type, []);
24
+ this.handlers.get(type)?.push(handler);
25
+ return this;
26
+ };
27
+ process = async (notification) => {
28
+ const eventHandlers = this.handlers.get(notification.type);
29
+ if (eventHandlers) await Promise.all(eventHandlers.map((handler) => handler(notification)));
30
+ else {
31
+ console.error(`No stripe event handler found for event type: ${notification.type}`);
32
+ throw new Error(`No stripe event handler found for event type: ${notification.type}`);
33
+ }
34
+ };
35
+ };
36
+ //#endregion
37
+ export { StripeThinEventHandler };
38
+
39
+ //# sourceMappingURL=thin-handler.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thin-handler.mjs","names":[],"sources":["../../src/stripe/thin-handler.ts"],"sourcesContent":["import type { Stripe } from 'stripe';\n\ntype StripeEventNotification = Stripe.V2.Core.EventNotification;\n\nexport type StripeThinEventType = StripeEventNotification['type'];\n\nexport type StripeThinEventNotificationOf<T extends StripeThinEventType> = Extract<\n StripeEventNotification,\n { type: T }\n>;\n\ntype EventHandlerFn<T extends StripeThinEventType = StripeThinEventType> = (\n notification: StripeThinEventNotificationOf<T>\n) => Promise<void>;\n\n/**\n * Stripe thin event dispatcher keyed by event type (`v1.customer.subscription.updated`, ...). The\n * caller verifies and parses the payload with `stripe.parseEventNotificationAsync` first and passes\n * the notification in. A thin event only says which object changed, so each handler reads current\n * state itself with `notification.fetchRelatedObject()` (or `fetchEvent()`). Nothing is pre-fetched\n * here: some handlers only need `related_object.id`, and fetching inside the handler keeps the API\n * call next to the code that uses its result. Delivery order carries no meaning: handlers re-read\n * state instead of applying a payload, and dedupe one-off facts with idempotency keys.\n *\n * The thin-event counterpart of StripeEventHandler, which dispatches snapshot events. Thin event\n * types and `parseEventNotificationAsync` need stripe-node 23+.\n */\nexport class StripeThinEventHandler {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private readonly handlers: Map<StripeThinEventType, Array<EventHandlerFn<any>>>;\n\n private constructor() {\n this.handlers = new Map();\n }\n\n static create() {\n return new StripeThinEventHandler();\n }\n\n on = <T extends StripeThinEventType>(type: T, handler: EventHandlerFn<T>) => {\n if (!this.handlers.has(type)) {\n this.handlers.set(type, []);\n }\n\n this.handlers.get(type)?.push(handler);\n return this;\n };\n\n process = async (notification: StripeEventNotification): Promise<void> => {\n const eventHandlers = this.handlers.get(notification.type);\n if (eventHandlers) {\n await Promise.all(eventHandlers.map((handler) => handler(notification)));\n } else {\n console.error(`No stripe event handler found for event type: ${notification.type}`);\n throw new Error(`No stripe event handler found for event type: ${notification.type}`);\n }\n };\n}\n"],"mappings":";;;;;;;;;;;;;AA2BA,IAAa,yBAAb,MAAa,uBAAuB;CAElC;CAEA,cAAsB;EACpB,KAAK,2BAAW,IAAI,IAAI;CAC1B;CAEA,OAAO,SAAS;EACd,OAAO,IAAI,uBAAuB;CACpC;CAEA,MAAqC,MAAS,YAA+B;EAC3E,IAAI,CAAC,KAAK,SAAS,IAAI,IAAI,GACzB,KAAK,SAAS,IAAI,MAAM,CAAC,CAAC;EAG5B,KAAK,SAAS,IAAI,IAAI,CAAC,EAAE,KAAK,OAAO;EACrC,OAAO;CACT;CAEA,UAAU,OAAO,iBAAyD;EACxE,MAAM,gBAAgB,KAAK,SAAS,IAAI,aAAa,IAAI;EACzD,IAAI,eACF,MAAM,QAAQ,IAAI,cAAc,KAAK,YAAY,QAAQ,YAAY,CAAC,CAAC;OAClE;GACL,QAAQ,MAAM,iDAAiD,aAAa,MAAM;GAClF,MAAM,IAAI,MAAM,iDAAiD,aAAa,MAAM;EACtF;CACF;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shware/purchase",
3
- "version": "5.2.2",
3
+ "version": "7.0.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -70,7 +70,7 @@
70
70
  },
71
71
  "peerDependencies": {
72
72
  "@apple/app-store-server-library": "^3.1.0",
73
- "stripe": "^22.6.2 || ^23.0.0",
73
+ "stripe": "^23.0.0",
74
74
  "zod": "^4.6.5"
75
75
  },
76
76
  "peerDependenciesMeta": {