@better-auth/stripe 1.5.0-beta.8 → 1.5.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 +17 -0
- package/dist/client.d.mts +52 -171
- package/dist/client.mjs +3 -2
- package/dist/client.mjs.map +1 -0
- package/dist/{error-codes-Clj-xYDP.mjs → error-codes-CCosYkXx.mjs} +4 -1
- package/dist/error-codes-CCosYkXx.mjs.map +1 -0
- package/dist/index.d.mts +519 -2
- package/dist/index.mjs +586 -232
- package/dist/index.mjs.map +1 -0
- package/dist/types-OT6L84x4.d.mts +480 -0
- package/package.json +28 -22
- package/.turbo/turbo-build.log +0 -17
- package/CHANGELOG.md +0 -24
- package/dist/index-BqGWQFAv.d.mts +0 -1015
- package/src/client.ts +0 -38
- package/src/error-codes.ts +0 -30
- package/src/hooks.ts +0 -435
- package/src/index.ts +0 -314
- package/src/middleware.ts +0 -104
- package/src/routes.ts +0 -1681
- package/src/schema.ts +0 -128
- package/src/types.ts +0 -449
- package/src/utils.ts +0 -70
- package/test/stripe-organization.test.ts +0 -1993
- package/test/stripe.test.ts +0 -4807
- package/tsconfig.json +0 -14
- package/tsdown.config.ts +0 -8
- package/vitest.config.ts +0 -8
package/src/index.ts
DELETED
|
@@ -1,314 +0,0 @@
|
|
|
1
|
-
import type { BetterAuthPlugin, User } from "better-auth";
|
|
2
|
-
import { APIError } from "better-auth";
|
|
3
|
-
import type { Organization } from "better-auth/plugins/organization";
|
|
4
|
-
import { defu } from "defu";
|
|
5
|
-
import type Stripe from "stripe";
|
|
6
|
-
import { STRIPE_ERROR_CODES } from "./error-codes";
|
|
7
|
-
import {
|
|
8
|
-
cancelSubscription,
|
|
9
|
-
cancelSubscriptionCallback,
|
|
10
|
-
createBillingPortal,
|
|
11
|
-
listActiveSubscriptions,
|
|
12
|
-
restoreSubscription,
|
|
13
|
-
stripeWebhook,
|
|
14
|
-
subscriptionSuccess,
|
|
15
|
-
upgradeSubscription,
|
|
16
|
-
} from "./routes";
|
|
17
|
-
import { getSchema } from "./schema";
|
|
18
|
-
import type {
|
|
19
|
-
StripeOptions,
|
|
20
|
-
StripePlan,
|
|
21
|
-
Subscription,
|
|
22
|
-
SubscriptionOptions,
|
|
23
|
-
WithStripeCustomerId,
|
|
24
|
-
} from "./types";
|
|
25
|
-
import { escapeStripeSearchValue } from "./utils";
|
|
26
|
-
|
|
27
|
-
declare module "@better-auth/core" {
|
|
28
|
-
// biome-ignore lint/correctness/noUnusedVariables: Auth and Context need to be same as declared in the module
|
|
29
|
-
interface BetterAuthPluginRegistry<Auth, Context> {
|
|
30
|
-
stripe: {
|
|
31
|
-
creator: typeof stripe;
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export const stripe = <O extends StripeOptions>(options: O) => {
|
|
37
|
-
const client = options.stripeClient;
|
|
38
|
-
|
|
39
|
-
const subscriptionEndpoints = {
|
|
40
|
-
upgradeSubscription: upgradeSubscription(options),
|
|
41
|
-
cancelSubscriptionCallback: cancelSubscriptionCallback(options),
|
|
42
|
-
cancelSubscription: cancelSubscription(options),
|
|
43
|
-
restoreSubscription: restoreSubscription(options),
|
|
44
|
-
listActiveSubscriptions: listActiveSubscriptions(options),
|
|
45
|
-
subscriptionSuccess: subscriptionSuccess(options),
|
|
46
|
-
createBillingPortal: createBillingPortal(options),
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
return {
|
|
50
|
-
id: "stripe",
|
|
51
|
-
endpoints: {
|
|
52
|
-
stripeWebhook: stripeWebhook(options),
|
|
53
|
-
...((options.subscription?.enabled
|
|
54
|
-
? subscriptionEndpoints
|
|
55
|
-
: {}) as O["subscription"] extends {
|
|
56
|
-
enabled: true;
|
|
57
|
-
}
|
|
58
|
-
? typeof subscriptionEndpoints
|
|
59
|
-
: {}),
|
|
60
|
-
},
|
|
61
|
-
init(ctx) {
|
|
62
|
-
if (options.organization?.enabled) {
|
|
63
|
-
const orgPlugin = ctx.getPlugin("organization");
|
|
64
|
-
if (!orgPlugin) {
|
|
65
|
-
ctx.logger.error(`Organization plugin not found`);
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const existingHooks = orgPlugin.options.organizationHooks ?? {};
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Sync organization name to Stripe customer
|
|
73
|
-
*/
|
|
74
|
-
const afterUpdateStripeOrg = async (data: {
|
|
75
|
-
organization: (Organization & WithStripeCustomerId) | null;
|
|
76
|
-
user: User;
|
|
77
|
-
}) => {
|
|
78
|
-
const { organization } = data;
|
|
79
|
-
if (!organization?.stripeCustomerId) return;
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
const stripeCustomer = await client.customers.retrieve(
|
|
83
|
-
organization.stripeCustomerId,
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
if (stripeCustomer.deleted) {
|
|
87
|
-
ctx.logger.warn(
|
|
88
|
-
`Stripe customer ${organization.stripeCustomerId} was deleted`,
|
|
89
|
-
);
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Update Stripe customer if name changed
|
|
94
|
-
if (organization.name !== stripeCustomer.name) {
|
|
95
|
-
await client.customers.update(organization.stripeCustomerId, {
|
|
96
|
-
name: organization.name,
|
|
97
|
-
});
|
|
98
|
-
ctx.logger.info(
|
|
99
|
-
`Synced organization name to Stripe: "${stripeCustomer.name}" → "${organization.name}"`,
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
} catch (e: any) {
|
|
103
|
-
ctx.logger.error(
|
|
104
|
-
`Failed to sync organization to Stripe: ${e.message}`,
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Block deletion if organization has active subscriptions
|
|
111
|
-
*/
|
|
112
|
-
const beforeDeleteStripeOrg = async (data: {
|
|
113
|
-
organization: Organization & WithStripeCustomerId;
|
|
114
|
-
user: User;
|
|
115
|
-
}) => {
|
|
116
|
-
const { organization } = data;
|
|
117
|
-
if (!organization.stripeCustomerId) return;
|
|
118
|
-
|
|
119
|
-
try {
|
|
120
|
-
// Check if organization has any active subscriptions
|
|
121
|
-
const subscriptions = await client.subscriptions.list({
|
|
122
|
-
customer: organization.stripeCustomerId,
|
|
123
|
-
status: "all",
|
|
124
|
-
limit: 100, // 1 ~ 100
|
|
125
|
-
});
|
|
126
|
-
for (const sub of subscriptions.data) {
|
|
127
|
-
if (
|
|
128
|
-
sub.status !== "canceled" &&
|
|
129
|
-
sub.status !== "incomplete" &&
|
|
130
|
-
sub.status !== "incomplete_expired"
|
|
131
|
-
) {
|
|
132
|
-
throw APIError.from(
|
|
133
|
-
"BAD_REQUEST",
|
|
134
|
-
STRIPE_ERROR_CODES.ORGANIZATION_HAS_ACTIVE_SUBSCRIPTION,
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
} catch (error: any) {
|
|
139
|
-
if (error instanceof APIError) {
|
|
140
|
-
throw error;
|
|
141
|
-
}
|
|
142
|
-
ctx.logger.error(
|
|
143
|
-
`Failed to check organization subscriptions: ${error.message}`,
|
|
144
|
-
);
|
|
145
|
-
throw error;
|
|
146
|
-
}
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
orgPlugin.options.organizationHooks = {
|
|
150
|
-
...existingHooks,
|
|
151
|
-
afterUpdateOrganization: existingHooks.afterUpdateOrganization
|
|
152
|
-
? async (data) => {
|
|
153
|
-
await existingHooks.afterUpdateOrganization!(data);
|
|
154
|
-
await afterUpdateStripeOrg(data);
|
|
155
|
-
}
|
|
156
|
-
: afterUpdateStripeOrg,
|
|
157
|
-
beforeDeleteOrganization: existingHooks.beforeDeleteOrganization
|
|
158
|
-
? async (data) => {
|
|
159
|
-
await existingHooks.beforeDeleteOrganization!(data);
|
|
160
|
-
await beforeDeleteStripeOrg(data);
|
|
161
|
-
}
|
|
162
|
-
: beforeDeleteStripeOrg,
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return {
|
|
167
|
-
options: {
|
|
168
|
-
databaseHooks: {
|
|
169
|
-
user: {
|
|
170
|
-
create: {
|
|
171
|
-
async after(user: User & WithStripeCustomerId, ctx) {
|
|
172
|
-
if (
|
|
173
|
-
!ctx ||
|
|
174
|
-
!options.createCustomerOnSignUp ||
|
|
175
|
-
user.stripeCustomerId // Skip if user already has a Stripe customer ID
|
|
176
|
-
) {
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
try {
|
|
181
|
-
// Check if user customer already exists in Stripe by email
|
|
182
|
-
const existingCustomers = await client.customers.search({
|
|
183
|
-
query: `email:"${escapeStripeSearchValue(user.email)}" AND -metadata["customerType"]:"organization"`,
|
|
184
|
-
limit: 1,
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
let stripeCustomer = existingCustomers.data[0];
|
|
188
|
-
|
|
189
|
-
// If user customer exists, link it to prevent duplicate creation
|
|
190
|
-
if (stripeCustomer) {
|
|
191
|
-
await ctx.context.internalAdapter.updateUser(user.id, {
|
|
192
|
-
stripeCustomerId: stripeCustomer.id,
|
|
193
|
-
});
|
|
194
|
-
await options.onCustomerCreate?.(
|
|
195
|
-
{
|
|
196
|
-
stripeCustomer,
|
|
197
|
-
user: {
|
|
198
|
-
...user,
|
|
199
|
-
stripeCustomerId: stripeCustomer.id,
|
|
200
|
-
},
|
|
201
|
-
},
|
|
202
|
-
ctx,
|
|
203
|
-
);
|
|
204
|
-
ctx.context.logger.info(
|
|
205
|
-
`Linked existing Stripe customer ${stripeCustomer.id} to user ${user.id}`,
|
|
206
|
-
);
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Create new Stripe customer
|
|
211
|
-
let extraCreateParams: Partial<Stripe.CustomerCreateParams> =
|
|
212
|
-
{};
|
|
213
|
-
if (options.getCustomerCreateParams) {
|
|
214
|
-
extraCreateParams = await options.getCustomerCreateParams(
|
|
215
|
-
user,
|
|
216
|
-
ctx,
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const params: Stripe.CustomerCreateParams = defu(
|
|
221
|
-
{
|
|
222
|
-
email: user.email,
|
|
223
|
-
name: user.name,
|
|
224
|
-
metadata: {
|
|
225
|
-
userId: user.id,
|
|
226
|
-
customerType: "user",
|
|
227
|
-
},
|
|
228
|
-
},
|
|
229
|
-
extraCreateParams,
|
|
230
|
-
);
|
|
231
|
-
stripeCustomer = await client.customers.create(params);
|
|
232
|
-
await ctx.context.internalAdapter.updateUser(user.id, {
|
|
233
|
-
stripeCustomerId: stripeCustomer.id,
|
|
234
|
-
});
|
|
235
|
-
await options.onCustomerCreate?.(
|
|
236
|
-
{
|
|
237
|
-
stripeCustomer,
|
|
238
|
-
user: {
|
|
239
|
-
...user,
|
|
240
|
-
stripeCustomerId: stripeCustomer.id,
|
|
241
|
-
},
|
|
242
|
-
},
|
|
243
|
-
ctx,
|
|
244
|
-
);
|
|
245
|
-
ctx.context.logger.info(
|
|
246
|
-
`Created new Stripe customer ${stripeCustomer.id} for user ${user.id}`,
|
|
247
|
-
);
|
|
248
|
-
} catch (e: any) {
|
|
249
|
-
ctx.context.logger.error(
|
|
250
|
-
`Failed to create or link Stripe customer: ${e.message}`,
|
|
251
|
-
e,
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
},
|
|
255
|
-
},
|
|
256
|
-
update: {
|
|
257
|
-
async after(user: User & WithStripeCustomerId, ctx) {
|
|
258
|
-
if (
|
|
259
|
-
!ctx ||
|
|
260
|
-
!user.stripeCustomerId // Only proceed if user has a Stripe customer ID
|
|
261
|
-
)
|
|
262
|
-
return;
|
|
263
|
-
|
|
264
|
-
try {
|
|
265
|
-
// Get the user from the database to check if email actually changed
|
|
266
|
-
// The 'user' parameter here is the freshly updated user
|
|
267
|
-
// We need to check if the Stripe customer's email matches
|
|
268
|
-
const stripeCustomer = await client.customers.retrieve(
|
|
269
|
-
user.stripeCustomerId,
|
|
270
|
-
);
|
|
271
|
-
|
|
272
|
-
// Check if customer was deleted
|
|
273
|
-
if (stripeCustomer.deleted) {
|
|
274
|
-
ctx.context.logger.warn(
|
|
275
|
-
`Stripe customer ${user.stripeCustomerId} was deleted, cannot update email`,
|
|
276
|
-
);
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// If Stripe customer email doesn't match the user's current email, update it
|
|
281
|
-
if (stripeCustomer.email !== user.email) {
|
|
282
|
-
await client.customers.update(user.stripeCustomerId, {
|
|
283
|
-
email: user.email,
|
|
284
|
-
});
|
|
285
|
-
ctx.context.logger.info(
|
|
286
|
-
`Updated Stripe customer email from ${stripeCustomer.email} to ${user.email}`,
|
|
287
|
-
);
|
|
288
|
-
}
|
|
289
|
-
} catch (e: any) {
|
|
290
|
-
// Ignore errors - this is a best-effort sync
|
|
291
|
-
// Email might have been deleted or Stripe customer might not exist
|
|
292
|
-
ctx.context.logger.error(
|
|
293
|
-
`Failed to sync email to Stripe customer: ${e.message}`,
|
|
294
|
-
e,
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
},
|
|
298
|
-
},
|
|
299
|
-
},
|
|
300
|
-
},
|
|
301
|
-
},
|
|
302
|
-
};
|
|
303
|
-
},
|
|
304
|
-
schema: getSchema(options),
|
|
305
|
-
options: options as NoInfer<O>,
|
|
306
|
-
$ERROR_CODES: STRIPE_ERROR_CODES,
|
|
307
|
-
} satisfies BetterAuthPlugin;
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
export type StripePlugin<O extends StripeOptions> = ReturnType<
|
|
311
|
-
typeof stripe<O>
|
|
312
|
-
>;
|
|
313
|
-
|
|
314
|
-
export type { Subscription, SubscriptionOptions, StripePlan };
|
package/src/middleware.ts
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import { createAuthMiddleware } from "@better-auth/core/api";
|
|
2
|
-
import { APIError } from "@better-auth/core/error";
|
|
3
|
-
import { sessionMiddleware } from "better-auth/api";
|
|
4
|
-
import { STRIPE_ERROR_CODES } from "./error-codes";
|
|
5
|
-
import type {
|
|
6
|
-
AuthorizeReferenceAction,
|
|
7
|
-
CustomerType,
|
|
8
|
-
StripeCtxSession,
|
|
9
|
-
SubscriptionOptions,
|
|
10
|
-
} from "./types";
|
|
11
|
-
|
|
12
|
-
export const stripeSessionMiddleware = createAuthMiddleware(
|
|
13
|
-
{
|
|
14
|
-
use: [sessionMiddleware],
|
|
15
|
-
},
|
|
16
|
-
async (ctx) => {
|
|
17
|
-
const session = ctx.context.session as StripeCtxSession;
|
|
18
|
-
return {
|
|
19
|
-
session,
|
|
20
|
-
};
|
|
21
|
-
},
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
export const referenceMiddleware = (
|
|
25
|
-
subscriptionOptions: SubscriptionOptions,
|
|
26
|
-
action: AuthorizeReferenceAction,
|
|
27
|
-
) =>
|
|
28
|
-
createAuthMiddleware(async (ctx) => {
|
|
29
|
-
const ctxSession = ctx.context.session as StripeCtxSession;
|
|
30
|
-
if (!ctxSession) {
|
|
31
|
-
throw APIError.from("UNAUTHORIZED", STRIPE_ERROR_CODES.UNAUTHORIZED);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const customerType: CustomerType =
|
|
35
|
-
ctx.body?.customerType || ctx.query?.customerType;
|
|
36
|
-
const explicitReferenceId = ctx.body?.referenceId || ctx.query?.referenceId;
|
|
37
|
-
|
|
38
|
-
if (customerType === "organization") {
|
|
39
|
-
// Organization subscriptions always require authorizeReference
|
|
40
|
-
if (!subscriptionOptions.authorizeReference) {
|
|
41
|
-
ctx.context.logger.error(
|
|
42
|
-
`Organization subscriptions require authorizeReference to be defined in your stripe plugin config.`,
|
|
43
|
-
);
|
|
44
|
-
throw APIError.from(
|
|
45
|
-
"BAD_REQUEST",
|
|
46
|
-
STRIPE_ERROR_CODES.ORGANIZATION_SUBSCRIPTION_NOT_ENABLED,
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const referenceId =
|
|
51
|
-
explicitReferenceId || ctxSession.session.activeOrganizationId;
|
|
52
|
-
if (!referenceId) {
|
|
53
|
-
throw APIError.from(
|
|
54
|
-
"BAD_REQUEST",
|
|
55
|
-
STRIPE_ERROR_CODES.ORGANIZATION_REFERENCE_ID_REQUIRED,
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
const isAuthorized = await subscriptionOptions.authorizeReference(
|
|
59
|
-
{
|
|
60
|
-
user: ctxSession.user,
|
|
61
|
-
session: ctxSession.session,
|
|
62
|
-
referenceId,
|
|
63
|
-
action,
|
|
64
|
-
},
|
|
65
|
-
ctx,
|
|
66
|
-
);
|
|
67
|
-
if (!isAuthorized) {
|
|
68
|
-
throw APIError.from("UNAUTHORIZED", STRIPE_ERROR_CODES.UNAUTHORIZED);
|
|
69
|
-
}
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// User subscriptions - pass if no explicit referenceId
|
|
74
|
-
if (!explicitReferenceId) {
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Pass if referenceId is user id
|
|
79
|
-
if (explicitReferenceId === ctxSession.user.id) {
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (!subscriptionOptions.authorizeReference) {
|
|
84
|
-
ctx.context.logger.error(
|
|
85
|
-
`Passing referenceId into a subscription action isn't allowed if subscription.authorizeReference isn't defined in your stripe plugin config.`,
|
|
86
|
-
);
|
|
87
|
-
throw APIError.from(
|
|
88
|
-
"BAD_REQUEST",
|
|
89
|
-
STRIPE_ERROR_CODES.REFERENCE_ID_NOT_ALLOWED,
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
const isAuthorized = await subscriptionOptions.authorizeReference(
|
|
93
|
-
{
|
|
94
|
-
user: ctxSession.user,
|
|
95
|
-
session: ctxSession.session,
|
|
96
|
-
referenceId: explicitReferenceId,
|
|
97
|
-
action,
|
|
98
|
-
},
|
|
99
|
-
ctx,
|
|
100
|
-
);
|
|
101
|
-
if (!isAuthorized) {
|
|
102
|
-
throw APIError.from("UNAUTHORIZED", STRIPE_ERROR_CODES.UNAUTHORIZED);
|
|
103
|
-
}
|
|
104
|
-
});
|