@better-auth/stripe 1.5.0-beta.1 → 1.5.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.
@@ -0,0 +1,94 @@
1
+ import { defu } from "defu";
2
+ import type Stripe from "stripe";
3
+
4
+ /**
5
+ * Internal metadata fields for Stripe Customer.
6
+ */
7
+ type CustomerInternalMetadata =
8
+ | { customerType: "user"; userId: string }
9
+ | { customerType: "organization"; organizationId: string };
10
+
11
+ /**
12
+ * Internal metadata fields for Stripe Subscription/Checkout.
13
+ */
14
+ type SubscriptionInternalMetadata = {
15
+ userId: string;
16
+ subscriptionId: string;
17
+ referenceId: string;
18
+ };
19
+
20
+ /**
21
+ * Customer metadata - set internal fields and extract typed fields.
22
+ */
23
+ export const customerMetadata = {
24
+ /**
25
+ * Internal metadata keys for type-safe access.
26
+ */
27
+ keys: {
28
+ userId: "userId",
29
+ organizationId: "organizationId",
30
+ customerType: "customerType",
31
+ } as const,
32
+
33
+ /**
34
+ * Create metadata with internal fields that cannot be overridden by user metadata.
35
+ * Uses `defu` which prioritizes the first argument.
36
+ */
37
+ set(
38
+ internalFields: CustomerInternalMetadata,
39
+ ...userMetadata: (Stripe.Emptyable<Stripe.MetadataParam> | undefined)[]
40
+ ): Stripe.MetadataParam {
41
+ return defu(internalFields, ...userMetadata.filter(Boolean));
42
+ },
43
+
44
+ /**
45
+ * Extract internal fields from Stripe metadata.
46
+ * Provides type-safe access to internal metadata keys.
47
+ */
48
+ get(metadata: Stripe.Metadata | null | undefined) {
49
+ return {
50
+ userId: metadata?.userId,
51
+ organizationId: metadata?.organizationId,
52
+ customerType: metadata?.customerType as
53
+ | CustomerInternalMetadata["customerType"]
54
+ | undefined,
55
+ };
56
+ },
57
+ };
58
+
59
+ /**
60
+ * Subscription/Checkout metadata - set internal fields and extract typed fields.
61
+ */
62
+ export const subscriptionMetadata = {
63
+ /**
64
+ * Internal metadata keys for type-safe access.
65
+ */
66
+ keys: {
67
+ userId: "userId",
68
+ subscriptionId: "subscriptionId",
69
+ referenceId: "referenceId",
70
+ } as const,
71
+
72
+ /**
73
+ * Create metadata with internal fields that cannot be overridden by user metadata.
74
+ * Uses `defu` which prioritizes the first argument.
75
+ */
76
+ set(
77
+ internalFields: SubscriptionInternalMetadata,
78
+ ...userMetadata: (Stripe.Emptyable<Stripe.MetadataParam> | undefined)[]
79
+ ): Stripe.MetadataParam {
80
+ return defu(internalFields, ...userMetadata.filter(Boolean));
81
+ },
82
+
83
+ /**
84
+ * Extract internal fields from Stripe metadata.
85
+ * Provides type-safe access to internal metadata keys.
86
+ */
87
+ get(metadata: Stripe.Metadata | null | undefined) {
88
+ return {
89
+ userId: metadata?.userId,
90
+ subscriptionId: metadata?.subscriptionId,
91
+ referenceId: metadata?.referenceId,
92
+ };
93
+ },
94
+ };
package/src/middleware.ts CHANGED
@@ -1,58 +1,104 @@
1
1
  import { createAuthMiddleware } from "@better-auth/core/api";
2
- import { logger } from "better-auth";
3
- import { APIError } from "better-auth/api";
4
- import type { SubscriptionOptions } from "./types";
2
+ import { APIError } from "@better-auth/core/error";
3
+ import { sessionMiddleware } from "better-auth/api";
4
+ import { STRIPE_ERROR_CODES } from "./error-codes";
5
+ import type {
6
+ AuthorizeReferenceAction,
7
+ CustomerType,
8
+ StripeCtxSession,
9
+ SubscriptionOptions,
10
+ } from "./types";
11
+
12
+ export const stripeSessionMiddleware = createAuthMiddleware(
13
+ {
14
+ use: [sessionMiddleware],
15
+ },
16
+ async (ctx) => {
17
+ const session = ctx.context.session as StripeCtxSession;
18
+ return {
19
+ session,
20
+ };
21
+ },
22
+ );
5
23
 
6
24
  export const referenceMiddleware = (
7
25
  subscriptionOptions: SubscriptionOptions,
8
- action:
9
- | "upgrade-subscription"
10
- | "list-subscription"
11
- | "cancel-subscription"
12
- | "restore-subscription"
13
- | "billing-portal",
26
+ action: AuthorizeReferenceAction,
14
27
  ) =>
15
28
  createAuthMiddleware(async (ctx) => {
16
- const session = ctx.context.session;
17
- if (!session) {
18
- throw new APIError("UNAUTHORIZED");
29
+ const ctxSession = ctx.context.session as StripeCtxSession;
30
+ if (!ctxSession) {
31
+ throw APIError.from("UNAUTHORIZED", STRIPE_ERROR_CODES.UNAUTHORIZED);
32
+ }
33
+
34
+ const customerType: CustomerType =
35
+ ctx.body?.customerType || ctx.query?.customerType;
36
+ const explicitReferenceId = ctx.body?.referenceId || ctx.query?.referenceId;
37
+
38
+ if (customerType === "organization") {
39
+ // Organization subscriptions always require authorizeReference
40
+ if (!subscriptionOptions.authorizeReference) {
41
+ ctx.context.logger.error(
42
+ `Organization subscriptions require authorizeReference to be defined in your stripe plugin config.`,
43
+ );
44
+ throw APIError.from(
45
+ "BAD_REQUEST",
46
+ STRIPE_ERROR_CODES.ORGANIZATION_SUBSCRIPTION_NOT_ENABLED,
47
+ );
48
+ }
49
+
50
+ const referenceId =
51
+ explicitReferenceId || ctxSession.session.activeOrganizationId;
52
+ if (!referenceId) {
53
+ throw APIError.from(
54
+ "BAD_REQUEST",
55
+ STRIPE_ERROR_CODES.ORGANIZATION_REFERENCE_ID_REQUIRED,
56
+ );
57
+ }
58
+ const isAuthorized = await subscriptionOptions.authorizeReference(
59
+ {
60
+ user: ctxSession.user,
61
+ session: ctxSession.session,
62
+ referenceId,
63
+ action,
64
+ },
65
+ ctx,
66
+ );
67
+ if (!isAuthorized) {
68
+ throw APIError.from("UNAUTHORIZED", STRIPE_ERROR_CODES.UNAUTHORIZED);
69
+ }
70
+ return;
19
71
  }
20
- const referenceId =
21
- ctx.body?.referenceId || ctx.query?.referenceId || session.user.id;
22
-
23
- if (
24
- referenceId !== session.user.id &&
25
- !subscriptionOptions.authorizeReference
26
- ) {
27
- logger.error(
72
+
73
+ // User subscriptions - pass if no explicit referenceId
74
+ if (!explicitReferenceId) {
75
+ return;
76
+ }
77
+
78
+ // Pass if referenceId is user id
79
+ if (explicitReferenceId === ctxSession.user.id) {
80
+ return;
81
+ }
82
+
83
+ if (!subscriptionOptions.authorizeReference) {
84
+ ctx.context.logger.error(
28
85
  `Passing referenceId into a subscription action isn't allowed if subscription.authorizeReference isn't defined in your stripe plugin config.`,
29
86
  );
30
- throw new APIError("BAD_REQUEST", {
31
- message:
32
- "Reference id is not allowed. Read server logs for more details.",
33
- });
87
+ throw APIError.from(
88
+ "BAD_REQUEST",
89
+ STRIPE_ERROR_CODES.REFERENCE_ID_NOT_ALLOWED,
90
+ );
34
91
  }
35
- /**
36
- * if referenceId is the same as the active session user's id
37
- */
38
- const sameReference =
39
- ctx.query?.referenceId === session.user.id ||
40
- ctx.body?.referenceId === session.user.id;
41
- const isAuthorized =
42
- ctx.body?.referenceId || ctx.query?.referenceId
43
- ? (await subscriptionOptions.authorizeReference?.(
44
- {
45
- user: session.user,
46
- session: session.session,
47
- referenceId,
48
- action,
49
- },
50
- ctx,
51
- )) || sameReference
52
- : true;
92
+ const isAuthorized = await subscriptionOptions.authorizeReference(
93
+ {
94
+ user: ctxSession.user,
95
+ session: ctxSession.session,
96
+ referenceId: explicitReferenceId,
97
+ action,
98
+ },
99
+ ctx,
100
+ );
53
101
  if (!isAuthorized) {
54
- throw new APIError("UNAUTHORIZED", {
55
- message: "Unauthorized",
56
- });
102
+ throw APIError.from("UNAUTHORIZED", STRIPE_ERROR_CODES.UNAUTHORIZED);
57
103
  }
58
104
  });