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