@better-auth/stripe 1.4.7-beta.2 → 1.4.7-beta.4

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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @better-auth/stripe@1.4.7-beta.2 build /home/runner/work/better-auth/better-auth/packages/stripe
2
+ > @better-auth/stripe@1.4.7-beta.4 build /home/runner/work/better-auth/better-auth/packages/stripe
3
3
  > tsdown
4
4
 
5
5
  ℹ tsdown v0.17.2 powered by rolldown v1.0.0-beta.53
@@ -7,10 +7,10 @@
7
7
  ℹ entry: src/index.ts, src/client.ts
8
8
  ℹ tsconfig: tsconfig.json
9
9
  ℹ Build start
10
- ℹ dist/index.mjs 40.34 kB │ gzip: 7.53 kB
10
+ ℹ dist/index.mjs 40.71 kB │ gzip: 7.59 kB
11
11
  ℹ dist/client.mjs  0.26 kB │ gzip: 0.20 kB
12
- ℹ dist/client.d.mts  0.62 kB │ gzip: 0.35 kB
12
+ ℹ dist/client.d.mts  0.62 kB │ gzip: 0.34 kB
13
13
  ℹ dist/index.d.mts  0.21 kB │ gzip: 0.14 kB
14
- ℹ dist/index-B_7z2Xag.d.mts 25.09 kB │ gzip: 4.83 kB
15
- ℹ 5 files, total: 66.53 kB
16
- ✔ Build complete in 12150ms
14
+ ℹ dist/index-Dbyi2yVo.d.mts 24.87 kB │ gzip: 4.82 kB
15
+ ℹ 5 files, total: 66.68 kB
16
+ ✔ Build complete in 12247ms
package/dist/client.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { n as stripe } from "./index-B_7z2Xag.mjs";
1
+ import { n as stripe } from "./index-Dbyi2yVo.mjs";
2
2
 
3
3
  //#region src/client.d.ts
4
4
  declare const stripeClient: <O extends {
@@ -366,15 +366,13 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
366
366
  stripeWebhook: better_call0.StrictEndpoint<"/stripe/webhook", {
367
367
  method: "POST";
368
368
  metadata: {
369
- isAction: false;
370
369
  openapi: {
371
370
  operationId: string;
372
371
  };
372
+ scope: "server";
373
373
  };
374
374
  cloneRequest: true;
375
375
  disableBody: true;
376
- } & {
377
- use: any[];
378
376
  }, {
379
377
  success: boolean;
380
378
  }>;
@@ -423,8 +421,6 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
423
421
  };
424
422
  };
425
423
  }>))[];
426
- } & {
427
- use: any[];
428
424
  }, {
429
425
  url: string;
430
426
  redirect: boolean;
@@ -515,8 +511,6 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
515
511
  };
516
512
  };
517
513
  use: ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<void>)[];
518
- } & {
519
- use: any[];
520
514
  }, never>;
521
515
  cancelSubscription: better_call0.StrictEndpoint<"/subscription/cancel", {
522
516
  method: "POST";
@@ -553,8 +547,6 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
553
547
  };
554
548
  };
555
549
  }>))[];
556
- } & {
557
- use: any[];
558
550
  }, {
559
551
  url: string;
560
552
  redirect: boolean;
@@ -593,8 +585,6 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
593
585
  };
594
586
  };
595
587
  }>))[];
596
- } & {
597
- use: any[];
598
588
  }, Stripe.Response<Stripe.Subscription>>;
599
589
  listActiveSubscriptions: better_call0.StrictEndpoint<"/subscription/list", {
600
590
  method: "GET";
@@ -629,8 +619,6 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
629
619
  };
630
620
  };
631
621
  }>))[];
632
- } & {
633
- use: any[];
634
622
  }, {
635
623
  limits: Record<string, unknown> | undefined;
636
624
  priceId: string | undefined;
@@ -657,8 +645,6 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
657
645
  };
658
646
  };
659
647
  use: ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<void>)[];
660
- } & {
661
- use: any[];
662
648
  }, {
663
649
  status: ("OK" | "CREATED" | "ACCEPTED" | "NO_CONTENT" | "MULTIPLE_CHOICES" | "MOVED_PERMANENTLY" | "FOUND" | "SEE_OTHER" | "NOT_MODIFIED" | "TEMPORARY_REDIRECT" | "BAD_REQUEST" | "UNAUTHORIZED" | "PAYMENT_REQUIRED" | "FORBIDDEN" | "NOT_FOUND" | "METHOD_NOT_ALLOWED" | "NOT_ACCEPTABLE" | "PROXY_AUTHENTICATION_REQUIRED" | "REQUEST_TIMEOUT" | "CONFLICT" | "GONE" | "LENGTH_REQUIRED" | "PRECONDITION_FAILED" | "PAYLOAD_TOO_LARGE" | "URI_TOO_LONG" | "UNSUPPORTED_MEDIA_TYPE" | "RANGE_NOT_SATISFIABLE" | "EXPECTATION_FAILED" | "I'M_A_TEAPOT" | "MISDIRECTED_REQUEST" | "UNPROCESSABLE_ENTITY" | "LOCKED" | "FAILED_DEPENDENCY" | "TOO_EARLY" | "UPGRADE_REQUIRED" | "PRECONDITION_REQUIRED" | "TOO_MANY_REQUESTS" | "REQUEST_HEADER_FIELDS_TOO_LARGE" | "UNAVAILABLE_FOR_LEGAL_REASONS" | "INTERNAL_SERVER_ERROR" | "NOT_IMPLEMENTED" | "BAD_GATEWAY" | "SERVICE_UNAVAILABLE" | "GATEWAY_TIMEOUT" | "HTTP_VERSION_NOT_SUPPORTED" | "VARIANT_ALSO_NEGOTIATES" | "INSUFFICIENT_STORAGE" | "LOOP_DETECTED" | "NOT_EXTENDED" | "NETWORK_AUTHENTICATION_REQUIRED") | better_call0.Status;
664
650
  body: ({
@@ -708,8 +694,6 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
708
694
  };
709
695
  };
710
696
  }>))[];
711
- } & {
712
- use: any[];
713
697
  }, {
714
698
  url: string;
715
699
  redirect: boolean;
package/dist/index.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { a as SubscriptionOptions, i as Subscription, n as stripe, r as StripePlan, t as StripePlugin } from "./index-B_7z2Xag.mjs";
1
+ import { a as SubscriptionOptions, i as Subscription, n as stripe, r as StripePlan, t as StripePlugin } from "./index-Dbyi2yVo.mjs";
2
2
  export { StripePlan, StripePlugin, Subscription, SubscriptionOptions, stripe };
package/dist/index.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  import { defineErrorCodes } from "@better-auth/core/utils";
2
2
  import { defu } from "defu";
3
3
  import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api";
4
+ import { HIDE_METADATA, logger } from "better-auth";
4
5
  import { APIError, getSessionFromCtx, originCheck, sessionMiddleware } from "better-auth/api";
5
6
  import * as z from "zod/v4";
6
- import { logger } from "better-auth";
7
7
  import { mergeSchema } from "better-auth/db";
8
8
 
9
9
  //#region src/utils.ts
@@ -265,7 +265,7 @@ const upgradeSubscription = (options) => {
265
265
  const referenceId = ctx.body.referenceId || user$1.id;
266
266
  const plan = await getPlanByName(options, ctx.body.plan);
267
267
  if (!plan) throw new APIError("BAD_REQUEST", { message: STRIPE_ERROR_CODES$1.SUBSCRIPTION_PLAN_NOT_FOUND });
268
- const subscriptionToUpdate = ctx.body.subscriptionId ? await ctx.context.adapter.findOne({
268
+ let subscriptionToUpdate = ctx.body.subscriptionId ? await ctx.context.adapter.findOne({
269
269
  model: "subscription",
270
270
  where: [{
271
271
  field: "id",
@@ -283,6 +283,7 @@ const upgradeSubscription = (options) => {
283
283
  value: referenceId
284
284
  }]
285
285
  }) : null;
286
+ if (ctx.body.subscriptionId && subscriptionToUpdate && subscriptionToUpdate.referenceId !== referenceId) subscriptionToUpdate = null;
286
287
  if (ctx.body.subscriptionId && !subscriptionToUpdate) throw new APIError("BAD_REQUEST", { message: STRIPE_ERROR_CODES$1.SUBSCRIPTION_NOT_FOUND });
287
288
  let customerId = subscriptionToUpdate?.stripeCustomerId || user$1.stripeCustomerId;
288
289
  if (!customerId) try {
@@ -553,7 +554,7 @@ const cancelSubscription = (options) => {
553
554
  ]
554
555
  }, async (ctx) => {
555
556
  const referenceId = ctx.body?.referenceId || ctx.context.session.user.id;
556
- const subscription = ctx.body.subscriptionId ? await ctx.context.adapter.findOne({
557
+ let subscription = ctx.body.subscriptionId ? await ctx.context.adapter.findOne({
557
558
  model: "subscription",
558
559
  where: [{
559
560
  field: "id",
@@ -566,6 +567,7 @@ const cancelSubscription = (options) => {
566
567
  value: referenceId
567
568
  }]
568
569
  }).then((subs) => subs.find((sub) => sub.status === "active" || sub.status === "trialing"));
570
+ if (ctx.body.subscriptionId && subscription && subscription.referenceId !== referenceId) subscription = void 0;
569
571
  if (!subscription || !subscription.stripeCustomerId) throw ctx.error("BAD_REQUEST", { message: STRIPE_ERROR_CODES$1.SUBSCRIPTION_NOT_FOUND });
570
572
  const activeSubscriptions = await client.subscriptions.list({ customer: subscription.stripeCustomerId }).then((res) => res.data.filter((sub) => sub.status === "active" || sub.status === "trialing"));
571
573
  if (!activeSubscriptions.length) {
@@ -631,7 +633,7 @@ const restoreSubscription = (options) => {
631
633
  use: [sessionMiddleware, referenceMiddleware(subscriptionOptions, "restore-subscription")]
632
634
  }, async (ctx) => {
633
635
  const referenceId = ctx.body?.referenceId || ctx.context.session.user.id;
634
- const subscription = ctx.body.subscriptionId ? await ctx.context.adapter.findOne({
636
+ let subscription = ctx.body.subscriptionId ? await ctx.context.adapter.findOne({
635
637
  model: "subscription",
636
638
  where: [{
637
639
  field: "id",
@@ -644,6 +646,7 @@ const restoreSubscription = (options) => {
644
646
  value: referenceId
645
647
  }]
646
648
  }).then((subs) => subs.find((sub) => sub.status === "active" || sub.status === "trialing"));
649
+ if (ctx.body.subscriptionId && subscription && subscription.referenceId !== referenceId) subscription = void 0;
647
650
  if (!subscription || !subscription.stripeCustomerId) throw ctx.error("BAD_REQUEST", { message: STRIPE_ERROR_CODES$1.SUBSCRIPTION_NOT_FOUND });
648
651
  if (subscription.status != "active" && subscription.status != "trialing") throw ctx.error("BAD_REQUEST", { message: STRIPE_ERROR_CODES$1.SUBSCRIPTION_NOT_ACTIVE });
649
652
  if (!subscription.cancelAtPeriodEnd) throw ctx.error("BAD_REQUEST", { message: STRIPE_ERROR_CODES$1.SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION });
@@ -824,7 +827,7 @@ const stripeWebhook = (options) => {
824
827
  return createAuthEndpoint("/stripe/webhook", {
825
828
  method: "POST",
826
829
  metadata: {
827
- isAction: false,
830
+ ...HIDE_METADATA,
828
831
  openapi: { operationId: "handleStripeWebhook" }
829
832
  },
830
833
  cloneRequest: true,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@better-auth/stripe",
3
3
  "author": "Bereket Engida",
4
- "version": "1.4.7-beta.2",
4
+ "version": "1.4.7-beta.4",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
7
7
  "types": "dist/index.d.mts",
@@ -50,15 +50,15 @@
50
50
  },
51
51
  "peerDependencies": {
52
52
  "stripe": "^18 || ^19 || ^20",
53
- "@better-auth/core": "1.4.7-beta.2",
54
- "better-auth": "1.4.7-beta.2"
53
+ "@better-auth/core": "1.4.7-beta.4",
54
+ "better-auth": "1.4.7-beta.4"
55
55
  },
56
56
  "devDependencies": {
57
57
  "better-call": "1.1.5",
58
58
  "stripe": "^20.0.0",
59
59
  "tsdown": "^0.17.2",
60
- "@better-auth/core": "1.4.7-beta.2",
61
- "better-auth": "1.4.7-beta.2"
60
+ "@better-auth/core": "1.4.7-beta.4",
61
+ "better-auth": "1.4.7-beta.4"
62
62
  },
63
63
  "scripts": {
64
64
  "test": "vitest",
package/src/routes.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createAuthEndpoint } from "@better-auth/core/api";
2
2
  import { defineErrorCodes } from "@better-auth/core/utils";
3
3
  import type { GenericEndpointContext } from "better-auth";
4
+ import { HIDE_METADATA } from "better-auth";
4
5
  import {
5
6
  APIError,
6
7
  getSessionFromCtx,
@@ -181,7 +182,7 @@ export const upgradeSubscription = (options: StripeOptions) => {
181
182
  message: STRIPE_ERROR_CODES.SUBSCRIPTION_PLAN_NOT_FOUND,
182
183
  });
183
184
  }
184
- const subscriptionToUpdate = ctx.body.subscriptionId
185
+ let subscriptionToUpdate = ctx.body.subscriptionId
185
186
  ? await ctx.context.adapter.findOne<Subscription>({
186
187
  model: "subscription",
187
188
  where: [
@@ -204,6 +205,14 @@ export const upgradeSubscription = (options: StripeOptions) => {
204
205
  })
205
206
  : null;
206
207
 
208
+ if (
209
+ ctx.body.subscriptionId &&
210
+ subscriptionToUpdate &&
211
+ subscriptionToUpdate.referenceId !== referenceId
212
+ ) {
213
+ subscriptionToUpdate = null;
214
+ }
215
+
207
216
  if (ctx.body.subscriptionId && !subscriptionToUpdate) {
208
217
  throw new APIError("BAD_REQUEST", {
209
218
  message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
@@ -697,7 +706,7 @@ export const cancelSubscription = (options: StripeOptions) => {
697
706
  },
698
707
  async (ctx) => {
699
708
  const referenceId = ctx.body?.referenceId || ctx.context.session.user.id;
700
- const subscription = ctx.body.subscriptionId
709
+ let subscription = ctx.body.subscriptionId
701
710
  ? await ctx.context.adapter.findOne<Subscription>({
702
711
  model: "subscription",
703
712
  where: [
@@ -718,6 +727,15 @@ export const cancelSubscription = (options: StripeOptions) => {
718
727
  ),
719
728
  );
720
729
 
730
+ // Ensure the specified subscription belongs to the (validated) referenceId.
731
+ if (
732
+ ctx.body.subscriptionId &&
733
+ subscription &&
734
+ subscription.referenceId !== referenceId
735
+ ) {
736
+ subscription = undefined;
737
+ }
738
+
721
739
  if (!subscription || !subscription.stripeCustomerId) {
722
740
  throw ctx.error("BAD_REQUEST", {
723
741
  message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
@@ -846,7 +864,7 @@ export const restoreSubscription = (options: StripeOptions) => {
846
864
  async (ctx) => {
847
865
  const referenceId = ctx.body?.referenceId || ctx.context.session.user.id;
848
866
 
849
- const subscription = ctx.body.subscriptionId
867
+ let subscription = ctx.body.subscriptionId
850
868
  ? await ctx.context.adapter.findOne<Subscription>({
851
869
  model: "subscription",
852
870
  where: [
@@ -871,6 +889,15 @@ export const restoreSubscription = (options: StripeOptions) => {
871
889
  (sub) => sub.status === "active" || sub.status === "trialing",
872
890
  ),
873
891
  );
892
+
893
+ // Ensure the specified subscription belongs to the (validated) referenceId.
894
+ if (
895
+ ctx.body.subscriptionId &&
896
+ subscription &&
897
+ subscription.referenceId !== referenceId
898
+ ) {
899
+ subscription = undefined;
900
+ }
874
901
  if (!subscription || !subscription.stripeCustomerId) {
875
902
  throw ctx.error("BAD_REQUEST", {
876
903
  message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
@@ -1221,7 +1248,7 @@ export const stripeWebhook = (options: StripeOptions) => {
1221
1248
  {
1222
1249
  method: "POST",
1223
1250
  metadata: {
1224
- isAction: false,
1251
+ ...HIDE_METADATA,
1225
1252
  openapi: {
1226
1253
  operationId: "handleStripeWebhook",
1227
1254
  },
@@ -227,6 +227,92 @@ describe("stripe", async () => {
227
227
  });
228
228
  });
229
229
 
230
+ it("should not allow cross-user subscriptionId operations (upgrade/cancel/restore)", async () => {
231
+ const userA = {
232
+ email: "user-a@email.com",
233
+ password: "password",
234
+ name: "User A",
235
+ };
236
+ const userARes = await authClient.signUp.email(userA, { throw: true });
237
+
238
+ const userAHeaders = new Headers();
239
+ await authClient.signIn.email(userA, {
240
+ throw: true,
241
+ onSuccess: setCookieToHeader(userAHeaders),
242
+ });
243
+ await authClient.subscription.upgrade({
244
+ plan: "starter",
245
+ fetchOptions: { headers: userAHeaders },
246
+ });
247
+
248
+ const userASub = await ctx.adapter.findOne<Subscription>({
249
+ model: "subscription",
250
+ where: [{ field: "referenceId", value: userARes.user.id }],
251
+ });
252
+ expect(userASub).toBeTruthy();
253
+
254
+ const userB = {
255
+ email: "user-b@email.com",
256
+ password: "password",
257
+ name: "User B",
258
+ };
259
+ await authClient.signUp.email(userB, { throw: true });
260
+ const userBHeaders = new Headers();
261
+ await authClient.signIn.email(userB, {
262
+ throw: true,
263
+ onSuccess: setCookieToHeader(userBHeaders),
264
+ });
265
+
266
+ mockStripe.checkout.sessions.create.mockClear();
267
+ mockStripe.billingPortal.sessions.create.mockClear();
268
+ mockStripe.subscriptions.list.mockClear();
269
+ mockStripe.subscriptions.update.mockClear();
270
+
271
+ const upgradeRes = await authClient.subscription.upgrade({
272
+ plan: "premium",
273
+ subscriptionId: userASub!.id,
274
+ fetchOptions: { headers: userBHeaders },
275
+ });
276
+ expect(upgradeRes.error?.message).toContain("Subscription not found");
277
+ expect(mockStripe.checkout.sessions.create).not.toHaveBeenCalled();
278
+ expect(mockStripe.billingPortal.sessions.create).not.toHaveBeenCalled();
279
+
280
+ const cancelHeaders = new Headers(userBHeaders);
281
+ cancelHeaders.set("content-type", "application/json");
282
+ const cancelResponse = await auth.handler(
283
+ new Request("http://localhost:3000/api/auth/subscription/cancel", {
284
+ method: "POST",
285
+ headers: cancelHeaders,
286
+ body: JSON.stringify({
287
+ subscriptionId: userASub!.id,
288
+ returnUrl: "/account",
289
+ }),
290
+ }),
291
+ );
292
+ expect(cancelResponse.status).toBe(400);
293
+ expect((await cancelResponse.json()).message).toContain(
294
+ "Subscription not found",
295
+ );
296
+ expect(mockStripe.billingPortal.sessions.create).not.toHaveBeenCalled();
297
+
298
+ const restoreHeaders = new Headers(userBHeaders);
299
+ restoreHeaders.set("content-type", "application/json");
300
+ const restoreResponse = await auth.handler(
301
+ new Request("http://localhost:3000/api/auth/subscription/restore", {
302
+ method: "POST",
303
+ headers: restoreHeaders,
304
+ body: JSON.stringify({
305
+ subscriptionId: userASub!.id,
306
+ }),
307
+ }),
308
+ );
309
+ expect(restoreResponse.status).toBe(400);
310
+ expect((await restoreResponse.json()).message).toContain(
311
+ "Subscription not found",
312
+ );
313
+ expect(mockStripe.subscriptions.update).not.toHaveBeenCalled();
314
+ });
315
+
230
316
  it("should list active subscriptions", async () => {
231
317
  const userRes = await authClient.signUp.email(
232
318
  {