@faire-auth/stripe 1.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/dist/index.mjs ADDED
@@ -0,0 +1,968 @@
1
+ import { callbackURLSchema, createRoute, redirectUrlSchema, req, res } from "@faire-auth/core/factory";
2
+ import { False, True } from "@faire-auth/core/static";
3
+ import { logger } from "faire-auth";
4
+ import { getSessionFromCtx, originCheck, sessionMiddleware } from "faire-auth/middleware";
5
+ import { createEndpoint, createMiddleware } from "faire-auth/plugins";
6
+ import Stripe from "stripe";
7
+ import * as z from "zod";
8
+ import { mergeSchema } from "faire-auth/db";
9
+
10
+ //#region src/utils.ts
11
+ async function getPlans(subscriptionOptions) {
12
+ if (subscriptionOptions?.enabled) return typeof subscriptionOptions.plans === "function" ? await subscriptionOptions.plans() : subscriptionOptions.plans;
13
+ throw new Error("Subscriptions are not enabled in the Stripe options.");
14
+ }
15
+ async function getPlanByPriceInfo(options, priceId, priceLookupKey) {
16
+ return await getPlans(options.subscription).then((res$1) => res$1?.find((plan) => plan.priceId === priceId || plan.annualDiscountPriceId === priceId || priceLookupKey && (plan.lookupKey === priceLookupKey || plan.annualDiscountLookupKey === priceLookupKey)));
17
+ }
18
+ async function getPlanByName(options, name) {
19
+ return await getPlans(options.subscription).then((res$1) => res$1?.find((plan) => plan.name.toLowerCase() === name.toLowerCase()));
20
+ }
21
+
22
+ //#endregion
23
+ //#region src/hooks.ts
24
+ async function onCheckoutSessionCompleted(ctx, options, event) {
25
+ try {
26
+ const client = options.stripeClient;
27
+ const checkoutSession = event.data.object;
28
+ if (checkoutSession.mode === "setup" || !options.subscription?.enabled) return;
29
+ const subscription = await client.subscriptions.retrieve(checkoutSession.subscription);
30
+ const priceId = subscription.items.data[0]?.price.id;
31
+ const plan = await getPlanByPriceInfo(options, priceId, subscription.items.data[0]?.price.lookup_key || null);
32
+ if (plan) {
33
+ const referenceId = checkoutSession?.client_reference_id || checkoutSession?.metadata?.referenceId;
34
+ const subscriptionId = checkoutSession?.metadata?.subscriptionId;
35
+ const seats = subscription.items.data[0].quantity;
36
+ if (referenceId && subscriptionId) {
37
+ const trial = subscription.trial_start && subscription.trial_end ? {
38
+ trialStart: /* @__PURE__ */ new Date(subscription.trial_start * 1e3),
39
+ trialEnd: /* @__PURE__ */ new Date(subscription.trial_end * 1e3)
40
+ } : {};
41
+ let dbSubscription = await ctx.get("context").adapter.update({
42
+ model: "subscription",
43
+ update: {
44
+ plan: plan.name.toLowerCase(),
45
+ status: subscription.status,
46
+ updatedAt: /* @__PURE__ */ new Date(),
47
+ periodStart: /* @__PURE__ */ new Date(subscription.items.data[0].current_period_start * 1e3),
48
+ periodEnd: /* @__PURE__ */ new Date(subscription.items.data[0].current_period_end * 1e3),
49
+ stripeSubscriptionId: checkoutSession.subscription,
50
+ seats,
51
+ ...trial
52
+ },
53
+ where: [{
54
+ field: "id",
55
+ value: subscriptionId
56
+ }]
57
+ });
58
+ if (trial.trialStart && plan.freeTrial?.onTrialStart) await plan.freeTrial.onTrialStart(dbSubscription);
59
+ if (!dbSubscription) dbSubscription = await ctx.get("context").adapter.findOne({
60
+ model: "subscription",
61
+ where: [{
62
+ field: "id",
63
+ value: subscriptionId
64
+ }]
65
+ });
66
+ await options.subscription?.onSubscriptionComplete?.({
67
+ event,
68
+ subscription: dbSubscription,
69
+ stripeSubscription: subscription,
70
+ plan
71
+ }, ctx);
72
+ return;
73
+ }
74
+ }
75
+ } catch (e) {
76
+ logger.error(`Stripe webhook failed. Error: ${e.message}`);
77
+ }
78
+ }
79
+ async function onSubscriptionUpdated(ctx, options, event) {
80
+ try {
81
+ if (!options.subscription?.enabled) return;
82
+ const subscriptionUpdated = event.data.object;
83
+ const priceId = subscriptionUpdated.items.data[0].price.id;
84
+ const plan = await getPlanByPriceInfo(options, priceId, subscriptionUpdated.items.data[0].price.lookup_key || null);
85
+ const subscriptionId = subscriptionUpdated.metadata?.subscriptionId;
86
+ const customerId = subscriptionUpdated.customer?.toString();
87
+ let subscription = await ctx.get("context").adapter.findOne({
88
+ model: "subscription",
89
+ where: subscriptionId ? [{
90
+ field: "id",
91
+ value: subscriptionId
92
+ }] : [{
93
+ field: "stripeSubscriptionId",
94
+ value: subscriptionUpdated.id
95
+ }]
96
+ });
97
+ if (!subscription) {
98
+ const subs = await ctx.get("context").adapter.findMany({
99
+ model: "subscription",
100
+ where: [{
101
+ field: "stripeCustomerId",
102
+ value: customerId
103
+ }]
104
+ });
105
+ if (subs.length > 1) {
106
+ const activeSub = subs.find((sub) => sub.status === "active" || sub.status === "trialing");
107
+ if (!activeSub) {
108
+ logger.warn(`Stripe webhook error: Multiple subscriptions found for customerId: ${customerId} and no active subscription is found`);
109
+ return;
110
+ }
111
+ subscription = activeSub;
112
+ } else subscription = subs[0];
113
+ }
114
+ const seats = subscriptionUpdated.items.data[0].quantity;
115
+ await ctx.get("context").adapter.update({
116
+ model: "subscription",
117
+ update: {
118
+ ...plan ? {
119
+ plan: plan.name.toLowerCase(),
120
+ limits: plan.limits
121
+ } : {},
122
+ updatedAt: /* @__PURE__ */ new Date(),
123
+ status: subscriptionUpdated.status,
124
+ periodStart: /* @__PURE__ */ new Date(subscriptionUpdated.items.data[0].current_period_start * 1e3),
125
+ periodEnd: /* @__PURE__ */ new Date(subscriptionUpdated.items.data[0].current_period_end * 1e3),
126
+ cancelAtPeriodEnd: subscriptionUpdated.cancel_at_period_end,
127
+ seats,
128
+ stripeSubscriptionId: subscriptionUpdated.id
129
+ },
130
+ where: [{
131
+ field: "id",
132
+ value: subscription.id
133
+ }]
134
+ });
135
+ if (subscriptionUpdated.status === "active" && subscriptionUpdated.cancel_at_period_end && !subscription.cancelAtPeriodEnd) await options.subscription.onSubscriptionCancel?.({
136
+ subscription,
137
+ ...subscriptionUpdated.cancellation_details !== void 0 && { cancellationDetails: subscriptionUpdated.cancellation_details },
138
+ stripeSubscription: subscriptionUpdated,
139
+ event
140
+ });
141
+ await options.subscription.onSubscriptionUpdate?.({
142
+ event,
143
+ subscription
144
+ });
145
+ if (plan) {
146
+ if (subscriptionUpdated.status === "active" && subscription.status === "trialing" && plan.freeTrial?.onTrialEnd) await plan.freeTrial.onTrialEnd({ subscription }, ctx);
147
+ if (subscriptionUpdated.status === "incomplete_expired" && subscription.status === "trialing" && plan.freeTrial?.onTrialExpired) await plan.freeTrial.onTrialExpired(subscription, ctx);
148
+ }
149
+ } catch (error) {
150
+ logger.error(`Stripe webhook failed. Error: ${error}`);
151
+ }
152
+ }
153
+ async function onSubscriptionDeleted(ctx, options, event) {
154
+ if (!options.subscription?.enabled) return;
155
+ try {
156
+ const subscriptionDeleted = event.data.object;
157
+ const subscriptionId = subscriptionDeleted.id;
158
+ const subscription = await ctx.get("context").adapter.findOne({
159
+ model: "subscription",
160
+ where: [{
161
+ field: "stripeSubscriptionId",
162
+ value: subscriptionId
163
+ }]
164
+ });
165
+ if (subscription) {
166
+ await ctx.get("context").adapter.update({
167
+ model: "subscription",
168
+ where: [{
169
+ field: "id",
170
+ value: subscription.id
171
+ }],
172
+ update: {
173
+ status: "canceled",
174
+ updatedAt: /* @__PURE__ */ new Date()
175
+ }
176
+ });
177
+ await options.subscription.onSubscriptionDeleted?.({
178
+ event,
179
+ stripeSubscription: subscriptionDeleted,
180
+ subscription
181
+ });
182
+ } else logger.warn(`Stripe webhook error: Subscription not found for subscriptionId: ${subscriptionId}`);
183
+ } catch (error) {
184
+ logger.error(`Stripe webhook failed. Error: ${error}`);
185
+ }
186
+ }
187
+
188
+ //#endregion
189
+ //#region src/schema.ts
190
+ const subscriptions = { subscription: { fields: {
191
+ plan: {
192
+ type: "string",
193
+ required: true
194
+ },
195
+ referenceId: {
196
+ type: "string",
197
+ required: true
198
+ },
199
+ stripeCustomerId: {
200
+ type: "string",
201
+ required: false
202
+ },
203
+ stripeSubscriptionId: {
204
+ type: "string",
205
+ required: false
206
+ },
207
+ status: {
208
+ type: "string",
209
+ defaultValue: "incomplete"
210
+ },
211
+ periodStart: {
212
+ type: "date",
213
+ required: false
214
+ },
215
+ periodEnd: {
216
+ type: "date",
217
+ required: false
218
+ },
219
+ trialStart: {
220
+ type: "date",
221
+ required: false
222
+ },
223
+ trialEnd: {
224
+ type: "date",
225
+ required: false
226
+ },
227
+ cancelAtPeriodEnd: {
228
+ type: "boolean",
229
+ required: false,
230
+ defaultValue: false
231
+ },
232
+ seats: {
233
+ type: "number",
234
+ required: false
235
+ }
236
+ } } };
237
+ const user = { user: { fields: { stripeCustomerId: {
238
+ type: "string",
239
+ required: false
240
+ } } } };
241
+ const getSchema = (options) => {
242
+ let baseSchema = {};
243
+ if (options.subscription?.enabled) baseSchema = {
244
+ ...subscriptions,
245
+ ...user
246
+ };
247
+ else baseSchema = { ...user };
248
+ if (options.schema && !options.subscription?.enabled && "subscription" in options.schema) {
249
+ const { subscription,...restSchema } = options.schema;
250
+ return mergeSchema(baseSchema, restSchema);
251
+ }
252
+ return mergeSchema(baseSchema, options.schema);
253
+ };
254
+
255
+ //#endregion
256
+ //#region src/index.ts
257
+ const STRIPE_ERROR_CODES = {
258
+ SUBSCRIPTION_NOT_FOUND: "Subscription not found",
259
+ SUBSCRIPTION_PLAN_NOT_FOUND: "Subscription plan not found",
260
+ ALREADY_SUBSCRIBED_PLAN: "You're already subscribed to this plan",
261
+ UNABLE_TO_CREATE_CUSTOMER: "Unable to create customer",
262
+ FAILED_TO_FETCH_PLANS: "Failed to fetch plans",
263
+ EMAIL_VERIFICATION_REQUIRED: "Email verification is required before you can subscribe to a plan",
264
+ SUBSCRIPTION_NOT_ACTIVE: "Subscription is not active",
265
+ SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION: "Subscription is not scheduled for cancellation"
266
+ };
267
+ const getUrl = (options, url) => {
268
+ if (url.startsWith("http")) return url;
269
+ return `${options.baseURL}${url.startsWith("/") ? url : `/${url}`}`;
270
+ };
271
+ async function resolvePriceIdFromLookupKey(stripeClient, lookupKey) {
272
+ if (!lookupKey) return void 0;
273
+ return (await stripeClient.prices.list({
274
+ lookup_keys: [lookupKey],
275
+ active: true,
276
+ limit: 1
277
+ })).data[0]?.id;
278
+ }
279
+ const stripe = (options) => {
280
+ const client = options.stripeClient;
281
+ const referenceMiddleware = (action) => createMiddleware()(async (ctx, next) => {
282
+ const session = ctx.get("session");
283
+ if (!session) return ctx.json({ success: False }, 401);
284
+ if (!options.subscription?.enabled) {
285
+ logger.error(`Subscription actions are not enabled in your stripe plugin config.`);
286
+ return ctx.json({ success: False }, 400);
287
+ }
288
+ let body = {};
289
+ let bodyHasId = false;
290
+ try {
291
+ body = await ctx.req.raw.clone().json();
292
+ } catch {}
293
+ if (body.referenceId) bodyHasId = true;
294
+ else body.referenceId = ctx.req.query("referenceId") ?? session.user.id;
295
+ if (bodyHasId && !options.subscription?.authorizeReference) {
296
+ logger.error(`Passing referenceId into a subscription action isn't allowed if subscription.authorizeReference isn't defined in your stripe plugin config.`);
297
+ return ctx.json({
298
+ success: False,
299
+ message: "Reference id is not allowed. Read server logs for more details."
300
+ }, 400);
301
+ }
302
+ if (!(bodyHasId ? await options.subscription?.authorizeReference?.({
303
+ user: session.user,
304
+ session: session.session,
305
+ referenceId: body.referenceId,
306
+ action
307
+ }, ctx) : true)) return ctx.json({ success: False }, 401);
308
+ return await next();
309
+ });
310
+ const subscriptionEndpoints = {
311
+ upgradeSubscription: createEndpoint(createRoute({
312
+ operationId: "upgradeSubscription",
313
+ method: "post",
314
+ path: "/subscription/upgrade",
315
+ middleware: [
316
+ sessionMiddleware(),
317
+ originCheck(async (ctx) => {
318
+ const { successURL, cancelURL } = await ctx.req.raw.clone().json();
319
+ return [successURL, cancelURL];
320
+ }),
321
+ referenceMiddleware("upgrade-subscription")
322
+ ],
323
+ request: req().bdy(z.object({
324
+ plan: z.string().meta({ description: "The name of the plan to upgrade to. Eg: \"pro\"" }),
325
+ annual: z.boolean().meta({ description: "Whether to upgrade to an annual plan. Eg: true" }).optional(),
326
+ referenceId: z.string().meta({ description: "Reference id of the subscription to upgrade. Eg: \"123\"" }).optional(),
327
+ subscriptionId: z.string().meta({ description: "The id of the subscription to upgrade. Eg: \"sub_123\"" }).optional(),
328
+ metadata: z.record(z.string(), z.any()).optional(),
329
+ seats: z.number().meta({ description: "Number of seats to upgrade to (if applicable). Eg: 1" }).optional(),
330
+ successUrl: z.string().meta({ description: "Callback URL to redirect back after successful subscription. Eg: \"https://example.com/success\"" }).default("/"),
331
+ cancelUrl: z.string().meta({ description: "If set, checkout shows a back button and customers will be directed here if they cancel payment. Eg: \"https://example.com/pricing\"" }).default("/"),
332
+ returnUrl: z.string().meta({ description: "URL to take customers to when they click on the billing portal’s link to return to your website. Eg: \"https://example.com/dashboard\"" }).optional(),
333
+ disableRedirect: z.boolean().meta({ description: "Disable redirect after successful subscription. Eg: true" }).default(false)
334
+ })).bld(),
335
+ responses: res(z.intersection(redirectUrlSchema, z.record(z.string(), z.any()))).err(400).err(401).err(500).bld()
336
+ }), (authOptions) => async (ctx) => {
337
+ if (!options.subscription?.enabled) {
338
+ logger.error(`Subscription actions are not enabled in your stripe plugin config.`);
339
+ return ctx.json({ success: False }, 400);
340
+ }
341
+ const { user: user$1, session } = ctx.get("session");
342
+ if (!user$1.emailVerified && options.subscription?.enabled && options.subscription.requireEmailVerification) return ctx.json({
343
+ success: False,
344
+ message: STRIPE_ERROR_CODES.EMAIL_VERIFICATION_REQUIRED
345
+ }, 400);
346
+ const { referenceId = user$1.id, plan: requestPlan, subscriptionId, metadata, seats, returnUrl, cancelUrl, disableRedirect, successUrl, annual } = ctx.req.valid("json");
347
+ const plan = await getPlanByName(options, requestPlan);
348
+ if (!plan) return ctx.json({
349
+ success: False,
350
+ message: STRIPE_ERROR_CODES.SUBSCRIPTION_PLAN_NOT_FOUND
351
+ }, 400);
352
+ const subscriptionToUpdate = subscriptionId ? await ctx.get("context").adapter.findOne({
353
+ model: "subscription",
354
+ where: [{
355
+ field: "id",
356
+ value: subscriptionId,
357
+ connector: "OR"
358
+ }, {
359
+ field: "stripeSubscriptionId",
360
+ value: subscriptionId,
361
+ connector: "OR"
362
+ }]
363
+ }) : referenceId ? await ctx.get("context").adapter.findOne({
364
+ model: "subscription",
365
+ where: [{
366
+ field: "referenceId",
367
+ value: referenceId
368
+ }]
369
+ }) : null;
370
+ if (subscriptionId && !subscriptionToUpdate) return ctx.json({
371
+ success: False,
372
+ message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND
373
+ }, 400);
374
+ let customerId = subscriptionToUpdate?.stripeCustomerId || user$1.stripeCustomerId;
375
+ if (!customerId) try {
376
+ let stripeCustomer = (await client.customers.list({
377
+ email: user$1.email,
378
+ limit: 1
379
+ })).data[0];
380
+ if (!stripeCustomer) stripeCustomer = await client.customers.create({
381
+ email: user$1.email,
382
+ ...user$1.name && { name: user$1.name },
383
+ metadata: {
384
+ ...metadata,
385
+ userId: user$1.id
386
+ }
387
+ });
388
+ await ctx.get("context").adapter.update({
389
+ model: "user",
390
+ update: { stripeCustomerId: stripeCustomer.id },
391
+ where: [{
392
+ field: "id",
393
+ value: user$1.id
394
+ }]
395
+ });
396
+ customerId = stripeCustomer.id;
397
+ } catch (e) {
398
+ ctx.get("context").logger.error(e);
399
+ return ctx.json({
400
+ success: False,
401
+ message: STRIPE_ERROR_CODES.UNABLE_TO_CREATE_CUSTOMER
402
+ }, 400);
403
+ }
404
+ const activeSubscription = (await client.subscriptions.list({ customer: customerId }).then((res$1) => res$1.data.filter((sub) => sub.status === "active" || sub.status === "trialing"))).find((sub) => subscriptionToUpdate?.stripeSubscriptionId || subscriptionId ? sub.id === subscriptionToUpdate?.stripeSubscriptionId || sub.id === subscriptionId : false);
405
+ const subscriptions$1 = subscriptionToUpdate ? [subscriptionToUpdate] : await ctx.get("context").adapter.findMany({
406
+ model: "subscription",
407
+ where: [{
408
+ field: "referenceId",
409
+ value: referenceId || user$1.id
410
+ }]
411
+ });
412
+ const activeOrTrialingSubscription = subscriptions$1.find((sub) => sub.status === "active" || sub.status === "trialing");
413
+ const incompleteSubscription = subscriptions$1.find((sub) => sub.status === "incomplete");
414
+ if (activeOrTrialingSubscription && activeOrTrialingSubscription.status === "active" && activeOrTrialingSubscription.plan === requestPlan && activeOrTrialingSubscription.seats === (seats || 1)) return ctx.json({
415
+ success: False,
416
+ message: STRIPE_ERROR_CODES.ALREADY_SUBSCRIBED_PLAN
417
+ }, 400);
418
+ if (activeSubscription && customerId) {
419
+ const res$1 = await client.billingPortal.sessions.create({
420
+ customer: customerId,
421
+ return_url: getUrl(authOptions, returnUrl || "/"),
422
+ flow_data: {
423
+ type: "subscription_update_confirm",
424
+ after_completion: {
425
+ type: "redirect",
426
+ redirect: { return_url: getUrl(authOptions, returnUrl || "/") }
427
+ },
428
+ subscription_update_confirm: {
429
+ subscription: activeSubscription.id,
430
+ items: [{
431
+ id: activeSubscription.items.data[0]?.id,
432
+ quantity: seats || 1,
433
+ price: annual ? plan.annualDiscountPriceId : plan.priceId
434
+ }]
435
+ }
436
+ }
437
+ }).catch(async (e) => ctx.json({
438
+ success: False,
439
+ message: e.message,
440
+ code: e.code
441
+ }, 400));
442
+ if (res$1 instanceof Response) return res$1;
443
+ return ctx.json({
444
+ url: res$1.url,
445
+ redirect: True
446
+ }, 200);
447
+ }
448
+ let subscription = activeOrTrialingSubscription || incompleteSubscription;
449
+ if (incompleteSubscription && !activeOrTrialingSubscription) subscription = await ctx.get("context").adapter.update({
450
+ model: "subscription",
451
+ update: {
452
+ plan: plan.name.toLowerCase(),
453
+ seats: seats || 1,
454
+ updatedAt: /* @__PURE__ */ new Date()
455
+ },
456
+ where: [{
457
+ field: "id",
458
+ value: incompleteSubscription.id
459
+ }]
460
+ }) || incompleteSubscription;
461
+ if (!subscription) subscription = await ctx.get("context").adapter.create({
462
+ model: "subscription",
463
+ data: {
464
+ plan: plan.name.toLowerCase(),
465
+ stripeCustomerId: customerId,
466
+ status: "incomplete",
467
+ referenceId,
468
+ seats: seats || 1
469
+ }
470
+ });
471
+ if (!subscription) {
472
+ ctx.get("context").logger.error("Subscription ID not found");
473
+ return ctx.json({ success: False }, 500);
474
+ }
475
+ const params = await options.subscription?.getCheckoutSessionParams?.({
476
+ user: user$1,
477
+ session,
478
+ plan,
479
+ subscription
480
+ }, ctx);
481
+ const freeTrial = !subscriptions$1.some((s) => {
482
+ return !!(s.trialStart || s.trialEnd) || s.status === "trialing";
483
+ }) && plan.freeTrial ? { trial_period_days: plan.freeTrial.days } : void 0;
484
+ let priceIdToUse = void 0;
485
+ if (annual) {
486
+ priceIdToUse = plan.annualDiscountPriceId;
487
+ if (!priceIdToUse && plan.annualDiscountLookupKey) priceIdToUse = await resolvePriceIdFromLookupKey(client, plan.annualDiscountLookupKey);
488
+ } else {
489
+ priceIdToUse = plan.priceId;
490
+ if (!priceIdToUse && plan.lookupKey) priceIdToUse = await resolvePriceIdFromLookupKey(client, plan.lookupKey);
491
+ }
492
+ const checkoutSession = await client.checkout.sessions.create({
493
+ ...customerId ? {
494
+ customer: customerId,
495
+ customer_update: {
496
+ name: "auto",
497
+ address: "auto"
498
+ }
499
+ } : { customer_email: user$1.email },
500
+ success_url: getUrl(authOptions, `${ctx.get("context").baseURL}/subscription/success?callbackURL=${encodeURIComponent(successUrl)}&subscriptionId=${encodeURIComponent(subscription.id)}`),
501
+ cancel_url: getUrl(authOptions, cancelUrl),
502
+ line_items: [{
503
+ price: priceIdToUse,
504
+ quantity: seats || 1
505
+ }],
506
+ subscription_data: { ...freeTrial },
507
+ mode: "subscription",
508
+ client_reference_id: referenceId,
509
+ ...params?.params,
510
+ metadata: {
511
+ userId: user$1.id,
512
+ subscriptionId: subscription.id,
513
+ referenceId,
514
+ ...params?.params?.metadata
515
+ }
516
+ }, params?.options).catch(async (e) => ctx.json({
517
+ success: False,
518
+ message: e.message,
519
+ code: e.code
520
+ }, 400));
521
+ if (checkoutSession instanceof Response) return checkoutSession;
522
+ return ctx.json({
523
+ ...checkoutSession,
524
+ redirect: !disableRedirect
525
+ }, 200);
526
+ }),
527
+ cancelSubscriptionCallback: createEndpoint(createRoute({
528
+ operationId: "cancelSubscriptionCallback",
529
+ method: "get",
530
+ path: "/subscription/cancel/callback",
531
+ middleware: [originCheck((ctx) => ctx.req.query("callbackURL"))],
532
+ request: req().qry(z.object({
533
+ callbackURL: callbackURLSchema(true),
534
+ subscriptionId: z.string().optional()
535
+ })).bld(),
536
+ responses: res().rdr().bld()
537
+ }), (authOptions) => async (ctx) => {
538
+ if (!options.subscription?.enabled) {
539
+ logger.error(`Subscription actions are not enabled in your stripe plugin config.`);
540
+ return ctx.json({ success: False }, 400);
541
+ }
542
+ const { callbackURL, subscriptionId } = ctx.req.valid("query");
543
+ if (!callbackURL || !subscriptionId) return ctx.redirect(getUrl(authOptions, callbackURL || "/"), 302);
544
+ const session = await getSessionFromCtx(ctx);
545
+ if (session instanceof Response) return ctx.redirect(getUrl(authOptions, callbackURL || "/"), 302);
546
+ if (session.user?.stripeCustomerId) try {
547
+ const subscription = await ctx.get("context").adapter.findOne({
548
+ model: "subscription",
549
+ where: [{
550
+ field: "id",
551
+ value: subscriptionId
552
+ }]
553
+ });
554
+ if (!subscription || subscription.cancelAtPeriodEnd || subscription.status === "canceled") return ctx.redirect(getUrl(authOptions, callbackURL), 302);
555
+ const currentSubscription = (await client.subscriptions.list({
556
+ customer: session.user.stripeCustomerId,
557
+ status: "active"
558
+ })).data.find((sub) => sub.id === subscription.stripeSubscriptionId);
559
+ if (currentSubscription?.cancel_at_period_end === true) {
560
+ await ctx.get("context").adapter.update({
561
+ model: "subscription",
562
+ update: {
563
+ status: currentSubscription?.status,
564
+ cancelAtPeriodEnd: true
565
+ },
566
+ where: [{
567
+ field: "id",
568
+ value: subscription.id
569
+ }]
570
+ });
571
+ await options.subscription?.onSubscriptionCancel?.({
572
+ subscription,
573
+ cancellationDetails: currentSubscription.cancellation_details,
574
+ stripeSubscription: currentSubscription,
575
+ event: void 0
576
+ });
577
+ }
578
+ } catch (error) {
579
+ ctx.get("context").logger.error("Error checking subscription status from Stripe", error);
580
+ }
581
+ return ctx.redirect(getUrl(authOptions, callbackURL), 302);
582
+ }),
583
+ cancelSubscription: createEndpoint(createRoute({
584
+ operationId: "cancelSubscription",
585
+ method: "post",
586
+ path: "/subscription/cancel",
587
+ middleware: [
588
+ sessionMiddleware(),
589
+ originCheck((ctx) => ctx.req.raw.clone().json().then((v) => v.returnUrl)),
590
+ referenceMiddleware("cancel-subscription")
591
+ ],
592
+ request: req().bdy(z.object({
593
+ referenceId: z.string().meta({ description: "Reference id of the subscription to cancel. Eg: '123'" }).optional(),
594
+ subscriptionId: z.string().meta({ description: "The id of the subscription to cancel. Eg: 'sub_123'" }).optional(),
595
+ returnUrl: z.string().meta({ description: "URL to take customers to when they click on the billing portal’s link to return to your website. Eg: \"https://example.com/dashboard\"" })
596
+ })).bld(),
597
+ responses: res(z.object({
598
+ url: z.string(),
599
+ redirect: z.literal(true)
600
+ })).err(400).err(401).bld()
601
+ }), (authOptions) => async (ctx) => {
602
+ const session = ctx.get("session");
603
+ const { referenceId = session.user.id, subscriptionId, returnUrl } = ctx.req.valid("json");
604
+ const subscription = subscriptionId ? await ctx.get("context").adapter.findOne({
605
+ model: "subscription",
606
+ where: [{
607
+ field: "id",
608
+ value: subscriptionId
609
+ }]
610
+ }) : await ctx.get("context").adapter.findMany({
611
+ model: "subscription",
612
+ where: [{
613
+ field: "referenceId",
614
+ value: referenceId
615
+ }]
616
+ }).then((subs) => subs.find((sub) => sub.status === "active" || sub.status === "trialing"));
617
+ if (!subscription || !subscription.stripeCustomerId) return ctx.json({
618
+ success: False,
619
+ message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND
620
+ }, 400);
621
+ const activeSubscriptions = await client.subscriptions.list({ customer: subscription.stripeCustomerId }).then((res$2) => res$2.data.filter((sub) => sub.status === "active" || sub.status === "trialing"));
622
+ if (!activeSubscriptions.length) {
623
+ /**
624
+ * If the subscription is not found, we need to delete the subscription
625
+ * from the database. This is a rare case and should not happen.
626
+ */
627
+ await ctx.get("context").adapter.deleteMany({
628
+ model: "subscription",
629
+ where: [{
630
+ field: "referenceId",
631
+ value: referenceId
632
+ }]
633
+ });
634
+ return ctx.json({
635
+ success: False,
636
+ message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND
637
+ }, 400);
638
+ }
639
+ const activeSubscription = activeSubscriptions.find((sub) => sub.id === subscription.stripeSubscriptionId);
640
+ if (!activeSubscription) return ctx.json({
641
+ success: False,
642
+ message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND
643
+ }, 400);
644
+ const res$1 = await client.billingPortal.sessions.create({
645
+ customer: subscription.stripeCustomerId,
646
+ return_url: getUrl(authOptions, `${ctx.get("context").baseURL}/subscription/cancel/callback?callbackURL=${encodeURIComponent(returnUrl || "/")}&subscriptionId=${encodeURIComponent(subscription.id)}`),
647
+ flow_data: {
648
+ type: "subscription_cancel",
649
+ subscription_cancel: { subscription: activeSubscription.id }
650
+ }
651
+ }).catch(async (e) => {
652
+ if (e.message.includes("already set to be cancel")) {
653
+ /**
654
+ * incase we missed the event from stripe, we set it manually
655
+ * this is a rare case and should not happen
656
+ */
657
+ if (!subscription.cancelAtPeriodEnd) await ctx.get("context").adapter.update({
658
+ model: "subscription",
659
+ update: { cancelAtPeriodEnd: true },
660
+ where: [{
661
+ field: "referenceId",
662
+ value: referenceId
663
+ }]
664
+ });
665
+ }
666
+ return ctx.json({
667
+ success: False,
668
+ message: e.message,
669
+ code: e.code
670
+ }, 400);
671
+ });
672
+ if (res$1 instanceof Response) return res$1;
673
+ return ctx.json({
674
+ url: res$1.url,
675
+ redirect: True
676
+ }, 200);
677
+ }),
678
+ restoreSubscription: createEndpoint(createRoute({
679
+ operationId: "restoreSubscription",
680
+ method: "post",
681
+ path: "/subscription/restore",
682
+ middleware: [sessionMiddleware(), referenceMiddleware("restore-subscription")],
683
+ request: req().bdy(z.object({
684
+ referenceId: z.string().meta({ description: "Reference id of the subscription to restore. Eg: '123'" }).optional(),
685
+ subscriptionId: z.string().meta({ description: "The id of the subscription to restore. Eg: 'sub_123'" }).optional()
686
+ })).bld(),
687
+ responses: res(z.looseObject({})).err(400).err(401).bld()
688
+ }), (authOptions) => async (ctx) => {
689
+ const session = ctx.get("session");
690
+ const { referenceId = session.user.id, subscriptionId } = ctx.req.valid("json");
691
+ const subscription = subscriptionId ? await ctx.get("context").adapter.findOne({
692
+ model: "subscription",
693
+ where: [{
694
+ field: "id",
695
+ value: subscriptionId
696
+ }]
697
+ }) : await ctx.get("context").adapter.findMany({
698
+ model: "subscription",
699
+ where: [{
700
+ field: "referenceId",
701
+ value: referenceId
702
+ }]
703
+ }).then((subs) => subs.find((sub) => sub.status === "active" || sub.status === "trialing"));
704
+ if (!subscription || !subscription.stripeCustomerId) return ctx.json({
705
+ success: False,
706
+ message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND
707
+ }, 400);
708
+ if (subscription.status != "active" && subscription.status != "trialing") return ctx.json({
709
+ success: False,
710
+ message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_ACTIVE
711
+ }, 400);
712
+ if (!subscription.cancelAtPeriodEnd) return ctx.json({
713
+ success: False,
714
+ message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION
715
+ }, 400);
716
+ const activeSubscription = await client.subscriptions.list({ customer: subscription.stripeCustomerId }).then((res$1) => res$1.data.filter((sub) => sub.status === "active" || sub.status === "trialing")[0]);
717
+ if (!activeSubscription) return ctx.json({
718
+ success: False,
719
+ message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND
720
+ }, 400);
721
+ try {
722
+ const newSub = await client.subscriptions.update(activeSubscription.id, { cancel_at_period_end: false });
723
+ await ctx.get("context").adapter.update({
724
+ model: "subscription",
725
+ update: {
726
+ cancelAtPeriodEnd: false,
727
+ updatedAt: /* @__PURE__ */ new Date()
728
+ },
729
+ where: [{
730
+ field: "id",
731
+ value: subscription.id
732
+ }]
733
+ });
734
+ return ctx.json(newSub, 200);
735
+ } catch (error) {
736
+ ctx.get("context").logger.error("Error restoring subscription", error);
737
+ return ctx.json({
738
+ success: False,
739
+ message: STRIPE_ERROR_CODES.UNABLE_TO_CREATE_CUSTOMER
740
+ }, 400);
741
+ }
742
+ }),
743
+ listActiveSubscriptions: createEndpoint(createRoute({
744
+ operationId: "listActiveSubscriptions",
745
+ method: "get",
746
+ path: "/subscription/list",
747
+ middleware: [sessionMiddleware(), referenceMiddleware("list-subscription")],
748
+ request: req().qry(z.object({ referenceId: z.string().meta({ description: "Reference id of the subscription to list. Eg: '123'" }).optional() })).bld(),
749
+ responses: res(z.array(z.looseObject({}))).err(400).err(401).bld()
750
+ }), (_authOptions) => async (ctx) => {
751
+ if (!options.subscription?.enabled) {
752
+ logger.error(`Subscription actions are not enabled in your stripe plugin config.`);
753
+ return ctx.json({ success: False }, 400);
754
+ }
755
+ const session = ctx.get("session");
756
+ const { referenceId = session.user.id } = ctx.req.valid("query");
757
+ const subscriptions$1 = await ctx.get("context").adapter.findMany({
758
+ model: "subscription",
759
+ where: [{
760
+ field: "referenceId",
761
+ value: referenceId
762
+ }]
763
+ });
764
+ if (!subscriptions$1.length) return ctx.json([], 200);
765
+ const plans = await getPlans(options.subscription);
766
+ if (!plans) return ctx.json([], 200);
767
+ const subs = subscriptions$1.map((sub) => {
768
+ const plan = plans.find((p) => p.name.toLowerCase() === sub.plan.toLowerCase());
769
+ return {
770
+ ...sub,
771
+ limits: plan?.limits,
772
+ priceId: plan?.priceId
773
+ };
774
+ }).filter((sub) => sub.status === "active" || sub.status === "trialing");
775
+ return ctx.json(subs, 200);
776
+ }),
777
+ subscriptionSuccess: createEndpoint(createRoute({
778
+ operationId: "subscriptionSuccess",
779
+ method: "get",
780
+ path: "/subscription/success",
781
+ middleware: [originCheck((ctx) => ctx.req.query("callbackURL"))],
782
+ request: req().qry(z.object({
783
+ callbackURL: callbackURLSchema(true),
784
+ subscriptionId: z.string().optional()
785
+ })).bld(),
786
+ responses: res().rdr().bld()
787
+ }), (authOptions) => async (ctx) => {
788
+ const { callbackURL, subscriptionId } = ctx.req.valid("query");
789
+ if (!callbackURL || !subscriptionId) return ctx.redirect(getUrl(authOptions, callbackURL || "/"), 302);
790
+ const session = await getSessionFromCtx(ctx);
791
+ if (session instanceof Response) return ctx.redirect(getUrl(authOptions, callbackURL || "/"), 302);
792
+ const subscription = await ctx.get("context").adapter.findOne({
793
+ model: "subscription",
794
+ where: [{
795
+ field: "id",
796
+ value: subscriptionId
797
+ }]
798
+ });
799
+ if (subscription?.status === "active" || subscription?.status === "trialing") return ctx.redirect(getUrl(authOptions, callbackURL), 302);
800
+ const customerId = subscription?.stripeCustomerId || session.user.stripeCustomerId;
801
+ if (customerId) try {
802
+ const stripeSubscription = await client.subscriptions.list({
803
+ customer: customerId,
804
+ status: "active"
805
+ }).then((res$1) => res$1.data[0]);
806
+ if (stripeSubscription) {
807
+ const firstItem = stripeSubscription.items.data[0];
808
+ if (firstItem) {
809
+ const plan = await getPlanByPriceInfo(options, firstItem.price.id, firstItem.price.lookup_key);
810
+ if (plan && subscription) await ctx.get("context").adapter.update({
811
+ model: "subscription",
812
+ update: {
813
+ status: stripeSubscription.status,
814
+ seats: firstItem.quantity || 1,
815
+ plan: plan.name.toLowerCase(),
816
+ periodEnd: /* @__PURE__ */ new Date(firstItem.current_period_end * 1e3),
817
+ periodStart: /* @__PURE__ */ new Date(firstItem.current_period_start * 1e3),
818
+ stripeSubscriptionId: stripeSubscription.id,
819
+ ...stripeSubscription.trial_start && stripeSubscription.trial_end ? {
820
+ trialStart: /* @__PURE__ */ new Date(stripeSubscription.trial_start * 1e3),
821
+ trialEnd: /* @__PURE__ */ new Date(stripeSubscription.trial_end * 1e3)
822
+ } : {}
823
+ },
824
+ where: [{
825
+ field: "id",
826
+ value: subscription.id
827
+ }]
828
+ });
829
+ }
830
+ }
831
+ } catch (error) {
832
+ ctx.get("context").logger.error("Error fetching subscription from Stripe", error);
833
+ }
834
+ return ctx.redirect(getUrl(authOptions, callbackURL), 302);
835
+ }),
836
+ createBillingPortal: createEndpoint(createRoute({
837
+ operationId: "createBillingPortal",
838
+ method: "post",
839
+ path: "/subscription/billing-portal",
840
+ middleware: [
841
+ sessionMiddleware(),
842
+ originCheck((ctx) => ctx.req.raw.clone().json().then((v) => v.returnUrl)),
843
+ referenceMiddleware("billing-portal")
844
+ ],
845
+ request: req().bdy(z.object({
846
+ locale: z.custom((localization) => {
847
+ return typeof localization === "string";
848
+ }).optional(),
849
+ referenceId: z.string().optional(),
850
+ returnUrl: z.string().default("/")
851
+ })).bld(),
852
+ responses: res(z.object({
853
+ url: z.string(),
854
+ redirect: z.literal(true)
855
+ })).err(400).err(401).bld()
856
+ }), (authOptions) => async (ctx) => {
857
+ const session = ctx.get("session");
858
+ const { referenceId = session.user.id, locale, returnUrl } = ctx.req.valid("json");
859
+ let customerId = session.user.stripeCustomerId;
860
+ if (!customerId) customerId = (await ctx.get("context").adapter.findMany({
861
+ model: "subscription",
862
+ where: [{
863
+ field: "referenceId",
864
+ value: referenceId
865
+ }]
866
+ }).then((subs) => subs.find((sub) => sub.status === "active" || sub.status === "trialing")))?.stripeCustomerId;
867
+ if (!customerId) return ctx.json({
868
+ success: False,
869
+ message: "No Stripe customer found for this user"
870
+ }, 400);
871
+ try {
872
+ const { url } = await client.billingPortal.sessions.create({
873
+ customer: customerId,
874
+ return_url: getUrl(authOptions, returnUrl),
875
+ ...locale && { locale }
876
+ });
877
+ return ctx.json({
878
+ url,
879
+ redirect: True
880
+ }, 200);
881
+ } catch (error) {
882
+ ctx.get("context").logger.error("Error creating billing portal session", error);
883
+ return ctx.json({
884
+ success: False,
885
+ message: error.message
886
+ }, 400);
887
+ }
888
+ })
889
+ };
890
+ return {
891
+ id: "stripe",
892
+ routes: {
893
+ stripeWebhook: createEndpoint(createRoute({
894
+ operationId: "stripeWebhook",
895
+ method: "post",
896
+ path: "/stripe/webhook",
897
+ client: false,
898
+ responses: res(z.object({ success: z.literal(true) })).err(400).err(500).bld()
899
+ }), (_authOptions) => async (ctx) => {
900
+ if (!ctx.req.raw?.body) return ctx.json({ success: False }, 500);
901
+ const buf = await ctx.req.text();
902
+ const sig = ctx.req.header("stripe-signature");
903
+ const webhookSecret = options.stripeWebhookSecret;
904
+ let event;
905
+ try {
906
+ if (!sig || !webhookSecret) return ctx.json({
907
+ success: False,
908
+ message: "Stripe webhook secret not found"
909
+ }, 400);
910
+ event = await client.webhooks.constructEventAsync(buf, sig, webhookSecret);
911
+ } catch (err) {
912
+ ctx.get("context").logger.error(`${err.message}`);
913
+ return ctx.json({
914
+ success: False,
915
+ message: `Webhook Error: ${err.message}`
916
+ }, 400);
917
+ }
918
+ try {
919
+ switch (event.type) {
920
+ case "checkout.session.completed":
921
+ await onCheckoutSessionCompleted(ctx, options, event);
922
+ await options.onEvent?.(event);
923
+ break;
924
+ case "customer.subscription.updated":
925
+ await onSubscriptionUpdated(ctx, options, event);
926
+ await options.onEvent?.(event);
927
+ break;
928
+ case "customer.subscription.deleted":
929
+ await onSubscriptionDeleted(ctx, options, event);
930
+ await options.onEvent?.(event);
931
+ break;
932
+ default:
933
+ await options.onEvent?.(event);
934
+ break;
935
+ }
936
+ } catch (e) {
937
+ ctx.get("context").logger.error(`Stripe webhook failed. Error: ${e.message}`);
938
+ return ctx.json({
939
+ success: False,
940
+ message: "Webhook error: See server logs for more information."
941
+ }, 400);
942
+ }
943
+ return ctx.json({ success: True }, 200);
944
+ }),
945
+ ...options.subscription?.enabled ? subscriptionEndpoints : {}
946
+ },
947
+ init(ctx) {
948
+ return { options: { databaseHooks: { user: { create: { async after(user$1, ctx$1) {
949
+ if (ctx$1 && options.createCustomerOnSignUp) {
950
+ const stripeCustomer = await client.customers.create({
951
+ email: user$1.email,
952
+ ...user$1.name && { name: user$1.name },
953
+ metadata: { userId: user$1.id }
954
+ });
955
+ if (!await ctx$1.get("context").internalAdapter.updateUser(user$1.id, { stripeCustomerId: stripeCustomer.id })) logger.error("#FAIRE_AUTH: Failed to create customer");
956
+ else if (options.onCustomerCreate) await options.onCustomerCreate({
957
+ stripeCustomer,
958
+ user: user$1
959
+ }, ctx$1);
960
+ }
961
+ } } } } } };
962
+ },
963
+ schema: getSchema(options)
964
+ };
965
+ };
966
+
967
+ //#endregion
968
+ export { stripe };