@fantastic.dev/better-stripe 0.10.0 → 0.10.1
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/CHANGELOG.md +6 -0
- package/MIGRATION.md +14 -0
- package/README.md +10 -1
- package/dist/client/apiVersion.d.ts +6 -0
- package/dist/client/apiVersion.d.ts.map +1 -0
- package/dist/client/apiVersion.js +13 -0
- package/dist/client/apiVersion.js.map +1 -0
- package/dist/client/index.d.ts +8 -12
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +18 -4
- package/dist/client/index.js.map +1 -1
- package/dist/client/previewRoutes.d.ts +9 -0
- package/dist/client/previewRoutes.d.ts.map +1 -0
- package/dist/client/previewRoutes.js +15 -0
- package/dist/client/previewRoutes.js.map +1 -0
- package/dist/client/types/options.d.ts +3 -0
- package/dist/client/types/options.d.ts.map +1 -1
- package/dist/client/utils/webhookEndpoints.d.ts +12 -8
- package/dist/client/utils/webhookEndpoints.d.ts.map +1 -1
- package/dist/client/utils/webhookEndpoints.js +41 -2
- package/dist/client/utils/webhookEndpoints.js.map +1 -1
- package/dist/client/webhooks/v2.d.ts.map +1 -1
- package/dist/client/webhooks/v2.js +10 -7
- package/dist/client/webhooks/v2.js.map +1 -1
- package/dist/component/lib/stripeFactory.d.ts +3 -2
- package/dist/component/lib/stripeFactory.d.ts.map +1 -1
- package/dist/component/lib/stripeFactory.js +6 -5
- package/dist/component/lib/stripeFactory.js.map +1 -1
- package/docs/accounts-v2-preview.md +132 -0
- package/package.json +2 -1
- package/src/client/apiVersion.ts +20 -0
- package/src/client/customer-preview-thin.test.ts +41 -0
- package/src/client/customer-preview-webhooks.test.ts +150 -0
- package/src/client/customer-preview.test.ts +185 -0
- package/src/client/index.ts +31 -16
- package/src/client/previewRoutes.ts +26 -0
- package/src/client/stripe-version.test.ts +10 -4
- package/src/client/types/options.ts +3 -0
- package/src/client/utils/webhookEndpoints.ts +70 -15
- package/src/client/webhooks/v2.ts +14 -10
- package/src/component/lib/stripeFactory.ts +13 -5
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import Stripe from "stripe";
|
|
2
|
+
import { componentsGeneric } from "convex/server";
|
|
3
|
+
import { expect, it, vi } from "vitest";
|
|
4
|
+
import { BetterStripe, type BetterStripeOptions } from "./index.js";
|
|
5
|
+
import { getStripeClient, type Component, type RunCtx } from "./helpers.js";
|
|
6
|
+
|
|
7
|
+
const preview = "2026-09-30.preview";
|
|
8
|
+
const component = componentsGeneric().betterStripe as unknown as Component;
|
|
9
|
+
const ctx = {
|
|
10
|
+
runQuery: vi.fn().mockImplementation(async (_ref, args) =>
|
|
11
|
+
args?.stripeAccountId
|
|
12
|
+
? {
|
|
13
|
+
_id: "row",
|
|
14
|
+
userId: "buyer",
|
|
15
|
+
stripeAccountId: "acct_buyer",
|
|
16
|
+
appliedConfigurations: [],
|
|
17
|
+
}
|
|
18
|
+
: null,
|
|
19
|
+
),
|
|
20
|
+
runMutation: vi.fn().mockResolvedValue("row"),
|
|
21
|
+
} as unknown as RunCtx;
|
|
22
|
+
function fixture(version: string | undefined) {
|
|
23
|
+
const key = `sk_test_preview_transport_${crypto.randomUUID()}`;
|
|
24
|
+
const bs = new BetterStripe(component, {
|
|
25
|
+
STRIPE_SECRET_KEY: key,
|
|
26
|
+
...(version === undefined ? {} : { stripeApiVersion: version }),
|
|
27
|
+
} as BetterStripeOptions);
|
|
28
|
+
const calls: {
|
|
29
|
+
path: string;
|
|
30
|
+
version: string;
|
|
31
|
+
idempotency: string | undefined;
|
|
32
|
+
body: string;
|
|
33
|
+
}[] = [];
|
|
34
|
+
for (const selected of new Set([version, "2026-09-30.endive"])) {
|
|
35
|
+
const sdk = getStripeClient(key, selected);
|
|
36
|
+
(
|
|
37
|
+
sdk as unknown as { _setApiField: (name: string, value: unknown) => void }
|
|
38
|
+
)._setApiField("httpClient", {
|
|
39
|
+
getClientName: () => "preview-contract-test",
|
|
40
|
+
makeRequest: async (
|
|
41
|
+
...[, , path, , headers, body]: Parameters<
|
|
42
|
+
ReturnType<typeof Stripe.createNodeHttpClient>["makeRequest"]
|
|
43
|
+
>
|
|
44
|
+
) => {
|
|
45
|
+
calls.push({
|
|
46
|
+
path,
|
|
47
|
+
version: String(headers["Stripe-Version"]),
|
|
48
|
+
idempotency: headers["Idempotency-Key"] as string | undefined,
|
|
49
|
+
body: String(body),
|
|
50
|
+
});
|
|
51
|
+
const data = path.includes("/checkout/sessions")
|
|
52
|
+
? {
|
|
53
|
+
id: "cs_preview",
|
|
54
|
+
object: "checkout.session",
|
|
55
|
+
client_secret: "test_secret",
|
|
56
|
+
status: "open",
|
|
57
|
+
}
|
|
58
|
+
: path.includes("/v2/core/accounts")
|
|
59
|
+
? {
|
|
60
|
+
id: "acct_buyer",
|
|
61
|
+
object: "v2.core.account",
|
|
62
|
+
applied_configurations: ["customer"],
|
|
63
|
+
configuration: { customer: {} },
|
|
64
|
+
metadata: { userId: "buyer" },
|
|
65
|
+
}
|
|
66
|
+
: path.includes("/prices/")
|
|
67
|
+
? {
|
|
68
|
+
id: "price_preview",
|
|
69
|
+
active: true,
|
|
70
|
+
unit_amount: 1000,
|
|
71
|
+
currency: "usd",
|
|
72
|
+
type: "one_time",
|
|
73
|
+
product: "prod_preview",
|
|
74
|
+
}
|
|
75
|
+
: path.includes("/products/")
|
|
76
|
+
? { id: "prod_preview", active: true }
|
|
77
|
+
: path.includes("/billing_portal/sessions")
|
|
78
|
+
? {
|
|
79
|
+
id: "bps_preview",
|
|
80
|
+
url: "https://billing.stripe.com/test",
|
|
81
|
+
}
|
|
82
|
+
: path.includes("/refunds")
|
|
83
|
+
? { id: "re_preview", status: "succeeded" }
|
|
84
|
+
: path.includes("/payment_intents/")
|
|
85
|
+
? {
|
|
86
|
+
id: "pi_preview",
|
|
87
|
+
latest_charge: {
|
|
88
|
+
id: "ch_preview",
|
|
89
|
+
amount: 1000,
|
|
90
|
+
currency: "usd",
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
: path.includes("/charges/")
|
|
94
|
+
? { id: "ch_preview", amount: 1000, currency: "usd" }
|
|
95
|
+
: path.includes("/coupons/")
|
|
96
|
+
? { id: "coupon_preview", valid: true }
|
|
97
|
+
: {
|
|
98
|
+
id: "sub_preview",
|
|
99
|
+
status: "active",
|
|
100
|
+
customer_account: "acct_buyer",
|
|
101
|
+
items: { data: [] },
|
|
102
|
+
metadata: {},
|
|
103
|
+
};
|
|
104
|
+
return {
|
|
105
|
+
getStatusCode: () => 200,
|
|
106
|
+
getHeaders: () => ({}),
|
|
107
|
+
getRawResponse: () => ({}),
|
|
108
|
+
toJSON: async () => data,
|
|
109
|
+
toStream: () => {
|
|
110
|
+
throw new Error("Unexpected stream");
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return { bs, calls };
|
|
117
|
+
}
|
|
118
|
+
it.each([undefined, preview])(
|
|
119
|
+
"uses one selected version for account, Checkout, billing, refunds and authoritative reads (%s)",
|
|
120
|
+
async (version) => {
|
|
121
|
+
const { bs, calls } = fixture(version);
|
|
122
|
+
await bs.ensureCustomerAccount(ctx, {
|
|
123
|
+
userId: "buyer",
|
|
124
|
+
email: "buyer@example.com",
|
|
125
|
+
idempotencyKey: "account:buyer",
|
|
126
|
+
});
|
|
127
|
+
await bs.createCheckoutSession(ctx, {
|
|
128
|
+
userId: "buyer",
|
|
129
|
+
accountId: "acct_buyer",
|
|
130
|
+
stripePriceId: "price_preview",
|
|
131
|
+
mode: "payment",
|
|
132
|
+
returnUrl: "https://example.com/receipt",
|
|
133
|
+
idempotencyKey: "order:one",
|
|
134
|
+
});
|
|
135
|
+
await bs.getCheckoutSessionFromStripe(ctx, {
|
|
136
|
+
stripeSessionId: "cs_preview",
|
|
137
|
+
});
|
|
138
|
+
await bs.getSubscriptionFromStripe(ctx, {
|
|
139
|
+
stripeSubscriptionId: "sub_preview",
|
|
140
|
+
});
|
|
141
|
+
await bs.getCouponFromStripe(ctx, { stripeCouponId: "coupon_preview" });
|
|
142
|
+
await bs.cancelSubscription(ctx, { stripeSubscriptionId: "sub_preview" });
|
|
143
|
+
await bs.reactivateSubscription(ctx, {
|
|
144
|
+
stripeSubscriptionId: "sub_preview",
|
|
145
|
+
});
|
|
146
|
+
await bs.createBillingPortalSession(ctx, {
|
|
147
|
+
stripeAccountId: "acct_buyer",
|
|
148
|
+
returnUrl: "https://example.com/billing",
|
|
149
|
+
});
|
|
150
|
+
await bs.createRefund(ctx, {
|
|
151
|
+
stripeChargeId: "ch_preview",
|
|
152
|
+
idempotencyKey: "refund:one",
|
|
153
|
+
});
|
|
154
|
+
expect(calls.length).toBeGreaterThanOrEqual(8);
|
|
155
|
+
expect(new Set(calls.map((c) => c.version))).toEqual(
|
|
156
|
+
new Set([version ?? "2026-09-30.endive"]),
|
|
157
|
+
);
|
|
158
|
+
expect(calls.find((c) => c.path === "/v2/core/accounts")?.idempotency).toBe(
|
|
159
|
+
"account:buyer",
|
|
160
|
+
);
|
|
161
|
+
expect(
|
|
162
|
+
calls.find((c) => c.path === "/v1/checkout/sessions" && c.idempotency)
|
|
163
|
+
?.idempotency,
|
|
164
|
+
).toBe("order:one");
|
|
165
|
+
expect(
|
|
166
|
+
calls.find((c) => c.path === "/v1/checkout/sessions" && c.idempotency)
|
|
167
|
+
?.body,
|
|
168
|
+
).toContain("customer_account=acct_buyer");
|
|
169
|
+
expect(
|
|
170
|
+
calls.find((c) => c.path === "/v2/core/accounts")?.body,
|
|
171
|
+
).not.toContain("merchant");
|
|
172
|
+
expect(bs.stripeApiVersion).toBe(version ?? "2026-09-30.endive");
|
|
173
|
+
},
|
|
174
|
+
);
|
|
175
|
+
it.each(["", "2020-08-27", "2026-09-30.preview; unsafe", "not-a-version"])(
|
|
176
|
+
"rejects unsupported client versions clearly (%s)",
|
|
177
|
+
(version) => {
|
|
178
|
+
expect(
|
|
179
|
+
() =>
|
|
180
|
+
new BetterStripe(component, {
|
|
181
|
+
stripeApiVersion: version,
|
|
182
|
+
} as unknown as BetterStripeOptions),
|
|
183
|
+
).toThrow("Unsupported Stripe API version");
|
|
184
|
+
},
|
|
185
|
+
);
|
package/src/client/index.ts
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
import type { HttpRouter } from "convex/server";
|
|
2
|
+
import { registerClientRoutes } from "./previewRoutes.js";
|
|
3
|
+
import {
|
|
4
|
+
resolveStripeApiVersion,
|
|
5
|
+
type SupportedStripeApiVersion,
|
|
6
|
+
} from "./apiVersion.js";
|
|
7
|
+
export {
|
|
8
|
+
STRIPE_CUSTOMER_PREVIEW_API_VERSION,
|
|
9
|
+
type SupportedStripeApiVersion,
|
|
10
|
+
} from "./apiVersion.js";
|
|
1
11
|
export type {
|
|
2
12
|
CheckoutSessionReadOptions,
|
|
3
13
|
SubscriptionReadOptions,
|
|
@@ -184,6 +194,7 @@ function clonePlatformFee(config: PlatformFeeConfig): PlatformFeeConfig {
|
|
|
184
194
|
|
|
185
195
|
export class BetterStripe {
|
|
186
196
|
public component: Component;
|
|
197
|
+
readonly stripeApiVersion: SupportedStripeApiVersion;
|
|
187
198
|
private _apiKey?: string;
|
|
188
199
|
private _triggers?: SyncTriggers;
|
|
189
200
|
private _hooks?: AsyncHooks;
|
|
@@ -192,6 +203,7 @@ export class BetterStripe {
|
|
|
192
203
|
|
|
193
204
|
constructor(component: BetterStripeComponent, options?: BetterStripeOptions) {
|
|
194
205
|
this.component = component;
|
|
206
|
+
this.stripeApiVersion = resolveStripeApiVersion(options?.stripeApiVersion);
|
|
195
207
|
this._apiKey = options?.STRIPE_SECRET_KEY;
|
|
196
208
|
this._triggers = options?.triggers;
|
|
197
209
|
this._hooks = options?.hooks;
|
|
@@ -240,8 +252,15 @@ export class BetterStripe {
|
|
|
240
252
|
return key;
|
|
241
253
|
}
|
|
242
254
|
|
|
243
|
-
|
|
244
|
-
|
|
255
|
+
registerRoutes(
|
|
256
|
+
http: HttpRouter,
|
|
257
|
+
config?: Parameters<typeof registerClientRoutes>[2],
|
|
258
|
+
) {
|
|
259
|
+
return registerClientRoutes(this, http, config);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private stripe(): Stripe {
|
|
263
|
+
return getStripeClient(this.apiKey, this.stripeApiVersion);
|
|
245
264
|
}
|
|
246
265
|
|
|
247
266
|
// Static defaults (preserved for backwards compatibility)
|
|
@@ -1427,13 +1446,12 @@ export class BetterStripe {
|
|
|
1427
1446
|
|
|
1428
1447
|
async createWebhookEndpoint(
|
|
1429
1448
|
ctx: RunCtx,
|
|
1430
|
-
opts:
|
|
1431
|
-
url: string;
|
|
1432
|
-
description?: string;
|
|
1433
|
-
enabledEvents?: string[];
|
|
1434
|
-
},
|
|
1449
|
+
opts: webhookEndpointsImpl.WebhookEndpointOptions,
|
|
1435
1450
|
): Promise<{ id: string; secret: string; url: string }> {
|
|
1436
|
-
return webhookEndpointsImpl.createWebhookEndpoint(this.stripe(), ctx,
|
|
1451
|
+
return webhookEndpointsImpl.createWebhookEndpoint(this.stripe(), ctx, {
|
|
1452
|
+
...opts,
|
|
1453
|
+
apiVersion: this.stripeApiVersion,
|
|
1454
|
+
});
|
|
1437
1455
|
}
|
|
1438
1456
|
|
|
1439
1457
|
async listEventDestinations(ctx: RunCtx, opts?: { limit?: number }) {
|
|
@@ -1442,13 +1460,7 @@ export class BetterStripe {
|
|
|
1442
1460
|
|
|
1443
1461
|
async setupEventDestination(
|
|
1444
1462
|
ctx: RunCtx,
|
|
1445
|
-
opts:
|
|
1446
|
-
url: string;
|
|
1447
|
-
name?: string;
|
|
1448
|
-
description?: string;
|
|
1449
|
-
enabledEvents?: string[];
|
|
1450
|
-
eventPayload?: "snapshot" | "thin";
|
|
1451
|
-
},
|
|
1463
|
+
opts: webhookEndpointsImpl.EventDestinationOptions,
|
|
1452
1464
|
): Promise<{
|
|
1453
1465
|
id: string;
|
|
1454
1466
|
secret: string;
|
|
@@ -1456,7 +1468,10 @@ export class BetterStripe {
|
|
|
1456
1468
|
enabledEvents: string[];
|
|
1457
1469
|
created: boolean;
|
|
1458
1470
|
}> {
|
|
1459
|
-
return webhookEndpointsImpl.setupEventDestination(this.stripe(), ctx,
|
|
1471
|
+
return webhookEndpointsImpl.setupEventDestination(this.stripe(), ctx, {
|
|
1472
|
+
...opts,
|
|
1473
|
+
apiVersion: this.stripeApiVersion,
|
|
1474
|
+
});
|
|
1460
1475
|
}
|
|
1461
1476
|
|
|
1462
1477
|
// WEBHOOK EVENT QUERIES
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { HttpRouter } from "convex/server";
|
|
2
|
+
import type { Component } from "./helpers.js";
|
|
3
|
+
import type { RegisterRoutesConfig } from "./types.js";
|
|
4
|
+
import { registerRoutes } from "./webhooks/index.js";
|
|
5
|
+
import { throwStripeError } from "./errors.js";
|
|
6
|
+
export function registerClientRoutes(
|
|
7
|
+
client: { component: Component; apiKey: string; stripeApiVersion: string },
|
|
8
|
+
http: HttpRouter,
|
|
9
|
+
config?: RegisterRoutesConfig,
|
|
10
|
+
) {
|
|
11
|
+
if (
|
|
12
|
+
(config?.stripeApiVersion !== undefined &&
|
|
13
|
+
config.stripeApiVersion !== client.stripeApiVersion) ||
|
|
14
|
+
(config?.stripeSecretKey !== undefined &&
|
|
15
|
+
config.stripeSecretKey !== client.apiKey)
|
|
16
|
+
)
|
|
17
|
+
throwStripeError(
|
|
18
|
+
"INVALID_CONFIGURATION",
|
|
19
|
+
"Webhook API version and secret key must match the BetterStripe client.",
|
|
20
|
+
);
|
|
21
|
+
registerRoutes(http, client.component, {
|
|
22
|
+
...config,
|
|
23
|
+
stripeApiVersion: client.stripeApiVersion,
|
|
24
|
+
stripeSecretKey: client.apiKey,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -11,10 +11,16 @@ describe("Stripe version pin", () => {
|
|
|
11
11
|
expect(Stripe.API_VERSION).toBe(STRIPE_API_VERSION);
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
it.each(
|
|
14
|
+
it.each(
|
|
15
|
+
[getStripeClient, getStripe].flatMap((factory) =>
|
|
16
|
+
[undefined, "2026-09-30.preview" as const].map(
|
|
17
|
+
(version) => [factory, version] as const,
|
|
18
|
+
),
|
|
19
|
+
),
|
|
20
|
+
)(
|
|
15
21
|
"pins outgoing API requests from each factory",
|
|
16
|
-
async (factory) => {
|
|
17
|
-
const stripe = factory("sk_test_version_transport_only");
|
|
22
|
+
async (factory, selectedVersion) => {
|
|
23
|
+
const stripe = factory("sk_test_version_transport_only", selectedVersion);
|
|
18
24
|
let version: string | undefined;
|
|
19
25
|
// Factories deliberately expose no transport override. Replace only the
|
|
20
26
|
// SDK's transport slot, leaving its request serializer/headers untouched.
|
|
@@ -41,7 +47,7 @@ describe("Stripe version pin", () => {
|
|
|
41
47
|
},
|
|
42
48
|
});
|
|
43
49
|
await stripe.products.retrieve("prod_local");
|
|
44
|
-
expect(version).toBe(STRIPE_API_VERSION);
|
|
50
|
+
expect(version).toBe(selectedVersion ?? STRIPE_API_VERSION);
|
|
45
51
|
},
|
|
46
52
|
);
|
|
47
53
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SupportedStripeApiVersion } from "../apiVersion.js";
|
|
1
2
|
import type {
|
|
2
3
|
StripeEventHandler,
|
|
3
4
|
StripeEventHandlers,
|
|
@@ -51,6 +52,8 @@ export type FeeOverride = PlatformFeeConfig;
|
|
|
51
52
|
// ---------------------------------------------------------------------------
|
|
52
53
|
|
|
53
54
|
export type BetterStripeOptions = {
|
|
55
|
+
/** Defaults to GA. Non-Connect Accounts v2 customers require explicit preview opt-in. */
|
|
56
|
+
stripeApiVersion?: SupportedStripeApiVersion;
|
|
54
57
|
STRIPE_SECRET_KEY?: string;
|
|
55
58
|
/** Sync triggers — run in the same transaction as component DB writes */
|
|
56
59
|
triggers?: SyncTriggers;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { STRIPE_CUSTOMER_PREVIEW_API_VERSION } from "../apiVersion.js";
|
|
2
|
+
import { throwStripeError } from "../errors.js";
|
|
1
3
|
import type Stripe from "stripe";
|
|
2
4
|
|
|
3
5
|
import type { RunCtx } from "../helpers.js";
|
|
@@ -73,6 +75,13 @@ export const BETTER_STRIPE_V2_WEBHOOK_EVENTS = [
|
|
|
73
75
|
"v2.core.account_link.returned",
|
|
74
76
|
] as const;
|
|
75
77
|
|
|
78
|
+
/** Minimal customer lifecycle subscription for non-Connect preview merchants. */
|
|
79
|
+
export const BETTER_STRIPE_CUSTOMER_ACCOUNT_EVENTS = [
|
|
80
|
+
"v2.core.account.created",
|
|
81
|
+
"v2.core.account[configuration.customer].updated",
|
|
82
|
+
"v2.core.account[identity].updated",
|
|
83
|
+
] as const;
|
|
84
|
+
|
|
76
85
|
export type BetterStripeV2WebhookEvent =
|
|
77
86
|
(typeof BETTER_STRIPE_V2_WEBHOOK_EVENTS)[number];
|
|
78
87
|
|
|
@@ -103,14 +112,19 @@ export async function listWebhookEndpoints(
|
|
|
103
112
|
}));
|
|
104
113
|
}
|
|
105
114
|
|
|
115
|
+
export type WebhookEndpointOptions = {
|
|
116
|
+
url: string;
|
|
117
|
+
description?: string;
|
|
118
|
+
enabledEvents?: string[];
|
|
119
|
+
};
|
|
120
|
+
export type EventDestinationOptions = WebhookEndpointOptions & {
|
|
121
|
+
name?: string;
|
|
122
|
+
eventPayload?: "snapshot" | "thin";
|
|
123
|
+
};
|
|
106
124
|
export async function createWebhookEndpoint(
|
|
107
125
|
stripe: Stripe,
|
|
108
126
|
_ctx: RunCtx,
|
|
109
|
-
opts: {
|
|
110
|
-
url: string;
|
|
111
|
-
description?: string;
|
|
112
|
-
enabledEvents?: string[];
|
|
113
|
-
},
|
|
127
|
+
opts: WebhookEndpointOptions & { apiVersion?: string },
|
|
114
128
|
): Promise<{ id: string; secret: string; url: string }> {
|
|
115
129
|
// Remove existing endpoint with same URL (secret is only available on create)
|
|
116
130
|
const existing = await stripe.webhookEndpoints.list({ limit: 100 });
|
|
@@ -121,6 +135,12 @@ export async function createWebhookEndpoint(
|
|
|
121
135
|
|
|
122
136
|
const endpoint = await stripe.webhookEndpoints.create({
|
|
123
137
|
url: opts.url,
|
|
138
|
+
...(opts.apiVersion
|
|
139
|
+
? {
|
|
140
|
+
api_version:
|
|
141
|
+
opts.apiVersion as Stripe.WebhookEndpointCreateParams.ApiVersion,
|
|
142
|
+
}
|
|
143
|
+
: {}),
|
|
124
144
|
enabled_events: (opts.enabledEvents ??
|
|
125
145
|
BETTER_STRIPE_WEBHOOK_EVENTS) as Stripe.WebhookEndpointCreateParams.EnabledEvent[],
|
|
126
146
|
description:
|
|
@@ -189,14 +209,7 @@ export async function listEventDestinations(
|
|
|
189
209
|
export async function setupEventDestination(
|
|
190
210
|
stripe: Stripe,
|
|
191
211
|
_ctx: RunCtx,
|
|
192
|
-
opts: {
|
|
193
|
-
url: string;
|
|
194
|
-
name?: string;
|
|
195
|
-
description?: string;
|
|
196
|
-
enabledEvents?: string[];
|
|
197
|
-
/** 'snapshot' for V1 events, 'thin' for V2 events. Defaults to 'thin'. */
|
|
198
|
-
eventPayload?: "snapshot" | "thin";
|
|
199
|
-
},
|
|
212
|
+
opts: EventDestinationOptions & { apiVersion?: string },
|
|
200
213
|
): Promise<{
|
|
201
214
|
id: string;
|
|
202
215
|
secret: string;
|
|
@@ -208,7 +221,9 @@ export async function setupEventDestination(
|
|
|
208
221
|
const events =
|
|
209
222
|
opts.enabledEvents ??
|
|
210
223
|
(payloadType === "thin"
|
|
211
|
-
?
|
|
224
|
+
? opts.apiVersion === STRIPE_CUSTOMER_PREVIEW_API_VERSION
|
|
225
|
+
? [...BETTER_STRIPE_CUSTOMER_ACCOUNT_EVENTS]
|
|
226
|
+
: [...BETTER_STRIPE_V2_WEBHOOK_EVENTS]
|
|
212
227
|
: [...BETTER_STRIPE_WEBHOOK_EVENTS]);
|
|
213
228
|
const name =
|
|
214
229
|
opts.name ??
|
|
@@ -228,6 +243,36 @@ export async function setupEventDestination(
|
|
|
228
243
|
d.webhook_endpoint?.url === opts.url && d.event_payload === payloadType,
|
|
229
244
|
);
|
|
230
245
|
|
|
246
|
+
if (
|
|
247
|
+
match &&
|
|
248
|
+
payloadType === "snapshot" &&
|
|
249
|
+
opts.apiVersion &&
|
|
250
|
+
match.snapshot_api_version !== opts.apiVersion
|
|
251
|
+
)
|
|
252
|
+
throwStripeError(
|
|
253
|
+
"INVALID_CONFIGURATION",
|
|
254
|
+
"Existing snapshot destination API version differs. Create a replacement destination with the selected version and configure its signing secret before retiring the old destination.",
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
if (match && opts.apiVersion === STRIPE_CUSTOMER_PREVIEW_API_VERSION) {
|
|
258
|
+
const scopes = new Set(
|
|
259
|
+
(match.events_from ?? []).map((scope) =>
|
|
260
|
+
scope === "self"
|
|
261
|
+
? "@self"
|
|
262
|
+
: scope === "other_accounts"
|
|
263
|
+
? "@accounts"
|
|
264
|
+
: scope,
|
|
265
|
+
),
|
|
266
|
+
);
|
|
267
|
+
const required =
|
|
268
|
+
payloadType === "thin" ? ["@self", "@accounts"] : ["@self"];
|
|
269
|
+
if (required.some((scope) => !scopes.has(scope)))
|
|
270
|
+
throwStripeError(
|
|
271
|
+
"INVALID_CONFIGURATION",
|
|
272
|
+
"Existing destination event scope differs. Create a replacement with the customer preview scopes before retiring the old destination.",
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
231
276
|
if (match) {
|
|
232
277
|
const updated = await stripe.v2.core.eventDestinations.update(match.id, {
|
|
233
278
|
name,
|
|
@@ -256,7 +301,17 @@ export async function setupEventDestination(
|
|
|
256
301
|
type: "webhook_endpoint",
|
|
257
302
|
event_payload: payloadType,
|
|
258
303
|
enabled_events: events,
|
|
259
|
-
|
|
304
|
+
...(payloadType === "snapshot" && opts.apiVersion
|
|
305
|
+
? { snapshot_api_version: opts.apiVersion }
|
|
306
|
+
: {}),
|
|
307
|
+
events_from:
|
|
308
|
+
opts.apiVersion === STRIPE_CUSTOMER_PREVIEW_API_VERSION
|
|
309
|
+
? payloadType === "thin"
|
|
310
|
+
? ["@self", "@accounts"]
|
|
311
|
+
: ["@self"]
|
|
312
|
+
: payloadType === "thin"
|
|
313
|
+
? ["self", "other_accounts"]
|
|
314
|
+
: ["self"],
|
|
260
315
|
webhook_endpoint: { url: opts.url },
|
|
261
316
|
include: ["webhook_endpoint.signing_secret", "webhook_endpoint.url"],
|
|
262
317
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { STRIPE_CUSTOMER_PREVIEW_API_VERSION } from "../apiVersion.js";
|
|
1
2
|
import type Stripe from "stripe";
|
|
2
3
|
|
|
3
4
|
import { deriveAccountStatus } from "../core/accountStatus.js";
|
|
@@ -74,16 +75,19 @@ export async function handleV2Event(
|
|
|
74
75
|
return null;
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
const retrieveParams
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
78
|
+
const retrieveParams: NonNullable<
|
|
79
|
+
Parameters<Stripe["v2"]["core"]["accounts"]["retrieve"]>[1]
|
|
80
|
+
> = {
|
|
81
|
+
include:
|
|
82
|
+
whCtx.config?.stripeApiVersion === STRIPE_CUSTOMER_PREVIEW_API_VERSION
|
|
83
|
+
? ["configuration.customer", "identity"]
|
|
84
|
+
: [
|
|
85
|
+
"configuration.merchant",
|
|
86
|
+
"configuration.recipient",
|
|
87
|
+
"configuration.customer",
|
|
88
|
+
"identity",
|
|
89
|
+
"requirements",
|
|
90
|
+
],
|
|
87
91
|
};
|
|
88
92
|
// Preserve the verified notification's account/organization context when
|
|
89
93
|
// fetching the latest resource; otherwise organization keys can fetch from
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import Stripe from "stripe";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
resolveStripeApiVersion,
|
|
5
|
+
type SupportedStripeApiVersion,
|
|
6
|
+
} from "../../client/apiVersion.js";
|
|
7
|
+
import type { StripeApiVersion } from "../../client/stripe-types.js";
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
10
|
* Keyed cache for Stripe SDK instances.
|
|
@@ -14,14 +18,18 @@ const _clients = new Map<string, Stripe>();
|
|
|
14
18
|
|
|
15
19
|
/**
|
|
16
20
|
* Get or create a Stripe SDK instance for the given API key.
|
|
17
|
-
*
|
|
21
|
+
* Defaults to GA; an explicitly selected supported preview gets its own cache entry.
|
|
18
22
|
*/
|
|
19
|
-
export function getStripe(
|
|
20
|
-
|
|
23
|
+
export function getStripe(
|
|
24
|
+
apiKey: string,
|
|
25
|
+
apiVersion?: SupportedStripeApiVersion,
|
|
26
|
+
): Stripe {
|
|
27
|
+
const version = resolveStripeApiVersion(apiVersion);
|
|
28
|
+
const cacheKey = `${apiKey}:${version}`;
|
|
21
29
|
let client = _clients.get(cacheKey);
|
|
22
30
|
if (!client) {
|
|
23
31
|
client = new Stripe(apiKey, {
|
|
24
|
-
apiVersion:
|
|
32
|
+
apiVersion: version as StripeApiVersion,
|
|
25
33
|
typescript: true,
|
|
26
34
|
});
|
|
27
35
|
_clients.set(cacheKey, client);
|