@better-auth/stripe 1.3.28 → 1.3.30

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,17 +1,17 @@
1
1
 
2
- > @better-auth/stripe@1.3.28 build /home/runner/work/better-auth/better-auth/packages/stripe
2
+ > @better-auth/stripe@1.3.30 build /home/runner/work/better-auth/better-auth/packages/stripe
3
3
  > unbuild
4
4
 
5
5
  [info] Automatically detected entries: src/index, src/client [esm] [cjs] [dts]
6
6
  [info] Building stripe
7
7
  [success] Build succeeded for stripe
8
- [log] dist/index.cjs (total size: 49.2 kB, chunk size: 49.2 kB, exports: stripe)
8
+ [log] dist/index.cjs (total size: 49.4 kB, chunk size: 49.4 kB, exports: stripe)
9
9
 
10
10
  [log] dist/client.cjs (total size: 270 B, chunk size: 270 B, exports: stripeClient)
11
11
 
12
- [log] dist/index.mjs (total size: 48.3 kB, chunk size: 48.3 kB, exports: stripe)
12
+ [log] dist/index.mjs (total size: 48.5 kB, chunk size: 48.5 kB, exports: stripe)
13
13
 
14
14
  [log] dist/client.mjs (total size: 243 B, chunk size: 243 B, exports: stripeClient)
15
15
 
16
- Σ Total dist size (byte size): 236 kB
16
+ Σ Total dist size (byte size): 238 kB
17
17
  [log]
package/dist/index.cjs CHANGED
@@ -1319,11 +1319,15 @@ const stripe = (options) => {
1319
1319
  message: "Stripe webhook secret not found"
1320
1320
  });
1321
1321
  }
1322
- event = await client.webhooks.constructEventAsync(
1323
- buf,
1324
- sig,
1325
- webhookSecret
1326
- );
1322
+ if (typeof client.webhooks.constructEventAsync === "function") {
1323
+ event = await client.webhooks.constructEventAsync(
1324
+ buf,
1325
+ sig,
1326
+ webhookSecret
1327
+ );
1328
+ } else {
1329
+ event = client.webhooks.constructEvent(buf, sig, webhookSecret);
1330
+ }
1327
1331
  } catch (err) {
1328
1332
  ctx.context.logger.error(`${err.message}`);
1329
1333
  throw new api.APIError("BAD_REQUEST", {
package/dist/index.d.cts CHANGED
@@ -460,6 +460,7 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
460
460
  amount_total: number | null;
461
461
  automatic_tax: Stripe.Checkout.Session.AutomaticTax;
462
462
  billing_address_collection: Stripe.Checkout.Session.BillingAddressCollection | null;
463
+ branding_settings?: Stripe.Checkout.Session.BrandingSettings;
463
464
  cancel_url: string | null;
464
465
  client_reference_id: string | null;
465
466
  client_secret: string | null;
@@ -476,6 +477,7 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
476
477
  customer_details: Stripe.Checkout.Session.CustomerDetails | null;
477
478
  customer_email: string | null;
478
479
  discounts: Array<Stripe.Checkout.Session.Discount> | null;
480
+ excluded_payment_method_types?: Array<string>;
479
481
  expires_at: number;
480
482
  invoice: string | Stripe.Invoice | null;
481
483
  invoice_creation: Stripe.Checkout.Session.InvoiceCreation | null;
@@ -484,6 +486,7 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
484
486
  locale: Stripe.Checkout.Session.Locale | null;
485
487
  metadata: Stripe.Metadata | null;
486
488
  mode: Stripe.Checkout.Session.Mode;
489
+ name_collection?: Stripe.Checkout.Session.NameCollection;
487
490
  optional_items?: Array<Stripe.Checkout.Session.OptionalItem> | null;
488
491
  origin_context: Stripe.Checkout.Session.OriginContext | null;
489
492
  payment_intent: string | Stripe.PaymentIntent | null;
@@ -538,6 +541,7 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
538
541
  amount_total: number | null;
539
542
  automatic_tax: Stripe.Checkout.Session.AutomaticTax;
540
543
  billing_address_collection: Stripe.Checkout.Session.BillingAddressCollection | null;
544
+ branding_settings?: Stripe.Checkout.Session.BrandingSettings;
541
545
  cancel_url: string | null;
542
546
  client_reference_id: string | null;
543
547
  client_secret: string | null;
@@ -554,6 +558,7 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
554
558
  customer_details: Stripe.Checkout.Session.CustomerDetails | null;
555
559
  customer_email: string | null;
556
560
  discounts: Array<Stripe.Checkout.Session.Discount> | null;
561
+ excluded_payment_method_types?: Array<string>;
557
562
  expires_at: number;
558
563
  invoice: string | Stripe.Invoice | null;
559
564
  invoice_creation: Stripe.Checkout.Session.InvoiceCreation | null;
@@ -562,6 +567,7 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
562
567
  locale: Stripe.Checkout.Session.Locale | null;
563
568
  metadata: Stripe.Metadata | null;
564
569
  mode: Stripe.Checkout.Session.Mode;
570
+ name_collection?: Stripe.Checkout.Session.NameCollection;
565
571
  optional_items?: Array<Stripe.Checkout.Session.OptionalItem> | null;
566
572
  origin_context: Stripe.Checkout.Session.OriginContext | null;
567
573
  payment_intent: string | Stripe.PaymentIntent | null;
package/dist/index.d.mts CHANGED
@@ -460,6 +460,7 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
460
460
  amount_total: number | null;
461
461
  automatic_tax: Stripe.Checkout.Session.AutomaticTax;
462
462
  billing_address_collection: Stripe.Checkout.Session.BillingAddressCollection | null;
463
+ branding_settings?: Stripe.Checkout.Session.BrandingSettings;
463
464
  cancel_url: string | null;
464
465
  client_reference_id: string | null;
465
466
  client_secret: string | null;
@@ -476,6 +477,7 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
476
477
  customer_details: Stripe.Checkout.Session.CustomerDetails | null;
477
478
  customer_email: string | null;
478
479
  discounts: Array<Stripe.Checkout.Session.Discount> | null;
480
+ excluded_payment_method_types?: Array<string>;
479
481
  expires_at: number;
480
482
  invoice: string | Stripe.Invoice | null;
481
483
  invoice_creation: Stripe.Checkout.Session.InvoiceCreation | null;
@@ -484,6 +486,7 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
484
486
  locale: Stripe.Checkout.Session.Locale | null;
485
487
  metadata: Stripe.Metadata | null;
486
488
  mode: Stripe.Checkout.Session.Mode;
489
+ name_collection?: Stripe.Checkout.Session.NameCollection;
487
490
  optional_items?: Array<Stripe.Checkout.Session.OptionalItem> | null;
488
491
  origin_context: Stripe.Checkout.Session.OriginContext | null;
489
492
  payment_intent: string | Stripe.PaymentIntent | null;
@@ -538,6 +541,7 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
538
541
  amount_total: number | null;
539
542
  automatic_tax: Stripe.Checkout.Session.AutomaticTax;
540
543
  billing_address_collection: Stripe.Checkout.Session.BillingAddressCollection | null;
544
+ branding_settings?: Stripe.Checkout.Session.BrandingSettings;
541
545
  cancel_url: string | null;
542
546
  client_reference_id: string | null;
543
547
  client_secret: string | null;
@@ -554,6 +558,7 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
554
558
  customer_details: Stripe.Checkout.Session.CustomerDetails | null;
555
559
  customer_email: string | null;
556
560
  discounts: Array<Stripe.Checkout.Session.Discount> | null;
561
+ excluded_payment_method_types?: Array<string>;
557
562
  expires_at: number;
558
563
  invoice: string | Stripe.Invoice | null;
559
564
  invoice_creation: Stripe.Checkout.Session.InvoiceCreation | null;
@@ -562,6 +567,7 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
562
567
  locale: Stripe.Checkout.Session.Locale | null;
563
568
  metadata: Stripe.Metadata | null;
564
569
  mode: Stripe.Checkout.Session.Mode;
570
+ name_collection?: Stripe.Checkout.Session.NameCollection;
565
571
  optional_items?: Array<Stripe.Checkout.Session.OptionalItem> | null;
566
572
  origin_context: Stripe.Checkout.Session.OriginContext | null;
567
573
  payment_intent: string | Stripe.PaymentIntent | null;
package/dist/index.d.ts CHANGED
@@ -460,6 +460,7 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
460
460
  amount_total: number | null;
461
461
  automatic_tax: Stripe.Checkout.Session.AutomaticTax;
462
462
  billing_address_collection: Stripe.Checkout.Session.BillingAddressCollection | null;
463
+ branding_settings?: Stripe.Checkout.Session.BrandingSettings;
463
464
  cancel_url: string | null;
464
465
  client_reference_id: string | null;
465
466
  client_secret: string | null;
@@ -476,6 +477,7 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
476
477
  customer_details: Stripe.Checkout.Session.CustomerDetails | null;
477
478
  customer_email: string | null;
478
479
  discounts: Array<Stripe.Checkout.Session.Discount> | null;
480
+ excluded_payment_method_types?: Array<string>;
479
481
  expires_at: number;
480
482
  invoice: string | Stripe.Invoice | null;
481
483
  invoice_creation: Stripe.Checkout.Session.InvoiceCreation | null;
@@ -484,6 +486,7 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
484
486
  locale: Stripe.Checkout.Session.Locale | null;
485
487
  metadata: Stripe.Metadata | null;
486
488
  mode: Stripe.Checkout.Session.Mode;
489
+ name_collection?: Stripe.Checkout.Session.NameCollection;
487
490
  optional_items?: Array<Stripe.Checkout.Session.OptionalItem> | null;
488
491
  origin_context: Stripe.Checkout.Session.OriginContext | null;
489
492
  payment_intent: string | Stripe.PaymentIntent | null;
@@ -538,6 +541,7 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
538
541
  amount_total: number | null;
539
542
  automatic_tax: Stripe.Checkout.Session.AutomaticTax;
540
543
  billing_address_collection: Stripe.Checkout.Session.BillingAddressCollection | null;
544
+ branding_settings?: Stripe.Checkout.Session.BrandingSettings;
541
545
  cancel_url: string | null;
542
546
  client_reference_id: string | null;
543
547
  client_secret: string | null;
@@ -554,6 +558,7 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
554
558
  customer_details: Stripe.Checkout.Session.CustomerDetails | null;
555
559
  customer_email: string | null;
556
560
  discounts: Array<Stripe.Checkout.Session.Discount> | null;
561
+ excluded_payment_method_types?: Array<string>;
557
562
  expires_at: number;
558
563
  invoice: string | Stripe.Invoice | null;
559
564
  invoice_creation: Stripe.Checkout.Session.InvoiceCreation | null;
@@ -562,6 +567,7 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
562
567
  locale: Stripe.Checkout.Session.Locale | null;
563
568
  metadata: Stripe.Metadata | null;
564
569
  mode: Stripe.Checkout.Session.Mode;
570
+ name_collection?: Stripe.Checkout.Session.NameCollection;
565
571
  optional_items?: Array<Stripe.Checkout.Session.OptionalItem> | null;
566
572
  origin_context: Stripe.Checkout.Session.OriginContext | null;
567
573
  payment_intent: string | Stripe.PaymentIntent | null;
package/dist/index.mjs CHANGED
@@ -1303,11 +1303,15 @@ const stripe = (options) => {
1303
1303
  message: "Stripe webhook secret not found"
1304
1304
  });
1305
1305
  }
1306
- event = await client.webhooks.constructEventAsync(
1307
- buf,
1308
- sig,
1309
- webhookSecret
1310
- );
1306
+ if (typeof client.webhooks.constructEventAsync === "function") {
1307
+ event = await client.webhooks.constructEventAsync(
1308
+ buf,
1309
+ sig,
1310
+ webhookSecret
1311
+ );
1312
+ } else {
1313
+ event = client.webhooks.constructEvent(buf, sig, webhookSecret);
1314
+ }
1311
1315
  } catch (err) {
1312
1316
  ctx.context.logger.error(`${err.message}`);
1313
1317
  throw new APIError("BAD_REQUEST", {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@better-auth/stripe",
3
3
  "author": "Bereket Engida",
4
- "version": "1.3.28",
4
+ "version": "1.3.30",
5
5
  "main": "dist/index.cjs",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -41,16 +41,16 @@
41
41
  "zod": "^4.1.5"
42
42
  },
43
43
  "peerDependencies": {
44
- "stripe": "^18",
45
- "@better-auth/core": "1.3.28",
46
- "better-auth": "1.3.28"
44
+ "stripe": "^18 || ^19",
45
+ "@better-auth/core": "1.3.30",
46
+ "better-auth": "1.3.30"
47
47
  },
48
48
  "devDependencies": {
49
49
  "better-call": "1.0.19",
50
- "stripe": "^18.5.0",
50
+ "stripe": "^19.1.0",
51
51
  "unbuild": "3.6.1",
52
- "@better-auth/core": "1.3.28",
53
- "better-auth": "1.3.28"
52
+ "@better-auth/core": "1.3.30",
53
+ "better-auth": "1.3.30"
54
54
  },
55
55
  "scripts": {
56
56
  "test": "vitest",
package/src/index.ts CHANGED
@@ -1244,11 +1244,18 @@ export const stripe = <O extends StripeOptions>(options: O) => {
1244
1244
  message: "Stripe webhook secret not found",
1245
1245
  });
1246
1246
  }
1247
- event = await client.webhooks.constructEventAsync(
1248
- buf,
1249
- sig,
1250
- webhookSecret,
1251
- );
1247
+ // Support both Stripe v18 (constructEvent) and v19+ (constructEventAsync)
1248
+ if (typeof client.webhooks.constructEventAsync === "function") {
1249
+ // Stripe v19+ - use async method
1250
+ event = await client.webhooks.constructEventAsync(
1251
+ buf,
1252
+ sig,
1253
+ webhookSecret,
1254
+ );
1255
+ } else {
1256
+ // Stripe v18 - use sync method
1257
+ event = client.webhooks.constructEvent(buf, sig, webhookSecret);
1258
+ }
1252
1259
  } catch (err: any) {
1253
1260
  ctx.context.logger.error(`${err.message}`);
1254
1261
  throw new APIError("BAD_REQUEST", {
@@ -49,7 +49,7 @@ describe("stripe", async () => {
49
49
  update: vi.fn(),
50
50
  },
51
51
  webhooks: {
52
- constructEvent: vi.fn(),
52
+ constructEventAsync: vi.fn(),
53
53
  },
54
54
  };
55
55
 
@@ -1739,4 +1739,412 @@ describe("stripe", async () => {
1739
1739
  });
1740
1740
  });
1741
1741
  });
1742
+
1743
+ describe("Webhook Error Handling (Stripe v19)", () => {
1744
+ it("should handle invalid webhook signature with constructEventAsync", async () => {
1745
+ const mockError = new Error("Invalid signature");
1746
+ const stripeWithError = {
1747
+ ...stripeOptions.stripeClient,
1748
+ webhooks: {
1749
+ constructEventAsync: vi.fn().mockRejectedValue(mockError),
1750
+ },
1751
+ };
1752
+
1753
+ const testOptions = {
1754
+ ...stripeOptions,
1755
+ stripeClient: stripeWithError as unknown as Stripe,
1756
+ stripeWebhookSecret: "test_secret",
1757
+ };
1758
+
1759
+ const testAuth = betterAuth({
1760
+ baseURL: "http://localhost:3000",
1761
+ database: memory,
1762
+ emailAndPassword: { enabled: true },
1763
+ plugins: [stripe(testOptions)],
1764
+ });
1765
+
1766
+ const mockRequest = new Request(
1767
+ "http://localhost:3000/api/auth/stripe/webhook",
1768
+ {
1769
+ method: "POST",
1770
+ headers: {
1771
+ "stripe-signature": "invalid_signature",
1772
+ },
1773
+ body: JSON.stringify({ type: "test.event" }),
1774
+ },
1775
+ );
1776
+
1777
+ const response = await testAuth.handler(mockRequest);
1778
+ expect(response.status).toBe(400);
1779
+ const data = await response.json();
1780
+ expect(data.message).toContain("Webhook Error");
1781
+ });
1782
+
1783
+ it("should reject webhook request without stripe-signature header", async () => {
1784
+ const testAuth = betterAuth({
1785
+ baseURL: "http://localhost:3000",
1786
+ database: memory,
1787
+ emailAndPassword: { enabled: true },
1788
+ plugins: [stripe(stripeOptions)],
1789
+ });
1790
+
1791
+ const mockRequest = new Request(
1792
+ "http://localhost:3000/api/auth/stripe/webhook",
1793
+ {
1794
+ method: "POST",
1795
+ headers: {
1796
+ "content-type": "application/json",
1797
+ },
1798
+ body: JSON.stringify({ type: "test.event" }),
1799
+ },
1800
+ );
1801
+
1802
+ const response = await testAuth.handler(mockRequest);
1803
+ expect(response.status).toBe(400);
1804
+ const data = await response.json();
1805
+ expect(data.message).toContain("Stripe webhook secret not found");
1806
+ });
1807
+
1808
+ it("should handle constructEventAsync returning null/undefined", async () => {
1809
+ const stripeWithNull = {
1810
+ ...stripeOptions.stripeClient,
1811
+ webhooks: {
1812
+ constructEventAsync: vi.fn().mockResolvedValue(null),
1813
+ },
1814
+ };
1815
+
1816
+ const testOptions = {
1817
+ ...stripeOptions,
1818
+ stripeClient: stripeWithNull as unknown as Stripe,
1819
+ stripeWebhookSecret: "test_secret",
1820
+ };
1821
+
1822
+ const testAuth = betterAuth({
1823
+ baseURL: "http://localhost:3000",
1824
+ database: memory,
1825
+ emailAndPassword: { enabled: true },
1826
+ plugins: [stripe(testOptions)],
1827
+ });
1828
+
1829
+ const mockRequest = new Request(
1830
+ "http://localhost:3000/api/auth/stripe/webhook",
1831
+ {
1832
+ method: "POST",
1833
+ headers: {
1834
+ "stripe-signature": "test_signature",
1835
+ },
1836
+ body: JSON.stringify({ type: "test.event" }),
1837
+ },
1838
+ );
1839
+
1840
+ const response = await testAuth.handler(mockRequest);
1841
+ expect(response.status).toBe(400);
1842
+ const data = await response.json();
1843
+ expect(data.message).toContain("Failed to construct event");
1844
+ });
1845
+
1846
+ it("should handle async errors in webhook event processing", async () => {
1847
+ const errorThrowingHandler = vi
1848
+ .fn()
1849
+ .mockRejectedValue(new Error("Event processing failed"));
1850
+
1851
+ const mockEvent = {
1852
+ type: "checkout.session.completed",
1853
+ data: {
1854
+ object: {
1855
+ mode: "subscription",
1856
+ subscription: "sub_123",
1857
+ metadata: {
1858
+ referenceId: "user_123",
1859
+ subscriptionId: "sub_123",
1860
+ },
1861
+ },
1862
+ },
1863
+ };
1864
+
1865
+ const stripeForTest = {
1866
+ ...stripeOptions.stripeClient,
1867
+ subscriptions: {
1868
+ retrieve: vi.fn().mockRejectedValue(new Error("Stripe API error")),
1869
+ },
1870
+ webhooks: {
1871
+ constructEventAsync: vi.fn().mockResolvedValue(mockEvent),
1872
+ },
1873
+ };
1874
+
1875
+ const testOptions = {
1876
+ ...stripeOptions,
1877
+ stripeClient: stripeForTest as unknown as Stripe,
1878
+ stripeWebhookSecret: "test_secret",
1879
+ subscription: {
1880
+ ...stripeOptions.subscription,
1881
+ onSubscriptionComplete: errorThrowingHandler,
1882
+ },
1883
+ };
1884
+
1885
+ const testAuth = betterAuth({
1886
+ baseURL: "http://localhost:3000",
1887
+ database: memory,
1888
+ emailAndPassword: { enabled: true },
1889
+ plugins: [stripe(testOptions as StripeOptions)],
1890
+ });
1891
+
1892
+ await ctx.adapter.create({
1893
+ model: "subscription",
1894
+ data: {
1895
+ referenceId: "user_123",
1896
+ stripeCustomerId: "cus_123",
1897
+ status: "incomplete",
1898
+ plan: "starter",
1899
+ id: "sub_123",
1900
+ },
1901
+ });
1902
+
1903
+ const mockRequest = new Request(
1904
+ "http://localhost:3000/api/auth/stripe/webhook",
1905
+ {
1906
+ method: "POST",
1907
+ headers: {
1908
+ "stripe-signature": "test_signature",
1909
+ },
1910
+ body: JSON.stringify(mockEvent),
1911
+ },
1912
+ );
1913
+
1914
+ const response = await testAuth.handler(mockRequest);
1915
+ // Errors inside event handlers are caught and logged but don't fail the webhook
1916
+ // This prevents Stripe from retrying and is the expected behavior
1917
+ expect(response.status).toBe(200);
1918
+ const data = await response.json();
1919
+ expect(data).toEqual({ success: true });
1920
+ // Verify the error was logged (via the stripeClient.subscriptions.retrieve rejection)
1921
+ expect(stripeForTest.subscriptions.retrieve).toHaveBeenCalled();
1922
+ });
1923
+
1924
+ it("should successfully process webhook with valid async signature verification", async () => {
1925
+ const mockEvent = {
1926
+ type: "customer.subscription.updated",
1927
+ data: {
1928
+ object: {
1929
+ id: "sub_test_async",
1930
+ customer: "cus_test_async",
1931
+ status: "active",
1932
+ items: {
1933
+ data: [
1934
+ {
1935
+ price: { id: process.env.STRIPE_PRICE_ID_1 },
1936
+ quantity: 1,
1937
+ current_period_start: Math.floor(Date.now() / 1000),
1938
+ current_period_end:
1939
+ Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
1940
+ },
1941
+ ],
1942
+ },
1943
+ current_period_start: Math.floor(Date.now() / 1000),
1944
+ current_period_end:
1945
+ Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
1946
+ },
1947
+ },
1948
+ };
1949
+
1950
+ const stripeForTest = {
1951
+ ...stripeOptions.stripeClient,
1952
+ webhooks: {
1953
+ // Simulate async verification success
1954
+ constructEventAsync: vi.fn().mockResolvedValue(mockEvent),
1955
+ },
1956
+ };
1957
+
1958
+ const testOptions = {
1959
+ ...stripeOptions,
1960
+ stripeClient: stripeForTest as unknown as Stripe,
1961
+ stripeWebhookSecret: "test_secret_async",
1962
+ };
1963
+
1964
+ const testAuth = betterAuth({
1965
+ baseURL: "http://localhost:3000",
1966
+ database: memory,
1967
+ emailAndPassword: { enabled: true },
1968
+ plugins: [stripe(testOptions)],
1969
+ });
1970
+
1971
+ const { id: subId } = await ctx.adapter.create({
1972
+ model: "subscription",
1973
+ data: {
1974
+ referenceId: userId,
1975
+ stripeCustomerId: "cus_test_async",
1976
+ stripeSubscriptionId: "sub_test_async",
1977
+ status: "incomplete",
1978
+ plan: "starter",
1979
+ },
1980
+ });
1981
+
1982
+ const mockRequest = new Request(
1983
+ "http://localhost:3000/api/auth/stripe/webhook",
1984
+ {
1985
+ method: "POST",
1986
+ headers: {
1987
+ "stripe-signature": "valid_async_signature",
1988
+ },
1989
+ body: JSON.stringify(mockEvent),
1990
+ },
1991
+ );
1992
+
1993
+ const response = await testAuth.handler(mockRequest);
1994
+ expect(response.status).toBe(200);
1995
+ expect(stripeForTest.webhooks.constructEventAsync).toHaveBeenCalledWith(
1996
+ expect.any(String),
1997
+ "valid_async_signature",
1998
+ "test_secret_async",
1999
+ );
2000
+
2001
+ const data = await response.json();
2002
+ expect(data).toEqual({ success: true });
2003
+ });
2004
+
2005
+ it("should call constructEventAsync with exactly 3 required parameters", async () => {
2006
+ const mockEvent = {
2007
+ type: "customer.subscription.created",
2008
+ data: {
2009
+ object: {
2010
+ id: "sub_test_params",
2011
+ customer: "cus_test_params",
2012
+ status: "active",
2013
+ },
2014
+ },
2015
+ };
2016
+
2017
+ const stripeForTest = {
2018
+ ...stripeOptions.stripeClient,
2019
+ webhooks: {
2020
+ constructEventAsync: vi.fn().mockResolvedValue(mockEvent),
2021
+ },
2022
+ };
2023
+
2024
+ const testOptions = {
2025
+ ...stripeOptions,
2026
+ stripeClient: stripeForTest as unknown as Stripe,
2027
+ stripeWebhookSecret: "test_secret_params",
2028
+ };
2029
+
2030
+ const testAuth = betterAuth({
2031
+ baseURL: "http://localhost:3000",
2032
+ database: memory,
2033
+ emailAndPassword: { enabled: true },
2034
+ plugins: [stripe(testOptions)],
2035
+ });
2036
+
2037
+ const mockRequest = new Request(
2038
+ "http://localhost:3000/api/auth/stripe/webhook",
2039
+ {
2040
+ method: "POST",
2041
+ headers: {
2042
+ "stripe-signature": "test_signature_params",
2043
+ },
2044
+ body: JSON.stringify(mockEvent),
2045
+ },
2046
+ );
2047
+
2048
+ await testAuth.handler(mockRequest);
2049
+
2050
+ // Verify that constructEventAsync is called with exactly 3 required parameters
2051
+ // (payload, signature, secret) and no optional parameters
2052
+ expect(stripeForTest.webhooks.constructEventAsync).toHaveBeenCalledWith(
2053
+ expect.any(String), // payload
2054
+ "test_signature_params", // signature
2055
+ "test_secret_params", // secret
2056
+ );
2057
+
2058
+ // Verify it was called exactly once
2059
+ expect(stripeForTest.webhooks.constructEventAsync).toHaveBeenCalledTimes(
2060
+ 1,
2061
+ );
2062
+ });
2063
+
2064
+ it("should support Stripe v18 with sync constructEvent method", async () => {
2065
+ const mockEvent = {
2066
+ type: "customer.subscription.updated",
2067
+ data: {
2068
+ object: {
2069
+ id: "sub_test_v18",
2070
+ customer: "cus_test_v18",
2071
+ status: "active",
2072
+ items: {
2073
+ data: [
2074
+ {
2075
+ price: { id: process.env.STRIPE_PRICE_ID_1 },
2076
+ quantity: 1,
2077
+ current_period_start: Math.floor(Date.now() / 1000),
2078
+ current_period_end:
2079
+ Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
2080
+ },
2081
+ ],
2082
+ },
2083
+ current_period_start: Math.floor(Date.now() / 1000),
2084
+ current_period_end:
2085
+ Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
2086
+ },
2087
+ },
2088
+ };
2089
+
2090
+ // Simulate Stripe v18 - only has sync constructEvent, no constructEventAsync
2091
+ const stripeV18 = {
2092
+ ...stripeOptions.stripeClient,
2093
+ webhooks: {
2094
+ constructEvent: vi.fn().mockReturnValue(mockEvent),
2095
+ // v18 doesn't have constructEventAsync
2096
+ constructEventAsync: undefined,
2097
+ },
2098
+ };
2099
+
2100
+ const testOptions = {
2101
+ ...stripeOptions,
2102
+ stripeClient: stripeV18 as unknown as Stripe,
2103
+ stripeWebhookSecret: "test_secret_v18",
2104
+ };
2105
+
2106
+ const testAuth = betterAuth({
2107
+ baseURL: "http://localhost:3000",
2108
+ database: memory,
2109
+ emailAndPassword: { enabled: true },
2110
+ plugins: [stripe(testOptions)],
2111
+ });
2112
+
2113
+ const { id: subId } = await ctx.adapter.create({
2114
+ model: "subscription",
2115
+ data: {
2116
+ referenceId: userId,
2117
+ stripeCustomerId: "cus_test_v18",
2118
+ stripeSubscriptionId: "sub_test_v18",
2119
+ status: "incomplete",
2120
+ plan: "starter",
2121
+ },
2122
+ });
2123
+
2124
+ const mockRequest = new Request(
2125
+ "http://localhost:3000/api/auth/stripe/webhook",
2126
+ {
2127
+ method: "POST",
2128
+ headers: {
2129
+ "stripe-signature": "test_signature_v18",
2130
+ },
2131
+ body: JSON.stringify(mockEvent),
2132
+ },
2133
+ );
2134
+
2135
+ const response = await testAuth.handler(mockRequest);
2136
+ expect(response.status).toBe(200);
2137
+
2138
+ // Verify that constructEvent (sync) was called instead of constructEventAsync
2139
+ expect(stripeV18.webhooks.constructEvent).toHaveBeenCalledWith(
2140
+ expect.any(String),
2141
+ "test_signature_v18",
2142
+ "test_secret_v18",
2143
+ );
2144
+ expect(stripeV18.webhooks.constructEvent).toHaveBeenCalledTimes(1);
2145
+
2146
+ const data = await response.json();
2147
+ expect(data).toEqual({ success: true });
2148
+ });
2149
+ });
1742
2150
  });