@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 +93 -26
- package/dist/stripe/index.cjs +3 -0
- package/dist/stripe/index.d.cts +3 -2
- package/dist/stripe/index.d.mts +3 -2
- package/dist/stripe/index.mjs +3 -2
- package/dist/stripe/schema.cjs +43 -3
- package/dist/stripe/schema.cjs.map +1 -1
- package/dist/stripe/schema.d.cts +14 -1
- package/dist/stripe/schema.d.cts.map +1 -1
- package/dist/stripe/schema.d.mts +14 -1
- package/dist/stripe/schema.d.mts.map +1 -1
- package/dist/stripe/schema.mjs +44 -5
- package/dist/stripe/schema.mjs.map +1 -1
- package/dist/stripe/thin-handler.cjs +40 -0
- package/dist/stripe/thin-handler.cjs.map +1 -0
- package/dist/stripe/thin-handler.d.cts +30 -0
- package/dist/stripe/thin-handler.d.cts.map +1 -0
- package/dist/stripe/thin-handler.d.mts +30 -0
- package/dist/stripe/thin-handler.d.mts.map +1 -0
- package/dist/stripe/thin-handler.mjs +39 -0
- package/dist/stripe/thin-handler.mjs.map +1 -0
- package/package.json +2 -2
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 (^
|
|
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
|
-
|
|
135
|
-
per 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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
.
|
|
163
|
-
.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
package/dist/stripe/index.cjs
CHANGED
|
@@ -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;
|
package/dist/stripe/index.d.cts
CHANGED
|
@@ -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 {
|
|
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 };
|
package/dist/stripe/index.d.mts
CHANGED
|
@@ -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 {
|
|
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 };
|
package/dist/stripe/index.mjs
CHANGED
|
@@ -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 {
|
|
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 };
|
package/dist/stripe/schema.cjs
CHANGED
|
@@ -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
|
-
|
|
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)((
|
|
16
|
-
successUrl: (0, zod_mini.optional)((
|
|
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"
|
|
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"}
|
package/dist/stripe/schema.d.cts
CHANGED
|
@@ -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
|
-
|
|
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":";;;
|
|
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"}
|
package/dist/stripe/schema.d.mts
CHANGED
|
@@ -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
|
-
|
|
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":";;;
|
|
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"}
|
package/dist/stripe/schema.mjs
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
15
|
-
successUrl: optional(
|
|
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(
|
|
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": "
|
|
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": "^
|
|
73
|
+
"stripe": "^23.0.0",
|
|
74
74
|
"zod": "^4.6.5"
|
|
75
75
|
},
|
|
76
76
|
"peerDependenciesMeta": {
|