@better-auth/stripe 1.7.0-beta.1 → 1.7.0-beta.10

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/client.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { l as StripePlan, n as stripe } from "./index-C8Utp7ig.mjs";
1
+ import { l as StripePlan, n as stripe } from "./index-XU8GMPhh.mjs";
2
2
  import * as better_auth0 from "better-auth";
3
3
 
4
4
  //#region src/error-codes.d.ts
package/dist/client.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { n as STRIPE_ERROR_CODES, t as PACKAGE_VERSION } from "./version-Bf-a0R3x.mjs";
1
+ import { n as STRIPE_ERROR_CODES, t as PACKAGE_VERSION } from "./version-Cup6oezK.mjs";
2
2
  //#region src/client.ts
3
3
  const stripeClient = (options) => {
4
4
  return {
@@ -341,21 +341,24 @@ type SubscriptionOptions = {
341
341
  plan: StripePlan;
342
342
  }, ctx: GenericEndpointContext) => Promise<void>) | undefined;
343
343
  /**
344
- * A callback to run after a user is about to cancel their subscription
344
+ * A callback to run on every subscription update webhook. Use `stripeSubscription`
345
+ * to read fields that are not persisted in the local subscription row.
345
346
  * @returns
346
347
  */
347
348
  onSubscriptionUpdate?: ((data: {
348
349
  event: Stripe.Event;
350
+ stripeSubscription: Stripe.Subscription;
349
351
  subscription: Subscription;
350
352
  }) => Promise<void>) | undefined;
351
353
  /**
352
- * A callback to run after a user is about to cancel their subscription
354
+ * A callback to run once when a subscription transitions into a pending-cancel state
355
+ * (e.g. `cancel_at_period_end` or a scheduled `cancel_at`).
353
356
  * @returns
354
357
  */
355
358
  onSubscriptionCancel?: ((data: {
356
- event?: Stripe.Event;
357
- subscription: Subscription;
359
+ event: Stripe.Event;
358
360
  stripeSubscription: Stripe.Subscription;
361
+ subscription: Subscription;
359
362
  cancellationDetails?: Stripe.Subscription.CancellationDetails | null;
360
363
  }) => Promise<void>) | undefined;
361
364
  /**
@@ -573,6 +576,8 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
573
576
  }>)[];
574
577
  }>) => Promise<{
575
578
  session: StripeCtxSession;
579
+ }>) | ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<{
580
+ referenceId: string;
576
581
  }>) | ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<void>))[];
577
582
  }, {
578
583
  url: string;
@@ -634,6 +639,8 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
634
639
  }>)[];
635
640
  }>) => Promise<{
636
641
  session: StripeCtxSession;
642
+ }>) | ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<{
643
+ referenceId: string;
637
644
  }>) | ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<void>))[];
638
645
  }, {
639
646
  url: string;
@@ -680,7 +687,9 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
680
687
  }>)[];
681
688
  }>) => Promise<{
682
689
  session: StripeCtxSession;
683
- }>) | ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<void>))[];
690
+ }>) | ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<{
691
+ referenceId: string;
692
+ }>))[];
684
693
  }, Stripe.Response<Stripe.Subscription>>;
685
694
  listActiveSubscriptions: better_call0.StrictEndpoint<"/subscription/list", {
686
695
  method: "GET";
@@ -722,7 +731,9 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
722
731
  }>)[];
723
732
  }>) => Promise<{
724
733
  session: StripeCtxSession;
725
- }>) | ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<void>))[];
734
+ }>) | ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<{
735
+ referenceId: string;
736
+ }>))[];
726
737
  }, {
727
738
  limits: Record<string, unknown> | undefined;
728
739
  priceId: string | undefined;
@@ -798,6 +809,8 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
798
809
  }>)[];
799
810
  }>) => Promise<{
800
811
  session: StripeCtxSession;
812
+ }>) | ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<{
813
+ referenceId: string;
801
814
  }>) | ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<void>))[];
802
815
  }, {
803
816
  url: string;
package/dist/index.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { a as CheckoutSessionLocale, c as StripeOptions, d as SubscriptionOptions, f as WithActiveOrganizationId, i as CheckoutSessionLineItem, l as StripePlan, n as stripe, o as CustomerType, p as WithStripeCustomerId, r as AuthorizeReferenceAction, s as StripeCtxSession, t as StripePlugin, u as Subscription } from "./index-C8Utp7ig.mjs";
1
+ import { a as CheckoutSessionLocale, c as StripeOptions, d as SubscriptionOptions, f as WithActiveOrganizationId, i as CheckoutSessionLineItem, l as StripePlan, n as stripe, o as CustomerType, p as WithStripeCustomerId, r as AuthorizeReferenceAction, s as StripeCtxSession, t as StripePlugin, u as Subscription } from "./index-XU8GMPhh.mjs";
2
2
  export { AuthorizeReferenceAction, CheckoutSessionLineItem, CheckoutSessionLocale, CustomerType, StripeCtxSession, StripeOptions, StripePlan, StripePlugin, Subscription, SubscriptionOptions, WithActiveOrganizationId, WithStripeCustomerId, stripe };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { n as STRIPE_ERROR_CODES, t as PACKAGE_VERSION } from "./version-Bf-a0R3x.mjs";
1
+ import { n as STRIPE_ERROR_CODES, t as PACKAGE_VERSION } from "./version-Cup6oezK.mjs";
2
2
  import { APIError, HIDE_METADATA } from "better-auth";
3
3
  import { defu } from "defu";
4
4
  import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api";
@@ -91,13 +91,13 @@ function isStripePendingCancel(stripeSub) {
91
91
  }
92
92
  /**
93
93
  * Escapes a value for use in Stripe search queries.
94
- * Stripe search query uses double quotes for string values,
95
- * and double quotes within the value need to be escaped with backslash.
94
+ * Stripe search query uses backslash escaping inside double-quoted
95
+ * string values, so literal backslashes must be escaped before quotes.
96
96
  *
97
97
  * @see https://docs.stripe.com/search#search-query-language
98
98
  */
99
99
  function escapeStripeSearchValue(value) {
100
- return value.replace(/"/g, "\\\"");
100
+ return value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"");
101
101
  }
102
102
  /**
103
103
  * Resolve the quantity for a subscription by checking the seat item first,
@@ -234,13 +234,13 @@ async function onCheckoutSessionCompleted(ctx, options, event) {
234
234
  async function onSubscriptionCreated(ctx, options, event) {
235
235
  try {
236
236
  if (!options.subscription?.enabled) return;
237
- const subscriptionCreated = event.data.object;
238
- const stripeCustomerId = subscriptionCreated.customer?.toString();
237
+ const stripeSubscriptionCreated = event.data.object;
238
+ const stripeCustomerId = stripeSubscriptionCreated.customer?.toString();
239
239
  if (!stripeCustomerId) {
240
240
  ctx.context.logger.warn(`Stripe webhook warning: customer.subscription.created event received without customer ID`);
241
241
  return;
242
242
  }
243
- const { subscriptionId } = subscriptionMetadata.get(subscriptionCreated.metadata);
243
+ const { subscriptionId } = subscriptionMetadata.get(stripeSubscriptionCreated.metadata);
244
244
  const existingSubscription = await ctx.context.adapter.findOne({
245
245
  model: "subscription",
246
246
  where: subscriptionId ? [{
@@ -248,7 +248,7 @@ async function onSubscriptionCreated(ctx, options, event) {
248
248
  value: subscriptionId
249
249
  }] : [{
250
250
  field: "stripeSubscriptionId",
251
- value: subscriptionCreated.id
251
+ value: stripeSubscriptionCreated.id
252
252
  }]
253
253
  });
254
254
  if (existingSubscription) {
@@ -261,9 +261,9 @@ async function onSubscriptionCreated(ctx, options, event) {
261
261
  return;
262
262
  }
263
263
  const { referenceId, customerType } = reference;
264
- const resolved = await resolvePlanItem(options, subscriptionCreated.items.data);
264
+ const resolved = await resolvePlanItem(options, stripeSubscriptionCreated.items.data);
265
265
  if (!resolved) {
266
- ctx.context.logger.warn(`Stripe webhook warning: Subscription ${subscriptionCreated.id} has no items matching a configured plan`);
266
+ ctx.context.logger.warn(`Stripe webhook warning: Subscription ${stripeSubscriptionCreated.id} has no items matching a configured plan`);
267
267
  return;
268
268
  }
269
269
  const { item: subscriptionItem, plan } = resolved;
@@ -271,12 +271,12 @@ async function onSubscriptionCreated(ctx, options, event) {
271
271
  ctx.context.logger.warn(`Stripe webhook warning: No matching plan found for priceId: ${subscriptionItem.price.id}`);
272
272
  return;
273
273
  }
274
- const seats = resolveQuantity(subscriptionCreated.items.data, subscriptionItem, plan.seatPriceId);
274
+ const seats = resolveQuantity(stripeSubscriptionCreated.items.data, subscriptionItem, plan.seatPriceId);
275
275
  const periodStart = /* @__PURE__ */ new Date(subscriptionItem.current_period_start * 1e3);
276
276
  const periodEnd = /* @__PURE__ */ new Date(subscriptionItem.current_period_end * 1e3);
277
- const trial = subscriptionCreated.trial_start && subscriptionCreated.trial_end ? {
278
- trialStart: /* @__PURE__ */ new Date(subscriptionCreated.trial_start * 1e3),
279
- trialEnd: /* @__PURE__ */ new Date(subscriptionCreated.trial_end * 1e3)
277
+ const trial = stripeSubscriptionCreated.trial_start && stripeSubscriptionCreated.trial_end ? {
278
+ trialStart: /* @__PURE__ */ new Date(stripeSubscriptionCreated.trial_start * 1e3),
279
+ trialEnd: /* @__PURE__ */ new Date(stripeSubscriptionCreated.trial_end * 1e3)
280
280
  } : {};
281
281
  const newSubscription = await ctx.context.adapter.create({
282
282
  model: "subscription",
@@ -285,8 +285,8 @@ async function onSubscriptionCreated(ctx, options, event) {
285
285
  ...plan.limits ? { limits: plan.limits } : {},
286
286
  referenceId,
287
287
  stripeCustomerId,
288
- stripeSubscriptionId: subscriptionCreated.id,
289
- status: subscriptionCreated.status,
288
+ stripeSubscriptionId: stripeSubscriptionCreated.id,
289
+ status: stripeSubscriptionCreated.status,
290
290
  plan: plan.name.toLowerCase(),
291
291
  periodStart,
292
292
  periodEnd,
@@ -294,11 +294,11 @@ async function onSubscriptionCreated(ctx, options, event) {
294
294
  billingInterval: subscriptionItem.price.recurring?.interval
295
295
  }
296
296
  });
297
- ctx.context.logger.info(`Stripe webhook: Created subscription ${subscriptionCreated.id} for ${customerType} ${referenceId} from dashboard`);
297
+ ctx.context.logger.info(`Stripe webhook: Created subscription ${stripeSubscriptionCreated.id} for ${customerType} ${referenceId} from dashboard`);
298
298
  await options.subscription.onSubscriptionCreated?.({
299
299
  event,
300
300
  subscription: newSubscription,
301
- stripeSubscription: subscriptionCreated,
301
+ stripeSubscription: stripeSubscriptionCreated,
302
302
  plan
303
303
  });
304
304
  } catch (error) {
@@ -308,15 +308,15 @@ async function onSubscriptionCreated(ctx, options, event) {
308
308
  async function onSubscriptionUpdated(ctx, options, event) {
309
309
  try {
310
310
  if (!options.subscription?.enabled) return;
311
- const subscriptionUpdated = event.data.object;
312
- const resolved = await resolvePlanItem(options, subscriptionUpdated.items.data);
311
+ const stripeSubscriptionUpdated = event.data.object;
312
+ const resolved = await resolvePlanItem(options, stripeSubscriptionUpdated.items.data);
313
313
  if (!resolved) {
314
- ctx.context.logger.warn(`Stripe webhook warning: Subscription ${subscriptionUpdated.id} has no items matching a configured plan`);
314
+ ctx.context.logger.warn(`Stripe webhook warning: Subscription ${stripeSubscriptionUpdated.id} has no items matching a configured plan`);
315
315
  return;
316
316
  }
317
317
  const { item: subscriptionItem, plan } = resolved;
318
- const { subscriptionId } = subscriptionMetadata.get(subscriptionUpdated.metadata);
319
- const customerId = subscriptionUpdated.customer?.toString();
318
+ const { subscriptionId } = subscriptionMetadata.get(stripeSubscriptionUpdated.metadata);
319
+ const customerId = stripeSubscriptionUpdated.customer?.toString();
320
320
  let subscription = await ctx.context.adapter.findOne({
321
321
  model: "subscription",
322
322
  where: subscriptionId ? [{
@@ -324,7 +324,7 @@ async function onSubscriptionUpdated(ctx, options, event) {
324
324
  value: subscriptionId
325
325
  }] : [{
326
326
  field: "stripeSubscriptionId",
327
- value: subscriptionUpdated.id
327
+ value: stripeSubscriptionUpdated.id
328
328
  }]
329
329
  });
330
330
  if (!subscription) {
@@ -344,12 +344,12 @@ async function onSubscriptionUpdated(ctx, options, event) {
344
344
  subscription = activeSub;
345
345
  } else subscription = subs[0];
346
346
  }
347
- const seats = plan ? resolveQuantity(subscriptionUpdated.items.data, subscriptionItem, plan.seatPriceId) : subscriptionItem.quantity;
348
- const trial = subscriptionUpdated.trial_start && subscriptionUpdated.trial_end ? {
349
- trialStart: /* @__PURE__ */ new Date(subscriptionUpdated.trial_start * 1e3),
350
- trialEnd: /* @__PURE__ */ new Date(subscriptionUpdated.trial_end * 1e3)
347
+ const seats = plan ? resolveQuantity(stripeSubscriptionUpdated.items.data, subscriptionItem, plan.seatPriceId) : subscriptionItem.quantity;
348
+ const trial = stripeSubscriptionUpdated.trial_start && stripeSubscriptionUpdated.trial_end ? {
349
+ trialStart: /* @__PURE__ */ new Date(stripeSubscriptionUpdated.trial_start * 1e3),
350
+ trialEnd: /* @__PURE__ */ new Date(stripeSubscriptionUpdated.trial_end * 1e3)
351
351
  } : {};
352
- const updatedSubscription = await ctx.context.adapter.update({
352
+ const subscriptionUpdated = await ctx.context.adapter.update({
353
353
  model: "subscription",
354
354
  update: {
355
355
  ...trial,
@@ -358,36 +358,41 @@ async function onSubscriptionUpdated(ctx, options, event) {
358
358
  limits: plan.limits
359
359
  } : {},
360
360
  updatedAt: /* @__PURE__ */ new Date(),
361
- status: subscriptionUpdated.status,
361
+ status: stripeSubscriptionUpdated.status,
362
362
  periodStart: /* @__PURE__ */ new Date(subscriptionItem.current_period_start * 1e3),
363
363
  periodEnd: /* @__PURE__ */ new Date(subscriptionItem.current_period_end * 1e3),
364
- cancelAtPeriodEnd: subscriptionUpdated.cancel_at_period_end,
365
- cancelAt: subscriptionUpdated.cancel_at ? /* @__PURE__ */ new Date(subscriptionUpdated.cancel_at * 1e3) : null,
366
- canceledAt: subscriptionUpdated.canceled_at ? /* @__PURE__ */ new Date(subscriptionUpdated.canceled_at * 1e3) : null,
367
- endedAt: subscriptionUpdated.ended_at ? /* @__PURE__ */ new Date(subscriptionUpdated.ended_at * 1e3) : null,
364
+ cancelAtPeriodEnd: stripeSubscriptionUpdated.cancel_at_period_end,
365
+ cancelAt: stripeSubscriptionUpdated.cancel_at ? /* @__PURE__ */ new Date(stripeSubscriptionUpdated.cancel_at * 1e3) : null,
366
+ canceledAt: stripeSubscriptionUpdated.canceled_at ? /* @__PURE__ */ new Date(stripeSubscriptionUpdated.canceled_at * 1e3) : null,
367
+ endedAt: stripeSubscriptionUpdated.ended_at ? /* @__PURE__ */ new Date(stripeSubscriptionUpdated.ended_at * 1e3) : null,
368
368
  seats,
369
- stripeSubscriptionId: subscriptionUpdated.id,
369
+ stripeSubscriptionId: stripeSubscriptionUpdated.id,
370
370
  billingInterval: subscriptionItem.price.recurring?.interval,
371
- stripeScheduleId: subscriptionUpdated.schedule ? typeof subscriptionUpdated.schedule === "string" ? subscriptionUpdated.schedule : subscriptionUpdated.schedule.id : null
371
+ stripeScheduleId: stripeSubscriptionUpdated.schedule ? typeof stripeSubscriptionUpdated.schedule === "string" ? stripeSubscriptionUpdated.schedule : stripeSubscriptionUpdated.schedule.id : null
372
372
  },
373
373
  where: [{
374
374
  field: "id",
375
375
  value: subscription.id
376
376
  }]
377
377
  });
378
- if (subscriptionUpdated.status === "active" && isStripePendingCancel(subscriptionUpdated) && !isPendingCancel(subscription)) await options.subscription.onSubscriptionCancel?.({
379
- subscription,
380
- cancellationDetails: subscriptionUpdated.cancellation_details || void 0,
381
- stripeSubscription: subscriptionUpdated,
382
- event
378
+ if (!subscriptionUpdated) {
379
+ ctx.context.logger.warn(`Stripe webhook warning: Subscription ${subscription.id} update returned no row (likely deleted concurrently), skipping callbacks`);
380
+ return;
381
+ }
382
+ if (stripeSubscriptionUpdated.status === "active" && isStripePendingCancel(stripeSubscriptionUpdated) && !isPendingCancel(subscription)) await options.subscription.onSubscriptionCancel?.({
383
+ event,
384
+ subscription: subscriptionUpdated,
385
+ stripeSubscription: stripeSubscriptionUpdated,
386
+ cancellationDetails: stripeSubscriptionUpdated.cancellation_details || void 0
383
387
  });
384
388
  await options.subscription.onSubscriptionUpdate?.({
385
389
  event,
386
- subscription: updatedSubscription || subscription
390
+ subscription: subscriptionUpdated,
391
+ stripeSubscription: stripeSubscriptionUpdated
387
392
  });
388
393
  if (plan) {
389
- if (subscriptionUpdated.status === "active" && subscription.status === "trialing" && plan.freeTrial?.onTrialEnd) await plan.freeTrial.onTrialEnd({ subscription }, ctx);
390
- if (subscriptionUpdated.status === "incomplete_expired" && subscription.status === "trialing" && plan.freeTrial?.onTrialExpired) await plan.freeTrial.onTrialExpired(subscription, ctx);
394
+ if (stripeSubscriptionUpdated.status === "active" && subscription.status === "trialing" && plan.freeTrial?.onTrialEnd) await plan.freeTrial.onTrialEnd({ subscription: subscriptionUpdated }, ctx);
395
+ if (stripeSubscriptionUpdated.status === "incomplete_expired" && subscription.status === "trialing" && plan.freeTrial?.onTrialExpired) await plan.freeTrial.onTrialExpired(subscriptionUpdated, ctx);
391
396
  }
392
397
  } catch (error) {
393
398
  ctx.context.logger.error(`Stripe webhook failed. Error: ${error}`);
@@ -396,8 +401,8 @@ async function onSubscriptionUpdated(ctx, options, event) {
396
401
  async function onSubscriptionDeleted(ctx, options, event) {
397
402
  if (!options.subscription?.enabled) return;
398
403
  try {
399
- const subscriptionDeleted = event.data.object;
400
- const subscriptionId = subscriptionDeleted.id;
404
+ const stripeSubscriptionDeleted = event.data.object;
405
+ const subscriptionId = stripeSubscriptionDeleted.id;
401
406
  const subscription = await ctx.context.adapter.findOne({
402
407
  model: "subscription",
403
408
  where: [{
@@ -406,11 +411,11 @@ async function onSubscriptionDeleted(ctx, options, event) {
406
411
  }]
407
412
  });
408
413
  if (subscription) {
409
- const trial = subscriptionDeleted.trial_start && subscriptionDeleted.trial_end ? {
410
- trialStart: /* @__PURE__ */ new Date(subscriptionDeleted.trial_start * 1e3),
411
- trialEnd: /* @__PURE__ */ new Date(subscriptionDeleted.trial_end * 1e3)
414
+ const trial = stripeSubscriptionDeleted.trial_start && stripeSubscriptionDeleted.trial_end ? {
415
+ trialStart: /* @__PURE__ */ new Date(stripeSubscriptionDeleted.trial_start * 1e3),
416
+ trialEnd: /* @__PURE__ */ new Date(stripeSubscriptionDeleted.trial_end * 1e3)
412
417
  } : {};
413
- await ctx.context.adapter.update({
418
+ const subscriptionUpdated = await ctx.context.adapter.update({
414
419
  model: "subscription",
415
420
  where: [{
416
421
  field: "id",
@@ -420,17 +425,21 @@ async function onSubscriptionDeleted(ctx, options, event) {
420
425
  ...trial,
421
426
  status: "canceled",
422
427
  updatedAt: /* @__PURE__ */ new Date(),
423
- cancelAtPeriodEnd: subscriptionDeleted.cancel_at_period_end,
424
- cancelAt: subscriptionDeleted.cancel_at ? /* @__PURE__ */ new Date(subscriptionDeleted.cancel_at * 1e3) : null,
425
- canceledAt: subscriptionDeleted.canceled_at ? /* @__PURE__ */ new Date(subscriptionDeleted.canceled_at * 1e3) : null,
426
- endedAt: subscriptionDeleted.ended_at ? /* @__PURE__ */ new Date(subscriptionDeleted.ended_at * 1e3) : null,
428
+ cancelAtPeriodEnd: stripeSubscriptionDeleted.cancel_at_period_end,
429
+ cancelAt: stripeSubscriptionDeleted.cancel_at ? /* @__PURE__ */ new Date(stripeSubscriptionDeleted.cancel_at * 1e3) : null,
430
+ canceledAt: stripeSubscriptionDeleted.canceled_at ? /* @__PURE__ */ new Date(stripeSubscriptionDeleted.canceled_at * 1e3) : null,
431
+ endedAt: stripeSubscriptionDeleted.ended_at ? /* @__PURE__ */ new Date(stripeSubscriptionDeleted.ended_at * 1e3) : null,
427
432
  stripeScheduleId: null
428
433
  }
429
434
  });
435
+ if (!subscriptionUpdated) {
436
+ ctx.context.logger.warn(`Stripe webhook warning: Subscription ${subscription.id} update returned no row (likely deleted concurrently), skipping callbacks`);
437
+ return;
438
+ }
430
439
  await options.subscription.onSubscriptionDeleted?.({
431
440
  event,
432
- stripeSubscription: subscriptionDeleted,
433
- subscription
441
+ stripeSubscription: stripeSubscriptionDeleted,
442
+ subscription: subscriptionUpdated
434
443
  });
435
444
  } else ctx.context.logger.warn(`Stripe webhook error: Subscription not found for subscriptionId: ${subscriptionId}`);
436
445
  } catch (error) {
@@ -442,12 +451,14 @@ async function onSubscriptionDeleted(ctx, options, event) {
442
451
  const stripeSessionMiddleware = createAuthMiddleware({ use: [sessionMiddleware] }, async (ctx) => {
443
452
  return { session: ctx.context.session };
444
453
  });
445
- const referenceMiddleware = (subscriptionOptions, action) => createAuthMiddleware(async (ctx) => {
454
+ const referenceMiddleware = (options, action) => createAuthMiddleware(async (ctx) => {
455
+ const subscriptionOptions = options.subscription;
446
456
  const ctxSession = ctx.context.session;
447
457
  if (!ctxSession) throw APIError$1.from("UNAUTHORIZED", STRIPE_ERROR_CODES.UNAUTHORIZED);
448
458
  const customerType = ctx.body?.customerType || ctx.query?.customerType;
449
459
  const explicitReferenceId = ctx.body?.referenceId || ctx.query?.referenceId;
450
460
  if (customerType === "organization") {
461
+ if (!options.organization?.enabled) throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.ORGANIZATION_SUBSCRIPTION_NOT_ENABLED);
451
462
  if (!subscriptionOptions.authorizeReference) {
452
463
  ctx.context.logger.error(`Organization subscriptions require authorizeReference to be defined in your stripe plugin config.`);
453
464
  throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.AUTHORIZE_REFERENCE_REQUIRED);
@@ -460,10 +471,10 @@ const referenceMiddleware = (subscriptionOptions, action) => createAuthMiddlewar
460
471
  referenceId,
461
472
  action
462
473
  }, ctx)) throw APIError$1.from("UNAUTHORIZED", STRIPE_ERROR_CODES.UNAUTHORIZED);
463
- return;
474
+ return { referenceId };
464
475
  }
465
- if (!explicitReferenceId) return;
466
- if (explicitReferenceId === ctxSession.user.id) return;
476
+ const referenceId = explicitReferenceId || ctxSession.user.id;
477
+ if (!explicitReferenceId || explicitReferenceId === ctxSession.user.id) return { referenceId };
467
478
  if (!subscriptionOptions.authorizeReference) {
468
479
  ctx.context.logger.error(`Passing referenceId into a subscription action isn't allowed if subscription.authorizeReference isn't defined in your stripe plugin config.`);
469
480
  throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.REFERENCE_ID_NOT_ALLOWED);
@@ -471,9 +482,10 @@ const referenceMiddleware = (subscriptionOptions, action) => createAuthMiddlewar
471
482
  if (!await subscriptionOptions.authorizeReference({
472
483
  user: ctxSession.user,
473
484
  session: ctxSession.session,
474
- referenceId: explicitReferenceId,
485
+ referenceId,
475
486
  action
476
487
  }, ctx)) throw APIError$1.from("UNAUTHORIZED", STRIPE_ERROR_CODES.UNAUTHORIZED);
488
+ return { referenceId };
477
489
  });
478
490
  //#endregion
479
491
  //#region src/routes.ts
@@ -511,19 +523,21 @@ function getUrl(ctx, url) {
511
523
  return `${ctx.context.options.baseURL}${url.startsWith("/") ? url : `/${url}`}`;
512
524
  }
513
525
  /**
514
- * Determines the reference ID based on customer type.
515
- * - `user` (default): uses userId
516
- * - `organization`: uses activeOrganizationId from session
526
+ * Retrieve the subscription a row points to, or `null` if it no longer exists
527
+ * (`resource_missing`) so callers can treat the row as stale.
517
528
  * @internal
518
529
  */
519
- function getReferenceId(ctxSession, customerType, options) {
520
- const { user, session } = ctxSession;
521
- if ((customerType || "user") === "organization") {
522
- if (!options.organization?.enabled) throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.ORGANIZATION_SUBSCRIPTION_NOT_ENABLED);
523
- if (!session.activeOrganizationId) throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.ORGANIZATION_NOT_FOUND);
524
- return session.activeOrganizationId;
525
- }
526
- return user.id;
530
+ async function retrieveStripeSubscription(client, ctx, stripeSubscriptionId) {
531
+ return await client.subscriptions.retrieve(stripeSubscriptionId).catch((e) => {
532
+ /**
533
+ * @see https://docs.stripe.com/error-codes
534
+ */
535
+ if (e?.code === "resource_missing") return null;
536
+ throw ctx.error("BAD_REQUEST", {
537
+ code: e.code,
538
+ message: e.message
539
+ });
540
+ });
527
541
  }
528
542
  const upgradeSubscriptionBodySchema = z.object({
529
543
  plan: z.string().meta({ description: "The name of the plan to upgrade to. Eg: \"pro\"" }),
@@ -566,15 +580,19 @@ const upgradeSubscription = (options) => {
566
580
  metadata: { openapi: { operationId: "upgradeSubscription" } },
567
581
  use: [
568
582
  stripeSessionMiddleware,
569
- referenceMiddleware(subscriptionOptions, "upgrade-subscription"),
583
+ referenceMiddleware(options, "upgrade-subscription"),
570
584
  originCheck((c) => {
571
- return [c.body.successUrl, c.body.cancelUrl];
585
+ return [
586
+ c.body.successUrl,
587
+ c.body.cancelUrl,
588
+ c.body.returnUrl
589
+ ];
572
590
  })
573
591
  ]
574
592
  }, async (ctx) => {
575
593
  const { user, session } = ctx.context.session;
576
594
  const customerType = ctx.body.customerType || "user";
577
- const referenceId = ctx.body.referenceId || getReferenceId(ctx.context.session, customerType, options);
595
+ const referenceId = ctx.context.referenceId;
578
596
  if (!user.emailVerified && subscriptionOptions.requireEmailVerification) throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.EMAIL_VERIFICATION_REQUIRED);
579
597
  const plan = await getPlanByName(options, ctx.body.plan);
580
598
  if (!plan) throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.SUBSCRIPTION_PLAN_NOT_FOUND);
@@ -666,6 +684,10 @@ const upgradeSubscription = (options) => {
666
684
  break;
667
685
  }
668
686
  }
687
+ if (stripeCustomer) {
688
+ const ownerId = customerMetadata.get(stripeCustomer.metadata).userId;
689
+ if (!!ownerId && ownerId !== user.id || !user.emailVerified) stripeCustomer = void 0;
690
+ }
669
691
  if (!stripeCustomer) stripeCustomer = await client.customers.create({
670
692
  email: user.email,
671
693
  name: user.name,
@@ -983,16 +1005,23 @@ const upgradeSubscription = (options) => {
983
1005
  })).some((s) => {
984
1006
  return !!(s.trialStart || s.trialEnd) || s.status === "trialing";
985
1007
  }) && plan.freeTrial ? { trial_period_days: plan.freeTrial.days } : void 0;
1008
+ const { mode: _mode, customer: _customer, customer_email: _customer_email, success_url: _success_url, cancel_url: _cancel_url, line_items: _line_items, client_reference_id: _client_reference_id, ...additionalParams } = params?.params ?? {};
986
1009
  const response = {
987
1010
  ...await client.checkout.sessions.create({
1011
+ ...additionalParams,
1012
+ mode: "subscription",
988
1013
  ...customerId ? {
989
1014
  customer: customerId,
990
- customer_update: customerType !== "user" ? { address: "auto" } : {
1015
+ customer_email: void 0,
1016
+ customer_update: additionalParams.customer_update ?? (customerType !== "user" ? { address: "auto" } : {
991
1017
  name: "auto",
992
1018
  address: "auto"
993
- }
994
- } : { customer_email: user.email },
995
- locale: ctx.body.locale,
1019
+ })
1020
+ } : {
1021
+ customer: void 0,
1022
+ customer_email: user.email
1023
+ },
1024
+ locale: ctx.body.locale ?? additionalParams.locale,
996
1025
  success_url: getUrl(ctx, `${ctx.context.baseURL}/subscription/success?callbackURL=${encodeURIComponent(ctx.body.successUrl)}&checkoutSessionId={CHECKOUT_SESSION_ID}`),
997
1026
  cancel_url: getUrl(ctx, ctx.body.cancelUrl),
998
1027
  line_items: [
@@ -1006,22 +1035,21 @@ const upgradeSubscription = (options) => {
1006
1035
  }] : [],
1007
1036
  ...plan.lineItems ?? []
1008
1037
  ],
1038
+ client_reference_id: referenceId,
1009
1039
  subscription_data: {
1010
1040
  ...freeTrial,
1041
+ ...additionalParams.subscription_data,
1011
1042
  metadata: subscriptionMetadata.set({
1012
1043
  userId: user.id,
1013
1044
  subscriptionId: subscription.id,
1014
1045
  referenceId
1015
- }, ctx.body.metadata, params?.params?.subscription_data?.metadata)
1046
+ }, ctx.body.metadata, additionalParams.subscription_data?.metadata)
1016
1047
  },
1017
- mode: "subscription",
1018
- client_reference_id: referenceId,
1019
- ...params?.params,
1020
1048
  metadata: subscriptionMetadata.set({
1021
1049
  userId: user.id,
1022
1050
  subscriptionId: subscription.id,
1023
1051
  referenceId
1024
- }, ctx.body.metadata, params?.params?.metadata)
1052
+ }, ctx.body.metadata, additionalParams.metadata)
1025
1053
  }, params?.options).catch(async (e) => {
1026
1054
  throw ctx.error("BAD_REQUEST", {
1027
1055
  message: e.message,
@@ -1057,19 +1085,17 @@ const cancelSubscriptionBodySchema = z.object({
1057
1085
  */
1058
1086
  const cancelSubscription = (options) => {
1059
1087
  const client = options.stripeClient;
1060
- const subscriptionOptions = options.subscription;
1061
1088
  return createAuthEndpoint("/subscription/cancel", {
1062
1089
  method: "POST",
1063
1090
  body: cancelSubscriptionBodySchema,
1064
1091
  metadata: { openapi: { operationId: "cancelSubscription" } },
1065
1092
  use: [
1066
1093
  stripeSessionMiddleware,
1067
- referenceMiddleware(subscriptionOptions, "cancel-subscription"),
1094
+ referenceMiddleware(options, "cancel-subscription"),
1068
1095
  originCheck((ctx) => ctx.body.returnUrl)
1069
1096
  ]
1070
1097
  }, async (ctx) => {
1071
- const customerType = ctx.body.customerType || "user";
1072
- const referenceId = ctx.body.referenceId || getReferenceId(ctx.context.session, customerType, options);
1098
+ const referenceId = ctx.context.referenceId;
1073
1099
  let subscription = ctx.body.subscriptionId ? await ctx.context.adapter.findOne({
1074
1100
  model: "subscription",
1075
1101
  where: [{
@@ -1085,23 +1111,18 @@ const cancelSubscription = (options) => {
1085
1111
  }).then((subs) => subs.find((sub) => isActiveOrTrialing(sub)));
1086
1112
  if (ctx.body.subscriptionId && subscription && subscription.referenceId !== referenceId) subscription = void 0;
1087
1113
  if (!subscription || !subscription.stripeCustomerId) throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND);
1088
- const activeSubscriptions = await client.subscriptions.list({ customer: subscription.stripeCustomerId }).then((res) => res.data.filter((sub) => isActiveOrTrialing(sub)));
1089
- if (!activeSubscriptions.length) {
1090
- /**
1091
- * If the subscription is not found, we need to delete the subscription
1092
- * from the database. This is a rare case and should not happen.
1093
- */
1094
- await ctx.context.adapter.deleteMany({
1114
+ if (!subscription.stripeSubscriptionId) throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND);
1115
+ const activeSubscription = await retrieveStripeSubscription(client, ctx, subscription.stripeSubscriptionId);
1116
+ if (!activeSubscription || !isActiveOrTrialing(activeSubscription)) {
1117
+ await ctx.context.adapter.delete({
1095
1118
  model: "subscription",
1096
1119
  where: [{
1097
- field: "referenceId",
1098
- value: referenceId
1120
+ field: "id",
1121
+ value: subscription.id
1099
1122
  }]
1100
1123
  });
1101
1124
  throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND);
1102
1125
  }
1103
- const activeSubscription = activeSubscriptions.find((sub) => sub.id === subscription.stripeSubscriptionId);
1104
- if (!activeSubscription) throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND);
1105
1126
  const { url } = await client.billingPortal.sessions.create({
1106
1127
  customer: subscription.stripeCustomerId,
1107
1128
  return_url: getUrl(ctx, ctx.body?.returnUrl || "/"),
@@ -1149,15 +1170,13 @@ const restoreSubscriptionBodySchema = z.object({
1149
1170
  });
1150
1171
  const restoreSubscription = (options) => {
1151
1172
  const client = options.stripeClient;
1152
- const subscriptionOptions = options.subscription;
1153
1173
  return createAuthEndpoint("/subscription/restore", {
1154
1174
  method: "POST",
1155
1175
  body: restoreSubscriptionBodySchema,
1156
1176
  metadata: { openapi: { operationId: "restoreSubscription" } },
1157
- use: [stripeSessionMiddleware, referenceMiddleware(subscriptionOptions, "restore-subscription")]
1177
+ use: [stripeSessionMiddleware, referenceMiddleware(options, "restore-subscription")]
1158
1178
  }, async (ctx) => {
1159
- const customerType = ctx.body.customerType || "user";
1160
- const referenceId = ctx.body.referenceId || getReferenceId(ctx.context.session, customerType, options);
1179
+ const referenceId = ctx.context.referenceId;
1161
1180
  let subscription = ctx.body.subscriptionId ? await ctx.context.adapter.findOne({
1162
1181
  model: "subscription",
1163
1182
  where: [{
@@ -1204,8 +1223,9 @@ const restoreSubscription = (options) => {
1204
1223
  const releasedSub = await client.subscriptions.retrieve(subscription.stripeSubscriptionId);
1205
1224
  return ctx.json(releasedSub);
1206
1225
  }
1207
- const activeSubscription = await client.subscriptions.list({ customer: subscription.stripeCustomerId }).then((res) => res.data.filter((sub) => isActiveOrTrialing(sub))[0]);
1208
- if (!activeSubscription) throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND);
1226
+ if (!subscription.stripeSubscriptionId) throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND);
1227
+ const activeSubscription = await retrieveStripeSubscription(client, ctx, subscription.stripeSubscriptionId);
1228
+ if (!activeSubscription || !isActiveOrTrialing(activeSubscription)) throw APIError$1.from("BAD_REQUEST", STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND);
1209
1229
  const updateParams = {};
1210
1230
  if (activeSubscription.cancel_at) updateParams.cancel_at = "";
1211
1231
  else if (activeSubscription.cancel_at_period_end) updateParams.cancel_at_period_end = false;
@@ -1251,15 +1271,13 @@ const listActiveSubscriptionsQuerySchema = z.optional(z.object({
1251
1271
  * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/stripe#api-method-subscription-list)
1252
1272
  */
1253
1273
  const listActiveSubscriptions = (options) => {
1254
- const subscriptionOptions = options.subscription;
1255
1274
  return createAuthEndpoint("/subscription/list", {
1256
1275
  method: "GET",
1257
1276
  query: listActiveSubscriptionsQuerySchema,
1258
1277
  metadata: { openapi: { operationId: "listActiveSubscriptions" } },
1259
- use: [stripeSessionMiddleware, referenceMiddleware(subscriptionOptions, "list-subscription")]
1278
+ use: [stripeSessionMiddleware, referenceMiddleware(options, "list-subscription")]
1260
1279
  }, async (ctx) => {
1261
- const customerType = ctx.query?.customerType || "user";
1262
- const referenceId = ctx.query?.referenceId || getReferenceId(ctx.context.session, customerType, options);
1280
+ const referenceId = ctx.context.referenceId;
1263
1281
  const subscriptions = await ctx.context.adapter.findMany({
1264
1282
  model: "subscription",
1265
1283
  where: [{
@@ -1292,8 +1310,7 @@ const subscriptionSuccess = (options) => {
1292
1310
  use: [originCheck((ctx) => ctx.query.callbackURL)]
1293
1311
  }, async (ctx) => {
1294
1312
  let callbackURL = ctx.query?.callbackURL || "/";
1295
- const session = await getSessionFromCtx(ctx);
1296
- if (!session) throw ctx.redirect(getUrl(ctx, callbackURL));
1313
+ if (!await getSessionFromCtx(ctx)) throw ctx.redirect(getUrl(ctx, callbackURL));
1297
1314
  if (!ctx.query?.checkoutSessionId) throw ctx.redirect(getUrl(ctx, callbackURL));
1298
1315
  /**
1299
1316
  * Replace the Stripe {CHECKOUT_SESSION_ID} template variable in callbackURL.
@@ -1322,12 +1339,9 @@ const subscriptionSuccess = (options) => {
1322
1339
  throw ctx.redirect(getUrl(ctx, callbackURL));
1323
1340
  }
1324
1341
  if (isActiveOrTrialing(subscription)) throw ctx.redirect(getUrl(ctx, callbackURL));
1325
- const customerId = subscription.stripeCustomerId || session.user.stripeCustomerId;
1326
- if (!customerId) throw ctx.redirect(getUrl(ctx, callbackURL));
1327
- const stripeSubscription = await client.subscriptions.list({
1328
- customer: customerId,
1329
- status: "active"
1330
- }).then((res) => res.data[0]).catch((error) => {
1342
+ const stripeSubscriptionId = typeof checkoutSession.subscription === "string" ? checkoutSession.subscription : checkoutSession.subscription?.id;
1343
+ if (!stripeSubscriptionId || checkoutSession.payment_status === "unpaid") throw ctx.redirect(getUrl(ctx, callbackURL));
1344
+ const stripeSubscription = await client.subscriptions.retrieve(stripeSubscriptionId).catch((error) => {
1331
1345
  ctx.context.logger.error("Error fetching subscription from Stripe", error);
1332
1346
  throw ctx.redirect(getUrl(ctx, callbackURL));
1333
1347
  });
@@ -1380,20 +1394,19 @@ const createBillingPortalBodySchema = z.object({
1380
1394
  });
1381
1395
  const createBillingPortal = (options) => {
1382
1396
  const client = options.stripeClient;
1383
- const subscriptionOptions = options.subscription;
1384
1397
  return createAuthEndpoint("/subscription/billing-portal", {
1385
1398
  method: "POST",
1386
1399
  body: createBillingPortalBodySchema,
1387
1400
  metadata: { openapi: { operationId: "createBillingPortal" } },
1388
1401
  use: [
1389
1402
  stripeSessionMiddleware,
1390
- referenceMiddleware(subscriptionOptions, "billing-portal"),
1403
+ referenceMiddleware(options, "billing-portal"),
1391
1404
  originCheck((ctx) => ctx.body.returnUrl)
1392
1405
  ]
1393
1406
  }, async (ctx) => {
1394
1407
  const { user } = ctx.context.session;
1395
1408
  const customerType = ctx.body.customerType || "user";
1396
- const referenceId = ctx.body.referenceId || getReferenceId(ctx.context.session, customerType, options);
1409
+ const referenceId = ctx.context.referenceId;
1397
1410
  let customerId;
1398
1411
  if (customerType === "organization") {
1399
1412
  customerId = (await ctx.context.adapter.findOne({
@@ -1650,12 +1663,11 @@ const stripe = (options) => {
1650
1663
  const { organization } = data;
1651
1664
  if (!organization.stripeCustomerId) return;
1652
1665
  try {
1653
- const subscriptions = await client.subscriptions.list({
1666
+ for await (const sub of client.subscriptions.list({
1654
1667
  customer: organization.stripeCustomerId,
1655
1668
  status: "all",
1656
1669
  limit: 100
1657
- });
1658
- for (const sub of subscriptions.data) if (sub.status !== "canceled" && sub.status !== "incomplete" && sub.status !== "incomplete_expired") throw APIError.from("BAD_REQUEST", STRIPE_ERROR_CODES.ORGANIZATION_HAS_ACTIVE_SUBSCRIPTION);
1670
+ })) if (sub.status !== "canceled" && sub.status !== "incomplete" && sub.status !== "incomplete_expired") throw APIError.from("BAD_REQUEST", STRIPE_ERROR_CODES.ORGANIZATION_HAS_ACTIVE_SUBSCRIPTION);
1659
1671
  } catch (error) {
1660
1672
  if (error instanceof APIError) throw error;
1661
1673
  ctx.logger.error(`Failed to check organization subscriptions: ${error.message}`);
@@ -1760,6 +1772,10 @@ const stripe = (options) => {
1760
1772
  break;
1761
1773
  }
1762
1774
  }
1775
+ if (stripeCustomer) {
1776
+ const ownerId = customerMetadata.get(stripeCustomer.metadata).userId;
1777
+ if (!!ownerId && ownerId !== user.id || !user.emailVerified) stripeCustomer = void 0;
1778
+ }
1763
1779
  if (stripeCustomer) {
1764
1780
  await ctx.context.internalAdapter.updateUser(user.id, { stripeCustomerId: stripeCustomer.id });
1765
1781
  await options.onCustomerCreate?.({
@@ -27,6 +27,6 @@ const STRIPE_ERROR_CODES = defineErrorCodes({
27
27
  });
28
28
  //#endregion
29
29
  //#region src/version.ts
30
- const PACKAGE_VERSION = "1.7.0-beta.1";
30
+ const PACKAGE_VERSION = "1.7.0-beta.10";
31
31
  //#endregion
32
32
  export { STRIPE_ERROR_CODES as n, PACKAGE_VERSION as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth/stripe",
3
- "version": "1.7.0-beta.1",
3
+ "version": "1.7.0-beta.10",
4
4
  "description": "Stripe plugin for Better Auth",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -55,17 +55,17 @@
55
55
  "zod": "^4.3.6"
56
56
  },
57
57
  "devDependencies": {
58
- "better-call": "1.3.5",
58
+ "better-call": "1.3.7",
59
59
  "stripe": "^22.0.1",
60
60
  "tsdown": "0.21.1",
61
- "@better-auth/core": "1.7.0-beta.1",
62
- "better-auth": "1.7.0-beta.1"
61
+ "@better-auth/core": "1.7.0-beta.10",
62
+ "better-auth": "1.7.0-beta.10"
63
63
  },
64
64
  "peerDependencies": {
65
- "better-call": "1.3.5",
65
+ "better-call": "1.3.7",
66
66
  "stripe": "^18 || ^19 || ^20 || ^21 || ^22",
67
- "@better-auth/core": "^1.7.0-beta.1",
68
- "better-auth": "^1.7.0-beta.1"
67
+ "@better-auth/core": "^1.7.0-beta.10",
68
+ "better-auth": "^1.7.0-beta.10"
69
69
  },
70
70
  "scripts": {
71
71
  "build": "tsdown",