From 87846d7668dc0f172040970c125b2b0574c80538 Mon Sep 17 00:00:00 2001 From: Yudi Xiao <463708580@qq.com> Date: Mon, 13 Oct 2025 18:17:57 +0800 Subject: [PATCH] revert back to 1.3.27 --- build.config.ts | 20 +- bun.lock | 96 +- package.json | 18 +- src/client.ts | 63 +- src/index.ts | 2669 ++++++++++++++++++++++++----------------------- tsconfig.json | 33 +- 6 files changed, 1427 insertions(+), 1472 deletions(-) diff --git a/build.config.ts b/build.config.ts index 2b1a140..ca7bb72 100644 --- a/build.config.ts +++ b/build.config.ts @@ -1,12 +1,14 @@ -import { defineBuildConfig } from "unbuild"; +import {defineBuildConfig} from "unbuild"; export default defineBuildConfig({ - declaration: true, - rollup: { - emitCJS: true, - }, - outDir: "dist", - clean: false, - failOnWarn: false, - externals: ["better-auth", "better-call", "@better-fetch/fetch", "stripe"], + declaration: "compatible", + rollup: { + esbuild: { + minify: false, + }, + }, + outDir: "dist", + clean: false, + failOnWarn: false, + externals: ["better-auth", "better-call", "@better-fetch/fetch", "stripe"], }); diff --git a/bun.lock b/bun.lock index 7658a71..ef480e6 100644 --- a/bun.lock +++ b/bun.lock @@ -8,16 +8,16 @@ "zod": "^4.1.5", }, "devDependencies": { - "@better-auth/core": "^1.4.0-beta.9", - "better-auth": "^1.4.0-beta.9", - "better-call": "^1.0.19", - "stripe": "^19.1.0", - "unbuild": "^3.6.1", + "@better-auth/core": "1.3.27", + "better-auth": "1.3.27", + "better-call": "1.0.19", + "stripe": "^18.5.0", + "unbuild": "3.6.1", }, "peerDependencies": { - "@better-auth/core": "^1.4.0-beta.9", - "better-auth": "^1.4.0-beta.9", - "stripe": "^19", + "@better-auth/core": "1.3.27", + "better-auth": "1.3.27", + "stripe": "^18", }, }, }, @@ -26,9 +26,7 @@ "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], - "@better-auth/core": ["@better-auth/core@1.4.0-beta.9", "", { "dependencies": { "zod": "^4.1.5" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.18", "better-call": "1.0.19", "better-sqlite3": "^12.4.1", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-4YanGHTihKhKamN2T4ndjYTR5By162kk88DPt2xwwVc2t/MnSO7TiVBB0V5gZ6JM6YMX2aSPTfMbk89AEjpvCQ=="], - - "@better-auth/telemetry": ["@better-auth/telemetry@1.4.0-beta.9", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.18" }, "peerDependencies": { "@better-auth/core": "1.4.0-beta.9" } }, "sha512-lvl3nq648uAt3ACpjtBBW1Nhec1lxciWYKy+9wgkQRKuE+l4NyxVdfCnDAxxUPAScQNJ+6zMXgt9ZRW5cJJ8bQ=="], + "@better-auth/core": ["@better-auth/core@1.3.27", "", { "dependencies": { "better-call": "1.0.19", "zod": "^4.1.5" } }, "sha512-3Sfdax6MQyronY+znx7bOsfQHI6m1SThvJWb0RDscFEAhfqLy95k1sl+/PgGyg0cwc2cUXoEiAOSqYdFYrg3vA=="], "@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="], @@ -190,26 +188,16 @@ "autoprefixer": ["autoprefixer@10.4.21", "", { "dependencies": { "browserslist": "^4.24.4", "caniuse-lite": "^1.0.30001702", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ=="], - "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.8.16", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw=="], - "better-auth": ["better-auth@1.4.0-beta.9", "", { "dependencies": { "@better-auth/core": "1.4.0-beta.9", "@better-auth/telemetry": "1.4.0-beta.9", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.18", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "@simplewebauthn/browser": "^13.1.2", "@simplewebauthn/server": "^13.1.2", "better-call": "1.0.19", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.1.5" } }, "sha512-JmVvaC/VCtFtI/sUGkoaLnzV/nnMn13XR6Vrkf4zGxah6g38S0GPvD7iZlBHehTN6Fb5aUwWPKgqFowxyOm2xw=="], + "better-auth": ["better-auth@1.3.27", "", { "dependencies": { "@better-auth/core": "1.3.27", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.18", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "@simplewebauthn/browser": "^13.1.2", "@simplewebauthn/server": "^13.1.2", "better-call": "1.0.19", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.1.5" } }, "sha512-SwiGAJ7yU6dBhNg0NdV1h5M8T5sa7/AszZVc4vBfMDrLLmvUfbt9JoJ0uRUJUEdKRAAxTyl9yA+F3+GhtAD80w=="], "better-call": ["better-call@1.0.19", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.5.1", "set-cookie-parser": "^2.7.1", "uncrypto": "^0.1.3" } }, "sha512-sI3GcA1SCVa3H+CDHl8W8qzhlrckwXOTKhqq3OOPXjgn5aTOMIqGY34zLY/pHA6tRRMjTUC3lz5Mi7EbDA24Kw=="], - "better-sqlite3": ["better-sqlite3@12.4.1", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ=="], - - "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="], - - "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], - "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], "browserslist": ["browserslist@4.26.3", "", { "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", "electron-to-chromium": "^1.5.227", "node-releases": "^2.0.21", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w=="], - "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], - "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], @@ -218,8 +206,6 @@ "caniuse-lite": ["caniuse-lite@1.0.30001750", "", {}, "sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ=="], - "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], - "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], "colord": ["colord@2.9.3", "", {}, "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw=="], @@ -250,16 +236,10 @@ "csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="], - "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], - - "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], - "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], - "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], @@ -272,8 +252,6 @@ "electron-to-chromium": ["electron-to-chromium@1.5.234", "", {}, "sha512-RXfEp2x+VRYn8jbKfQlRImzoJU01kyDvVPBmG39eU2iuRVhuS6vQNocB8J0/8GrIMLnPzgz4eW6WiRnJkTuNWg=="], - "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], - "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], @@ -288,20 +266,14 @@ "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], - "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], - "exsolve": ["exsolve@1.0.7", "", {}, "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], - "fix-dts-default-cjs-exports": ["fix-dts-default-cjs-exports@1.0.1", "", { "dependencies": { "magic-string": "^0.30.17", "mlly": "^1.7.4", "rollup": "^4.34.8" } }, "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg=="], "fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="], - "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -310,8 +282,6 @@ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], @@ -320,12 +290,6 @@ "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], - "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - - "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], - "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], "is-module": ["is-module@1.0.0", "", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="], @@ -354,12 +318,6 @@ "mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="], - "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], - - "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - - "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], - "mkdist": ["mkdist@2.4.1", "", { "dependencies": { "autoprefixer": "^10.4.21", "citty": "^0.1.6", "cssnano": "^7.1.1", "defu": "^6.1.4", "esbuild": "^0.25.9", "jiti": "^1.21.7", "mlly": "^1.8.0", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "postcss": "^8.5.6", "postcss-nested": "^7.0.2", "semver": "^7.7.2", "tinyglobby": "^0.2.15" }, "peerDependencies": { "sass": "^1.92.1", "typescript": ">=5.9.2", "vue": "^3.5.21", "vue-sfc-transformer": "^0.1.1", "vue-tsc": "^1.8.27 || ^2.0.21 || ^3.0.0" }, "optionalPeers": ["sass", "typescript", "vue", "vue-sfc-transformer", "vue-tsc"], "bin": { "mkdist": "dist/cli.cjs" } }, "sha512-Ezk0gi04GJBkqMfsksICU5Rjoemc4biIekwgrONWVPor2EO/N9nBgN6MZXAf7Yw4mDDhrNyKbdETaHNevfumKg=="], "mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="], @@ -368,10 +326,6 @@ "nanostores": ["nanostores@1.0.1", "", {}, "sha512-kNZ9xnoJYKg/AfxjrVL4SS0fKX++4awQReGqWnwTRHxeHGZ1FJFVgTqr/eMrNQdp0Tz7M7tG/TDaX8QfHDwVCw=="], - "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], - - "node-abi": ["node-abi@3.78.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-E2wEyrgX/CqvicaQYU3Ze1PFGjc4QYPGsjUrlYkqAE0WjHEZwgOsGMPMzkMse4LjJbDmaEuDX3CM036j5K2DSQ=="], - "node-releases": ["node-releases@2.0.23", "", {}, "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg=="], "normalize-range": ["normalize-range@0.1.2", "", {}, "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA=="], @@ -380,8 +334,6 @@ "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], @@ -454,22 +406,14 @@ "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], - "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], - "pretty-bytes": ["pretty-bytes@7.1.0", "", {}, "sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw=="], - "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], - "pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="], "pvutils": ["pvutils@1.1.3", "", {}, "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ=="], "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], - "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], - - "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "reflect-metadata": ["reflect-metadata@0.2.2", "", {}, "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="], "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], @@ -480,8 +424,6 @@ "rou3": ["rou3@0.5.1", "", {}, "sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ=="], - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - "sax": ["sax@1.4.1", "", {}, "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="], "scule": ["scule@1.3.0", "", {}, "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g=="], @@ -498,17 +440,9 @@ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], - - "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], - - "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], - - "stripe": ["stripe@19.1.0", "", { "dependencies": { "qs": "^6.11.0" }, "peerDependencies": { "@types/node": ">=16" }, "optionalPeers": ["@types/node"] }, "sha512-FjgIiE98dMMTNssfdjMvFdD4eZyEzdWAOwPYqzhPRNZeg9ggFWlPXmX1iJKD5pPIwZBaPlC3SayQQkwsPo6/YQ=="], + "stripe": ["stripe@18.5.0", "", { "dependencies": { "qs": "^6.11.0" }, "peerDependencies": { "@types/node": ">=12.x.x" }, "optionalPeers": ["@types/node"] }, "sha512-Hp+wFiEQtCB0LlNgcFh5uVyKznpDjzyUZ+CNVEf+I3fhlYvh7rZruIg+jOwzJRCpy0ZTPMjlzm7J2/M2N6d+DA=="], "stylehacks": ["stylehacks@7.0.6", "", { "dependencies": { "browserslist": "^4.25.1", "postcss-selector-parser": "^7.1.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-iitguKivmsueOmTO0wmxURXBP8uqOO+zikLGZ7Mm9e/94R4w5T999Js2taS/KBOnQ/wdC3jN3vNSrkGDrlnqQg=="], @@ -516,18 +450,12 @@ "svgo": ["svgo@4.0.0", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.4.1" }, "bin": "./bin/svgo.js" }, "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw=="], - "tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="], - - "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], - "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "tsyringe": ["tsyringe@4.10.0", "", { "dependencies": { "tslib": "^1.9.3" } }, "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw=="], - "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="], @@ -542,8 +470,6 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - "zod": ["zod@4.1.12", "", {}, "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ=="], "csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="], diff --git a/package.json b/package.json index d54edd5..c025731 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@bowong/better-auth-stripe", "author": "Bowong", - "version": "1.4.0-beta.10", + "version": "1.3.27-a", "main": "dist/index.cjs", "license": "MIT", "keywords": [ @@ -47,15 +47,15 @@ "zod": "^4.1.5" }, "peerDependencies": { - "@better-auth/core": "^1.4.0-beta.9", - "better-auth": "^1.4.0-beta.9", - "stripe": "^19" + "@better-auth/core": "1.3.27", + "better-auth": "1.3.27", + "stripe": "^18" }, "devDependencies": { - "@better-auth/core": "^1.4.0-beta.9", - "better-auth": "^1.4.0-beta.9", - "better-call": "^1.0.19", - "stripe": "^19.1.0", - "unbuild": "^3.6.1" + "@better-auth/core": "1.3.27", + "better-auth": "1.3.27", + "better-call": "1.0.19", + "stripe": "^18.5.0", + "unbuild": "3.6.1" } } diff --git a/src/client.ts b/src/client.ts index 36ee7dd..de00830 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,36 +1,35 @@ -import {type BetterAuthClientPlugin} from "better-auth"; -import type {stripe} from "./index"; +import type { BetterAuthClientPlugin } from "better-auth"; +import type { stripe } from "./index"; export const stripeClient = < - O extends { - subscription: boolean; - }, + O extends { + subscription: boolean; + }, >( - options?: O, -): BetterAuthClientPlugin => { - return { - id: "stripe-client", - $InferServerPlugin: {} as ReturnType< - typeof stripe< - O["subscription"] extends true - ? { - stripeClient: any; - stripeWebhookSecret: string; - subscription: { - enabled: true; - plans: []; - }; - } - : { - stripeClient: any; - stripeWebhookSecret: string; - } - > - >, - pathMethods: { - "/subscription/restore": "POST", - "/subscription/billing-portal": "POST", - "/subscription/meter-event": "POST", - }, - } satisfies BetterAuthClientPlugin; + options?: O, +) => { + return { + id: "stripe-client", + $InferServerPlugin: {} as ReturnType< + typeof stripe< + O["subscription"] extends true + ? { + stripeClient: any; + stripeWebhookSecret: string; + subscription: { + enabled: true; + plans: []; + }; + } + : { + stripeClient: any; + stripeWebhookSecret: string; + } + > + >, + pathMethods: { + "/subscription/restore": "POST", + "/subscription/billing-portal": "POST", + }, + } satisfies BetterAuthClientPlugin; }; diff --git a/src/index.ts b/src/index.ts index 37778d0..6bc4c94 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,1222 +1,1221 @@ import { - type GenericEndpointContext, - type BetterAuthPlugin, - logger, type BetterAuthClientPlugin, + type GenericEndpointContext, + type BetterAuthPlugin, + logger, } from "better-auth"; -import {createAuthEndpoint, createAuthMiddleware} from "better-auth/plugins"; +import { createAuthEndpoint, createAuthMiddleware } from "better-auth/plugins"; import Stripe from "stripe"; -import {type Stripe as StripeType} from "stripe"; +import { type Stripe as StripeType } from "stripe"; import * as z from "zod/v4"; import { - sessionMiddleware, - APIError, - originCheck, - getSessionFromCtx, + sessionMiddleware, + APIError, + originCheck, + getSessionFromCtx, } from "better-auth/api"; import { - onCheckoutSessionCompleted, - onSubscriptionDeleted, - onSubscriptionUpdated, + onCheckoutSessionCompleted, + onSubscriptionDeleted, + onSubscriptionUpdated, } from "./hooks"; import type { - InputSubscription, - StripeOptions, - StripePlan, - Subscription, + InputSubscription, + StripeOptions, + StripePlan, + Subscription, } from "./types"; -import {getPlanByName, getPlanByPriceInfo, getPlans} from "./utils"; -import {getSchema} from "./schema"; -import {defu} from "defu"; -import {defineErrorCodes} from "@better-auth/core/utils"; +import { getPlanByName, getPlanByPriceInfo, getPlans } from "./utils"; +import { getSchema } from "./schema"; +import { defu } from "defu"; -const STRIPE_ERROR_CODES = defineErrorCodes({ - SUBSCRIPTION_NOT_FOUND: "Subscription not found", - SUBSCRIPTION_PLAN_NOT_FOUND: "Subscription plan not found", - ALREADY_SUBSCRIBED_PLAN: "You're already subscribed to this plan", - UNABLE_TO_CREATE_CUSTOMER: "Unable to create customer", - FAILED_TO_FETCH_PLANS: "Failed to fetch plans", - EMAIL_VERIFICATION_REQUIRED: - "Email verification is required before you can subscribe to a plan", - SUBSCRIPTION_NOT_ACTIVE: "Subscription is not active", - SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION: - "Subscription is not scheduled for cancellation", -}); +const STRIPE_ERROR_CODES = { + SUBSCRIPTION_NOT_FOUND: "Subscription not found", + SUBSCRIPTION_PLAN_NOT_FOUND: "Subscription plan not found", + ALREADY_SUBSCRIBED_PLAN: "You're already subscribed to this plan", + UNABLE_TO_CREATE_CUSTOMER: "Unable to create customer", + FAILED_TO_FETCH_PLANS: "Failed to fetch plans", + EMAIL_VERIFICATION_REQUIRED: + "Email verification is required before you can subscribe to a plan", + SUBSCRIPTION_NOT_ACTIVE: "Subscription is not active", + SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION: + "Subscription is not scheduled for cancellation", +} as const; const getUrl = (ctx: GenericEndpointContext, url: string) => { - if (url.startsWith("http")) { - return url; - } - return `${ctx.context.options.baseURL}${ - url.startsWith("/") ? url : `/${url}` - }`; + if (url.startsWith("http")) { + return url; + } + return `${ctx.context.options.baseURL}${ + url.startsWith("/") ? url : `/${url}` + }`; }; async function resolvePriceIdFromLookupKey( - stripeClient: Stripe, - lookupKey: string, + stripeClient: Stripe, + lookupKey: string, ): Promise { - if (!lookupKey) return undefined; - const prices = await stripeClient.prices.list({ - lookup_keys: [lookupKey], - active: true, - limit: 1, - }); - return prices.data[0]?.id; + if (!lookupKey) return undefined; + const prices = await stripeClient.prices.list({ + lookup_keys: [lookupKey], + active: true, + limit: 1, + }); + return prices.data[0]?.id; } -export const stripe = (options: O) : BetterAuthPlugin => { - const client = options.stripeClient; +export const stripe = (options: O) => { + const client = options.stripeClient; - const referenceMiddleware = ( - action: - | "upgrade-subscription" - | "list-subscription" - | "cancel-subscription" - | "restore-subscription" - | "billing-portal" - | "meter-event", - ) => - createAuthMiddleware(async (ctx) => { - const session = ctx.context.session; - if (!session) { - throw new APIError("UNAUTHORIZED"); - } - const referenceId = - ctx.body?.referenceId || ctx.query?.referenceId || session.user.id; + const referenceMiddleware = ( + action: + | "upgrade-subscription" + | "list-subscription" + | "cancel-subscription" + | "restore-subscription" + | "meter-event" + | "billing-portal", + ) => + createAuthMiddleware(async (ctx) => { + const session = ctx.context.session; + if (!session) { + throw new APIError("UNAUTHORIZED"); + } + const referenceId = + ctx.body?.referenceId || ctx.query?.referenceId || session.user.id; - if (ctx.body?.referenceId && !options.subscription?.authorizeReference) { - logger.error( - `Passing referenceId into a subscription action isn't allowed if subscription.authorizeReference isn't defined in your stripe plugin config.`, - ); - throw new APIError("BAD_REQUEST", { - message: - "Reference id is not allowed. Read server logs for more details.", - }); - } - const isAuthorized = ctx.body?.referenceId - ? await options.subscription?.authorizeReference?.( - { - user: session.user, - session: session.session, - referenceId, - action, - }, - ctx, - ) - : true; - if (!isAuthorized) { - throw new APIError("UNAUTHORIZED", { - message: "Unauthorized", - }); - } - }); + if (ctx.body?.referenceId && !options.subscription?.authorizeReference) { + logger.error( + `Passing referenceId into a subscription action isn't allowed if subscription.authorizeReference isn't defined in your stripe plugin config.`, + ); + throw new APIError("BAD_REQUEST", { + message: + "Reference id is not allowed. Read server logs for more details.", + }); + } + const isAuthorized = ctx.body?.referenceId + ? await options.subscription?.authorizeReference?.( + { + user: session.user, + session: session.session, + referenceId, + action, + }, + ctx, + ) + : true; + if (!isAuthorized) { + throw new APIError("UNAUTHORIZED", { + message: "Unauthorized", + }); + } + }); - const subscriptionEndpoints = { - /** - * ### Endpoint - * - * POST `/subscription/upgrade` - * - * ### API Methods - * - * **server:** - * `auth.api.upgradeSubscription` - * - * **client:** - * `authClient.subscription.upgrade` - * - * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/stripe#api-method-subscription-upgrade) - */ - upgradeSubscription: createAuthEndpoint( - "/subscription/upgrade", - { - method: "POST", - body: z.object({ - /** - * The name of the plan to subscribe - */ - plan: z.string().meta({ - description: 'The name of the plan to upgrade to. Eg: "pro"', - }), - /** - * If annual plan should be applied. - */ - annual: z - .boolean() - .meta({ - description: "Whether to upgrade to an annual plan. Eg: true", - }) - .optional(), - /** - * Reference id of the subscription to upgrade - * This is used to identify the subscription to upgrade - * If not provided, the user's id will be used - */ - referenceId: z - .string() - .meta({ - description: - 'Reference id of the subscription to upgrade. Eg: "123"', - }) - .optional(), - /** - * This is to allow a specific subscription to be upgrade. - * If subscription id is provided, and subscription isn't found, - * it'll throw an error. - */ - subscriptionId: z - .string() - .meta({ - description: - 'The id of the subscription to upgrade. Eg: "sub_123"', - }) - .optional(), - /** - * Any additional data you want to store in your database - * subscriptions - */ - metadata: z.record(z.string(), z.any()).optional(), - /** - * If a subscription - */ - seats: z - .number() - .meta({ - description: - "Number of seats to upgrade to (if applicable). Eg: 1", - }) - .optional(), - /** - * Success URL to redirect back after successful subscription - */ - successUrl: z - .string() - .meta({ - description: - 'Callback URL to redirect back after successful subscription. Eg: "https://example.com/success"', - }) - .default("/"), - /** - * Cancel URL - */ - cancelUrl: z - .string() - .meta({ - description: - 'If set, checkout shows a back button and customers will be directed here if they cancel payment. Eg: "https://example.com/pricing"', - }) - .default("/"), - /** - * Return URL - */ - returnUrl: z - .string() - .meta({ - description: - 'URL to take customers to when they click on the billing portal’s link to return to your website. Eg: "https://example.com/dashboard"', - }) - .optional(), - /** - * Disable Redirect - */ - disableRedirect: z - .boolean() - .meta({ - description: - "Disable redirect after successful subscription. Eg: true", - }) - .default(false), - }), - use: [ - sessionMiddleware, - originCheck((c) => { - return [c.body.successURL as string, c.body.cancelURL as string]; - }), - referenceMiddleware("upgrade-subscription"), - ], - }, - async (ctx) => { - const {user, session} = ctx.context.session; - if ( - !user.emailVerified && - options.subscription?.requireEmailVerification - ) { - throw new APIError("BAD_REQUEST", { - message: STRIPE_ERROR_CODES.EMAIL_VERIFICATION_REQUIRED, - }); - } - const referenceId = ctx.body.referenceId || user.id; - const plan = await getPlanByName(options, ctx.body.plan); - if (!plan) { - throw new APIError("BAD_REQUEST", { - message: STRIPE_ERROR_CODES.SUBSCRIPTION_PLAN_NOT_FOUND, - }); - } - const subscriptionToUpdate = ctx.body.subscriptionId - ? await ctx.context.adapter.findOne({ - model: "subscription", - where: [ - { - field: "id", - value: ctx.body.subscriptionId, - connector: "OR", - }, - { - field: "stripeSubscriptionId", - value: ctx.body.subscriptionId, - connector: "OR", - }, - ], - }) - : referenceId - ? await ctx.context.adapter.findOne({ - model: "subscription", - where: [{field: "referenceId", value: referenceId}], - }) - : null; + const subscriptionEndpoints = { + /** + * ### Endpoint + * + * POST `/subscription/upgrade` + * + * ### API Methods + * + * **server:** + * `auth.api.upgradeSubscription` + * + * **client:** + * `authClient.subscription.upgrade` + * + * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/stripe#api-method-subscription-upgrade) + */ + upgradeSubscription: createAuthEndpoint( + "/subscription/upgrade", + { + method: "POST", + body: z.object({ + /** + * The name of the plan to subscribe + */ + plan: z.string().meta({ + description: 'The name of the plan to upgrade to. Eg: "pro"', + }), + /** + * If annual plan should be applied. + */ + annual: z + .boolean() + .meta({ + description: "Whether to upgrade to an annual plan. Eg: true", + }) + .optional(), + /** + * Reference id of the subscription to upgrade + * This is used to identify the subscription to upgrade + * If not provided, the user's id will be used + */ + referenceId: z + .string() + .meta({ + description: + 'Reference id of the subscription to upgrade. Eg: "123"', + }) + .optional(), + /** + * This is to allow a specific subscription to be upgrade. + * If subscription id is provided, and subscription isn't found, + * it'll throw an error. + */ + subscriptionId: z + .string() + .meta({ + description: + 'The id of the subscription to upgrade. Eg: "sub_123"', + }) + .optional(), + /** + * Any additional data you want to store in your database + * subscriptions + */ + metadata: z.record(z.string(), z.any()).optional(), + /** + * If a subscription + */ + seats: z + .number() + .meta({ + description: + "Number of seats to upgrade to (if applicable). Eg: 1", + }) + .optional(), + /** + * Success URL to redirect back after successful subscription + */ + successUrl: z + .string() + .meta({ + description: + 'Callback URL to redirect back after successful subscription. Eg: "https://example.com/success"', + }) + .default("/"), + /** + * Cancel URL + */ + cancelUrl: z + .string() + .meta({ + description: + 'If set, checkout shows a back button and customers will be directed here if they cancel payment. Eg: "https://example.com/pricing"', + }) + .default("/"), + /** + * Return URL + */ + returnUrl: z + .string() + .meta({ + description: + 'URL to take customers to when they click on the billing portal’s link to return to your website. Eg: "https://example.com/dashboard"', + }) + .optional(), + /** + * Disable Redirect + */ + disableRedirect: z + .boolean() + .meta({ + description: + "Disable redirect after successful subscription. Eg: true", + }) + .default(false), + }), + use: [ + sessionMiddleware, + originCheck((c) => { + return [c.body.successURL as string, c.body.cancelURL as string]; + }), + referenceMiddleware("upgrade-subscription"), + ], + }, + async (ctx) => { + const { user, session } = ctx.context.session; + if ( + !user.emailVerified && + options.subscription?.requireEmailVerification + ) { + throw new APIError("BAD_REQUEST", { + message: STRIPE_ERROR_CODES.EMAIL_VERIFICATION_REQUIRED, + }); + } + const referenceId = ctx.body.referenceId || user.id; + const plan = await getPlanByName(options, ctx.body.plan); + if (!plan) { + throw new APIError("BAD_REQUEST", { + message: STRIPE_ERROR_CODES.SUBSCRIPTION_PLAN_NOT_FOUND, + }); + } + const subscriptionToUpdate = ctx.body.subscriptionId + ? await ctx.context.adapter.findOne({ + model: "subscription", + where: [ + { + field: "id", + value: ctx.body.subscriptionId, + connector: "OR", + }, + { + field: "stripeSubscriptionId", + value: ctx.body.subscriptionId, + connector: "OR", + }, + ], + }) + : referenceId + ? await ctx.context.adapter.findOne({ + model: "subscription", + where: [{ field: "referenceId", value: referenceId }], + }) + : null; - if (ctx.body.subscriptionId && !subscriptionToUpdate) { - throw new APIError("BAD_REQUEST", { - message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND, - }); - } + if (ctx.body.subscriptionId && !subscriptionToUpdate) { + throw new APIError("BAD_REQUEST", { + message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND, + }); + } - let customerId = - subscriptionToUpdate?.stripeCustomerId || user.stripeCustomerId; + let customerId = + subscriptionToUpdate?.stripeCustomerId || user.stripeCustomerId; - if (!customerId) { - try { - // Try to find existing Stripe customer by email - const existingCustomers = await client.customers.list({ - email: user.email, - limit: 1, - }); + if (!customerId) { + try { + // Try to find existing Stripe customer by email + const existingCustomers = await client.customers.list({ + email: user.email, + limit: 1, + }); - let stripeCustomer = existingCustomers.data[0]; + let stripeCustomer = existingCustomers.data[0]; - if (!stripeCustomer) { - stripeCustomer = await client.customers.create({ - email: user.email, - name: user.name, - metadata: { - ...ctx.body.metadata, - userId: user.id, - }, - }); - } + if (!stripeCustomer) { + stripeCustomer = await client.customers.create({ + email: user.email, + name: user.name, + metadata: { + ...ctx.body.metadata, + userId: user.id, + }, + }); + } - // Update local DB with Stripe customer ID - await ctx.context.adapter.update({ - model: "user", - update: { - stripeCustomerId: stripeCustomer.id, - }, - where: [ - { - field: "id", - value: user.id, - }, - ], - }); + // Update local DB with Stripe customer ID + await ctx.context.adapter.update({ + model: "user", + update: { + stripeCustomerId: stripeCustomer.id, + }, + where: [ + { + field: "id", + value: user.id, + }, + ], + }); - customerId = stripeCustomer.id; - } catch (e: any) { - ctx.context.logger.error(e); - throw new APIError("BAD_REQUEST", { - message: STRIPE_ERROR_CODES.UNABLE_TO_CREATE_CUSTOMER, - }); - } - } + customerId = stripeCustomer.id; + } catch (e: any) { + ctx.context.logger.error(e); + throw new APIError("BAD_REQUEST", { + message: STRIPE_ERROR_CODES.UNABLE_TO_CREATE_CUSTOMER, + }); + } + } - const subscriptions = subscriptionToUpdate - ? [subscriptionToUpdate] - : await ctx.context.adapter.findMany({ - model: "subscription", - where: [ - { - field: "referenceId", - value: ctx.body.referenceId || user.id, - }, - ], - }); + const subscriptions = subscriptionToUpdate + ? [subscriptionToUpdate] + : await ctx.context.adapter.findMany({ + model: "subscription", + where: [ + { + field: "referenceId", + value: ctx.body.referenceId || user.id, + }, + ], + }); - const activeOrTrialingSubscription = subscriptions.find( - (sub) => sub.status === "active" || sub.status === "trialing", - ); + const activeOrTrialingSubscription = subscriptions.find( + (sub) => sub.status === "active" || sub.status === "trialing", + ); - const activeSubscriptions = await client.subscriptions - .list({ - customer: customerId, - }) - .then((res) => - res.data.filter( - (sub) => sub.status === "active" || sub.status === "trialing", - ), - ); + const activeSubscriptions = await client.subscriptions + .list({ + customer: customerId, + }) + .then((res) => + res.data.filter( + (sub) => sub.status === "active" || sub.status === "trialing", + ), + ); - const activeSubscription = activeSubscriptions.find((sub) => { - // If we have a specific subscription to update, match by ID - if ( - subscriptionToUpdate?.stripeSubscriptionId || - ctx.body.subscriptionId - ) { - return ( - sub.id === subscriptionToUpdate?.stripeSubscriptionId || - sub.id === ctx.body.subscriptionId - ); - } - // Only find subscription for the same referenceId to avoid mixing personal and org subscriptions - if (activeOrTrialingSubscription?.stripeSubscriptionId) { - return sub.id === activeOrTrialingSubscription.stripeSubscriptionId; - } - return false; - }); + const activeSubscription = activeSubscriptions.find((sub) => { + // If we have a specific subscription to update, match by ID + if ( + subscriptionToUpdate?.stripeSubscriptionId || + ctx.body.subscriptionId + ) { + return ( + sub.id === subscriptionToUpdate?.stripeSubscriptionId || + sub.id === ctx.body.subscriptionId + ); + } + // Only find subscription for the same referenceId to avoid mixing personal and org subscriptions + if (activeOrTrialingSubscription?.stripeSubscriptionId) { + return sub.id === activeOrTrialingSubscription.stripeSubscriptionId; + } + return false; + }); - // Also find any incomplete subscription that we can reuse - const incompleteSubscription = subscriptions.find( - (sub) => sub.status === "incomplete", - ); + // Also find any incomplete subscription that we can reuse + const incompleteSubscription = subscriptions.find( + (sub) => sub.status === "incomplete", + ); - if ( - activeOrTrialingSubscription && - activeOrTrialingSubscription.status === "active" && - activeOrTrialingSubscription.plan === ctx.body.plan && - activeOrTrialingSubscription.seats === (ctx.body.seats || 1) - ) { - throw new APIError("BAD_REQUEST", { - message: STRIPE_ERROR_CODES.ALREADY_SUBSCRIBED_PLAN, - }); - } + if ( + activeOrTrialingSubscription && + activeOrTrialingSubscription.status === "active" && + activeOrTrialingSubscription.plan === ctx.body.plan && + activeOrTrialingSubscription.seats === (ctx.body.seats || 1) + ) { + throw new APIError("BAD_REQUEST", { + message: STRIPE_ERROR_CODES.ALREADY_SUBSCRIBED_PLAN, + }); + } - if (activeSubscription && customerId) { - // Find the corresponding database subscription for this Stripe subscription - let dbSubscription = await ctx.context.adapter.findOne({ - model: "subscription", - where: [ - { - field: "stripeSubscriptionId", - value: activeSubscription.id, - }, - ], - }); + if (activeSubscription && customerId) { + // Find the corresponding database subscription for this Stripe subscription + let dbSubscription = await ctx.context.adapter.findOne({ + model: "subscription", + where: [ + { + field: "stripeSubscriptionId", + value: activeSubscription.id, + }, + ], + }); - // If no database record exists for this Stripe subscription, update the existing one - if (!dbSubscription && activeOrTrialingSubscription) { - await ctx.context.adapter.update({ - model: "subscription", - update: { - stripeSubscriptionId: activeSubscription.id, - updatedAt: new Date(), - }, - where: [ - { - field: "id", - value: activeOrTrialingSubscription.id, - }, - ], - }); - dbSubscription = activeOrTrialingSubscription; - } + // If no database record exists for this Stripe subscription, update the existing one + if (!dbSubscription && activeOrTrialingSubscription) { + await ctx.context.adapter.update({ + model: "subscription", + update: { + stripeSubscriptionId: activeSubscription.id, + updatedAt: new Date(), + }, + where: [ + { + field: "id", + value: activeOrTrialingSubscription.id, + }, + ], + }); + dbSubscription = activeOrTrialingSubscription; + } - // Resolve price ID if using lookup keys - let priceIdToUse: string | undefined = undefined; - if (ctx.body.annual) { - priceIdToUse = plan.annualDiscountPriceId; - if (!priceIdToUse && plan.annualDiscountLookupKey) { - priceIdToUse = await resolvePriceIdFromLookupKey( - client, - plan.annualDiscountLookupKey, - ); - } - } else { - priceIdToUse = plan.priceId; - if (!priceIdToUse && plan.lookupKey) { - priceIdToUse = await resolvePriceIdFromLookupKey( - client, - plan.lookupKey, - ); - } - } + // Resolve price ID if using lookup keys + let priceIdToUse: string | undefined = undefined; + if (ctx.body.annual) { + priceIdToUse = plan.annualDiscountPriceId; + if (!priceIdToUse && plan.annualDiscountLookupKey) { + priceIdToUse = await resolvePriceIdFromLookupKey( + client, + plan.annualDiscountLookupKey, + ); + } + } else { + priceIdToUse = plan.priceId; + if (!priceIdToUse && plan.lookupKey) { + priceIdToUse = await resolvePriceIdFromLookupKey( + client, + plan.lookupKey, + ); + } + } - if (!priceIdToUse) { - throw ctx.error("BAD_REQUEST", { - message: "Price ID not found for the selected plan", - }); - } + if (!priceIdToUse) { + throw ctx.error("BAD_REQUEST", { + message: "Price ID not found for the selected plan", + }); + } - const {url} = await client.billingPortal.sessions - .create({ - customer: customerId, - return_url: getUrl(ctx, ctx.body.returnUrl || "/"), - flow_data: { - type: "subscription_update_confirm", - after_completion: { - type: "redirect", - redirect: { - return_url: getUrl(ctx, ctx.body.returnUrl || "/"), - }, - }, - subscription_update_confirm: { - subscription: activeSubscription.id, - items: [ - { - id: activeSubscription.items.data[0]?.id as string, - quantity: ctx.body.seats || 1, - price: priceIdToUse, - }, - ], - }, - }, - }) - .catch(async (e) => { - throw ctx.error("BAD_REQUEST", { - message: e.message, - code: e.code, - }); - }); - return ctx.json({ - url, - redirect: true, - }); - } + const { url } = await client.billingPortal.sessions + .create({ + customer: customerId, + return_url: getUrl(ctx, ctx.body.returnUrl || "/"), + flow_data: { + type: "subscription_update_confirm", + after_completion: { + type: "redirect", + redirect: { + return_url: getUrl(ctx, ctx.body.returnUrl || "/"), + }, + }, + subscription_update_confirm: { + subscription: activeSubscription.id, + items: [ + { + id: activeSubscription.items.data[0]?.id as string, + quantity: ctx.body.seats || 1, + price: priceIdToUse, + }, + ], + }, + }, + }) + .catch(async (e) => { + throw ctx.error("BAD_REQUEST", { + message: e.message, + code: e.code, + }); + }); + return ctx.json({ + url, + redirect: true, + }); + } - let subscription: Subscription | undefined = - activeOrTrialingSubscription || incompleteSubscription; + let subscription: Subscription | undefined = + activeOrTrialingSubscription || incompleteSubscription; - if (incompleteSubscription && !activeOrTrialingSubscription) { - const updated = await ctx.context.adapter.update({ - model: "subscription", - update: { - plan: plan.name.toLowerCase(), - seats: ctx.body.seats || 1, - updatedAt: new Date(), - }, - where: [ - { - field: "id", - value: incompleteSubscription.id, - }, - ], - }); - subscription = (updated as Subscription) || incompleteSubscription; - } + if (incompleteSubscription && !activeOrTrialingSubscription) { + const updated = await ctx.context.adapter.update({ + model: "subscription", + update: { + plan: plan.name.toLowerCase(), + seats: ctx.body.seats || 1, + updatedAt: new Date(), + }, + where: [ + { + field: "id", + value: incompleteSubscription.id, + }, + ], + }); + subscription = (updated as Subscription) || incompleteSubscription; + } - if (!subscription) { - subscription = await ctx.context.adapter.create< - InputSubscription, - Subscription - >({ - model: "subscription", - data: { - plan: plan.name.toLowerCase(), - stripeCustomerId: customerId, - status: "incomplete", - referenceId, - seats: ctx.body.seats || 1, - }, - }); - } + if (!subscription) { + subscription = await ctx.context.adapter.create< + InputSubscription, + Subscription + >({ + model: "subscription", + data: { + plan: plan.name.toLowerCase(), + stripeCustomerId: customerId, + status: "incomplete", + referenceId, + seats: ctx.body.seats || 1, + }, + }); + } - if (!subscription) { - ctx.context.logger.error("Subscription ID not found"); - throw new APIError("INTERNAL_SERVER_ERROR"); - } + if (!subscription) { + ctx.context.logger.error("Subscription ID not found"); + throw new APIError("INTERNAL_SERVER_ERROR"); + } - const params = await options.subscription?.getCheckoutSessionParams?.( - { - user, - session, - plan, - subscription, - }, - ctx.request, - //@ts-expect-error - ctx, - ); + const params = await options.subscription?.getCheckoutSessionParams?.( + { + user, + session, + plan, + subscription, + }, + ctx.request, + //@ts-expect-error + ctx, + ); - const hasEverTrialed = subscriptions.some((s) => { - // Check if user has ever had a trial for any plan (not just the same plan) - // This prevents users from getting multiple trials by switching plans - const hadTrial = - !!(s.trialStart || s.trialEnd) || s.status === "trialing"; - return hadTrial; - }); + const hasEverTrialed = subscriptions.some((s) => { + // Check if user has ever had a trial for any plan (not just the same plan) + // This prevents users from getting multiple trials by switching plans + const hadTrial = + !!(s.trialStart || s.trialEnd) || s.status === "trialing"; + return hadTrial; + }); - const freeTrial = - !hasEverTrialed && plan.freeTrial - ? {trial_period_days: plan.freeTrial.days} - : undefined; + const freeTrial = + !hasEverTrialed && plan.freeTrial + ? { trial_period_days: plan.freeTrial.days } + : undefined; - let priceIdToUse: string | undefined = undefined; - if (ctx.body.annual) { - priceIdToUse = plan.annualDiscountPriceId; - if (!priceIdToUse && plan.annualDiscountLookupKey) { - priceIdToUse = await resolvePriceIdFromLookupKey( - client, - plan.annualDiscountLookupKey, - ); - } - } else { - priceIdToUse = plan.priceId; - if (!priceIdToUse && plan.lookupKey) { - priceIdToUse = await resolvePriceIdFromLookupKey( - client, - plan.lookupKey, - ); - } - } - const checkoutSession = await client.checkout.sessions - .create( - { - ...(customerId - ? { - customer: customerId, - customer_update: { - name: "auto", - address: "auto", - }, - } - : { - customer_email: session.user.email, - }), - success_url: getUrl( - ctx, - `${ - ctx.context.baseURL - }/subscription/success?callbackURL=${encodeURIComponent( - ctx.body.successUrl, - )}&subscriptionId=${encodeURIComponent(subscription.id)}`, - ), - cancel_url: getUrl(ctx, ctx.body.cancelUrl), - line_items: [ - { - price: priceIdToUse, - quantity: ctx.body.seats || 1, - }, - ], - subscription_data: { - ...freeTrial, - }, - mode: "subscription", - client_reference_id: referenceId, - ...params?.params, - metadata: { - userId: user.id, - subscriptionId: subscription.id, - referenceId, - ...params?.params?.metadata, - }, - }, - params?.options, - ) - .catch(async (e) => { - throw ctx.error("BAD_REQUEST", { - message: e.message, - code: e.code, - }); - }); - return ctx.json({ - ...checkoutSession, - redirect: !ctx.body.disableRedirect, - }); - }, - ), - cancelSubscriptionCallback: createAuthEndpoint( - "/subscription/cancel/callback", - { - method: "GET", - query: z.record(z.string(), z.any()).optional(), - use: [originCheck((ctx) => ctx.query.callbackURL)], - }, - async (ctx) => { - if (!ctx.query || !ctx.query.callbackURL || !ctx.query.subscriptionId) { - throw ctx.redirect(getUrl(ctx, ctx.query?.callbackURL || "/")); - } - const session = await getSessionFromCtx<{ stripeCustomerId: string }>( - ctx, - ); - if (!session) { - throw ctx.redirect(getUrl(ctx, ctx.query?.callbackURL || "/")); - } - const {user} = session; - const {callbackURL, subscriptionId} = ctx.query; + let priceIdToUse: string | undefined = undefined; + if (ctx.body.annual) { + priceIdToUse = plan.annualDiscountPriceId; + if (!priceIdToUse && plan.annualDiscountLookupKey) { + priceIdToUse = await resolvePriceIdFromLookupKey( + client, + plan.annualDiscountLookupKey, + ); + } + } else { + priceIdToUse = plan.priceId; + if (!priceIdToUse && plan.lookupKey) { + priceIdToUse = await resolvePriceIdFromLookupKey( + client, + plan.lookupKey, + ); + } + } + const checkoutSession = await client.checkout.sessions + .create( + { + ...(customerId + ? { + customer: customerId, + customer_update: { + name: "auto", + address: "auto", + }, + } + : { + customer_email: session.user.email, + }), + success_url: getUrl( + ctx, + `${ + ctx.context.baseURL + }/subscription/success?callbackURL=${encodeURIComponent( + ctx.body.successUrl, + )}&subscriptionId=${encodeURIComponent(subscription.id)}`, + ), + cancel_url: getUrl(ctx, ctx.body.cancelUrl), + line_items: [ + { + price: priceIdToUse, + quantity: ctx.body.seats || 1, + }, + ], + subscription_data: { + ...freeTrial, + }, + mode: "subscription", + client_reference_id: referenceId, + ...params?.params, + metadata: { + userId: user.id, + subscriptionId: subscription.id, + referenceId, + ...params?.params?.metadata, + }, + }, + params?.options, + ) + .catch(async (e) => { + throw ctx.error("BAD_REQUEST", { + message: e.message, + code: e.code, + }); + }); + return ctx.json({ + ...checkoutSession, + redirect: !ctx.body.disableRedirect, + }); + }, + ), + cancelSubscriptionCallback: createAuthEndpoint( + "/subscription/cancel/callback", + { + method: "GET", + query: z.record(z.string(), z.any()).optional(), + use: [originCheck((ctx) => ctx.query.callbackURL)], + }, + async (ctx) => { + if (!ctx.query || !ctx.query.callbackURL || !ctx.query.subscriptionId) { + throw ctx.redirect(getUrl(ctx, ctx.query?.callbackURL || "/")); + } + const session = await getSessionFromCtx<{ stripeCustomerId: string }>( + ctx, + ); + if (!session) { + throw ctx.redirect(getUrl(ctx, ctx.query?.callbackURL || "/")); + } + const { user } = session; + const { callbackURL, subscriptionId } = ctx.query; - if (user?.stripeCustomerId) { - try { - const subscription = - await ctx.context.adapter.findOne({ - model: "subscription", - where: [ - { - field: "id", - value: subscriptionId, - }, - ], - }); - if ( - !subscription || - subscription.cancelAtPeriodEnd || - subscription.status === "canceled" - ) { - throw ctx.redirect(getUrl(ctx, callbackURL)); - } + if (user?.stripeCustomerId) { + try { + const subscription = + await ctx.context.adapter.findOne({ + model: "subscription", + where: [ + { + field: "id", + value: subscriptionId, + }, + ], + }); + if ( + !subscription || + subscription.cancelAtPeriodEnd || + subscription.status === "canceled" + ) { + throw ctx.redirect(getUrl(ctx, callbackURL)); + } - const stripeSubscription = await client.subscriptions.list({ - customer: user.stripeCustomerId, - status: "active", - }); - const currentSubscription = stripeSubscription.data.find( - (sub) => sub.id === subscription.stripeSubscriptionId, - ); - if (currentSubscription?.cancel_at_period_end === true) { - await ctx.context.adapter.update({ - model: "subscription", - update: { - status: currentSubscription?.status, - cancelAtPeriodEnd: true, - }, - where: [ - { - field: "id", - value: subscription.id, - }, - ], - }); - await options.subscription?.onSubscriptionCancel?.({ - subscription, - cancellationDetails: currentSubscription.cancellation_details, - stripeSubscription: currentSubscription, - event: undefined, - }); - } - } catch (error) { - ctx.context.logger.error( - "Error checking subscription status from Stripe", - error, - ); - } - } - throw ctx.redirect(getUrl(ctx, callbackURL)); - }, - ), - /** - * ### Endpoint - * - * POST `/subscription/cancel` - * - * ### API Methods - * - * **server:** - * `auth.api.cancelSubscription` - * - * **client:** - * `authClient.subscription.cancel` - * - * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/stripe#api-method-subscription-cancel) - */ - cancelSubscription: createAuthEndpoint( - "/subscription/cancel", - { - method: "POST", - body: z.object({ - referenceId: z - .string() - .meta({ - description: - "Reference id of the subscription to cancel. Eg: '123'", - }) - .optional(), - subscriptionId: z - .string() - .meta({ - description: - "The id of the subscription to cancel. Eg: 'sub_123'", - }) - .optional(), - returnUrl: z.string().meta({ - description: - 'URL to take customers to when they click on the billing portal’s link to return to your website. Eg: "https://example.com/dashboard"', - }), - }), - use: [ - sessionMiddleware, - originCheck((ctx) => ctx.body.returnUrl), - referenceMiddleware("cancel-subscription"), - ], - }, - async (ctx) => { - const referenceId = - ctx.body?.referenceId || ctx.context.session.user.id; - const subscription = ctx.body.subscriptionId - ? await ctx.context.adapter.findOne({ - model: "subscription", - where: [ - { - field: "id", - value: ctx.body.subscriptionId, - }, - ], - }) - : await ctx.context.adapter - .findMany({ - model: "subscription", - where: [{field: "referenceId", value: referenceId}], - }) - .then((subs) => - subs.find( - (sub) => sub.status === "active" || sub.status === "trialing", - ), - ); + const stripeSubscription = await client.subscriptions.list({ + customer: user.stripeCustomerId, + status: "active", + }); + const currentSubscription = stripeSubscription.data.find( + (sub) => sub.id === subscription.stripeSubscriptionId, + ); + if (currentSubscription?.cancel_at_period_end === true) { + await ctx.context.adapter.update({ + model: "subscription", + update: { + status: currentSubscription?.status, + cancelAtPeriodEnd: true, + }, + where: [ + { + field: "id", + value: subscription.id, + }, + ], + }); + await options.subscription?.onSubscriptionCancel?.({ + subscription, + cancellationDetails: currentSubscription.cancellation_details, + stripeSubscription: currentSubscription, + event: undefined, + }); + } + } catch (error) { + ctx.context.logger.error( + "Error checking subscription status from Stripe", + error, + ); + } + } + throw ctx.redirect(getUrl(ctx, callbackURL)); + }, + ), + /** + * ### Endpoint + * + * POST `/subscription/cancel` + * + * ### API Methods + * + * **server:** + * `auth.api.cancelSubscription` + * + * **client:** + * `authClient.subscription.cancel` + * + * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/stripe#api-method-subscription-cancel) + */ + cancelSubscription: createAuthEndpoint( + "/subscription/cancel", + { + method: "POST", + body: z.object({ + referenceId: z + .string() + .meta({ + description: + "Reference id of the subscription to cancel. Eg: '123'", + }) + .optional(), + subscriptionId: z + .string() + .meta({ + description: + "The id of the subscription to cancel. Eg: 'sub_123'", + }) + .optional(), + returnUrl: z.string().meta({ + description: + 'URL to take customers to when they click on the billing portal’s link to return to your website. Eg: "https://example.com/dashboard"', + }), + }), + use: [ + sessionMiddleware, + originCheck((ctx) => ctx.body.returnUrl), + referenceMiddleware("cancel-subscription"), + ], + }, + async (ctx) => { + const referenceId = + ctx.body?.referenceId || ctx.context.session.user.id; + const subscription = ctx.body.subscriptionId + ? await ctx.context.adapter.findOne({ + model: "subscription", + where: [ + { + field: "id", + value: ctx.body.subscriptionId, + }, + ], + }) + : await ctx.context.adapter + .findMany({ + model: "subscription", + where: [{ field: "referenceId", value: referenceId }], + }) + .then((subs) => + subs.find( + (sub) => sub.status === "active" || sub.status === "trialing", + ), + ); - if (!subscription || !subscription.stripeCustomerId) { - throw ctx.error("BAD_REQUEST", { - message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND, - }); - } - const activeSubscriptions = await client.subscriptions - .list({ - customer: subscription.stripeCustomerId, - }) - .then((res) => - res.data.filter( - (sub) => sub.status === "active" || sub.status === "trialing", - ), - ); - if (!activeSubscriptions.length) { - /** - * If the subscription is not found, we need to delete the subscription - * from the database. This is a rare case and should not happen. - */ - await ctx.context.adapter.deleteMany({ - model: "subscription", - where: [ - { - field: "referenceId", - value: referenceId, - }, - ], - }); - throw ctx.error("BAD_REQUEST", { - message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND, - }); - } - const activeSubscription = activeSubscriptions.find( - (sub) => sub.id === subscription.stripeSubscriptionId, - ); - if (!activeSubscription) { - throw ctx.error("BAD_REQUEST", { - message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND, - }); - } - const {url} = await client.billingPortal.sessions - .create({ - customer: subscription.stripeCustomerId, - return_url: getUrl( - ctx, - `${ - ctx.context.baseURL - }/subscription/cancel/callback?callbackURL=${encodeURIComponent( - ctx.body?.returnUrl || "/", - )}&subscriptionId=${encodeURIComponent(subscription.id)}`, - ), - flow_data: { - type: "subscription_cancel", - subscription_cancel: { - subscription: activeSubscription.id, - }, - }, - }) - .catch(async (e) => { - if (e.message.includes("already set to be cancel")) { - /** - * incase we missed the event from stripe, we set it manually - * this is a rare case and should not happen - */ - if (!subscription.cancelAtPeriodEnd) { - await ctx.context.adapter.update({ - model: "subscription", - update: { - cancelAtPeriodEnd: true, - }, - where: [ - { - field: "referenceId", - value: referenceId, - }, - ], - }); - } - } - throw ctx.error("BAD_REQUEST", { - message: e.message, - code: e.code, - }); - }); - return { - url, - redirect: true, - }; - }, - ), - restoreSubscription: createAuthEndpoint( - "/subscription/restore", - { - method: "POST", - body: z.object({ - referenceId: z - .string() - .meta({ - description: - "Reference id of the subscription to restore. Eg: '123'", - }) - .optional(), - subscriptionId: z - .string() - .meta({ - description: - "The id of the subscription to restore. Eg: 'sub_123'", - }) - .optional(), - }), - use: [sessionMiddleware, referenceMiddleware("restore-subscription")], - }, - async (ctx) => { - const referenceId = - ctx.body?.referenceId || ctx.context.session.user.id; + if (!subscription || !subscription.stripeCustomerId) { + throw ctx.error("BAD_REQUEST", { + message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND, + }); + } + const activeSubscriptions = await client.subscriptions + .list({ + customer: subscription.stripeCustomerId, + }) + .then((res) => + res.data.filter( + (sub) => sub.status === "active" || sub.status === "trialing", + ), + ); + if (!activeSubscriptions.length) { + /** + * If the subscription is not found, we need to delete the subscription + * from the database. This is a rare case and should not happen. + */ + await ctx.context.adapter.deleteMany({ + model: "subscription", + where: [ + { + field: "referenceId", + value: referenceId, + }, + ], + }); + throw ctx.error("BAD_REQUEST", { + message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND, + }); + } + const activeSubscription = activeSubscriptions.find( + (sub) => sub.id === subscription.stripeSubscriptionId, + ); + if (!activeSubscription) { + throw ctx.error("BAD_REQUEST", { + message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND, + }); + } + const { url } = await client.billingPortal.sessions + .create({ + customer: subscription.stripeCustomerId, + return_url: getUrl( + ctx, + `${ + ctx.context.baseURL + }/subscription/cancel/callback?callbackURL=${encodeURIComponent( + ctx.body?.returnUrl || "/", + )}&subscriptionId=${encodeURIComponent(subscription.id)}`, + ), + flow_data: { + type: "subscription_cancel", + subscription_cancel: { + subscription: activeSubscription.id, + }, + }, + }) + .catch(async (e) => { + if (e.message.includes("already set to be cancel")) { + /** + * incase we missed the event from stripe, we set it manually + * this is a rare case and should not happen + */ + if (!subscription.cancelAtPeriodEnd) { + await ctx.context.adapter.update({ + model: "subscription", + update: { + cancelAtPeriodEnd: true, + }, + where: [ + { + field: "referenceId", + value: referenceId, + }, + ], + }); + } + } + throw ctx.error("BAD_REQUEST", { + message: e.message, + code: e.code, + }); + }); + return { + url, + redirect: true, + }; + }, + ), + restoreSubscription: createAuthEndpoint( + "/subscription/restore", + { + method: "POST", + body: z.object({ + referenceId: z + .string() + .meta({ + description: + "Reference id of the subscription to restore. Eg: '123'", + }) + .optional(), + subscriptionId: z + .string() + .meta({ + description: + "The id of the subscription to restore. Eg: 'sub_123'", + }) + .optional(), + }), + use: [sessionMiddleware, referenceMiddleware("restore-subscription")], + }, + async (ctx) => { + const referenceId = + ctx.body?.referenceId || ctx.context.session.user.id; - const subscription = ctx.body.subscriptionId - ? await ctx.context.adapter.findOne({ - model: "subscription", - where: [ - { - field: "id", - value: ctx.body.subscriptionId, - }, - ], - }) - : await ctx.context.adapter - .findMany({ - model: "subscription", - where: [ - { - field: "referenceId", - value: referenceId, - }, - ], - }) - .then((subs) => - subs.find( - (sub) => sub.status === "active" || sub.status === "trialing", - ), - ); - if (!subscription || !subscription.stripeCustomerId) { - throw ctx.error("BAD_REQUEST", { - message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND, - }); - } - if ( - subscription.status != "active" && - subscription.status != "trialing" - ) { - throw ctx.error("BAD_REQUEST", { - message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_ACTIVE, - }); - } - if (!subscription.cancelAtPeriodEnd) { - throw ctx.error("BAD_REQUEST", { - message: - STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION, - }); - } + const subscription = ctx.body.subscriptionId + ? await ctx.context.adapter.findOne({ + model: "subscription", + where: [ + { + field: "id", + value: ctx.body.subscriptionId, + }, + ], + }) + : await ctx.context.adapter + .findMany({ + model: "subscription", + where: [ + { + field: "referenceId", + value: referenceId, + }, + ], + }) + .then((subs) => + subs.find( + (sub) => sub.status === "active" || sub.status === "trialing", + ), + ); + if (!subscription || !subscription.stripeCustomerId) { + throw ctx.error("BAD_REQUEST", { + message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND, + }); + } + if ( + subscription.status != "active" && + subscription.status != "trialing" + ) { + throw ctx.error("BAD_REQUEST", { + message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_ACTIVE, + }); + } + if (!subscription.cancelAtPeriodEnd) { + throw ctx.error("BAD_REQUEST", { + message: + STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION, + }); + } - const activeSubscription = await client.subscriptions - .list({ - customer: subscription.stripeCustomerId, - }) - .then( - (res) => - res.data.filter( - (sub) => sub.status === "active" || sub.status === "trialing", - )[0], - ); - if (!activeSubscription) { - throw ctx.error("BAD_REQUEST", { - message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND, - }); - } + const activeSubscription = await client.subscriptions + .list({ + customer: subscription.stripeCustomerId, + }) + .then( + (res) => + res.data.filter( + (sub) => sub.status === "active" || sub.status === "trialing", + )[0], + ); + if (!activeSubscription) { + throw ctx.error("BAD_REQUEST", { + message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND, + }); + } - try { - const newSub = await client.subscriptions.update( - activeSubscription.id, - { - cancel_at_period_end: false, - }, - ); + try { + const newSub = await client.subscriptions.update( + activeSubscription.id, + { + cancel_at_period_end: false, + }, + ); - await ctx.context.adapter.update({ - model: "subscription", - update: { - cancelAtPeriodEnd: false, - updatedAt: new Date(), - }, - where: [ - { - field: "id", - value: subscription.id, - }, - ], - }); + await ctx.context.adapter.update({ + model: "subscription", + update: { + cancelAtPeriodEnd: false, + updatedAt: new Date(), + }, + where: [ + { + field: "id", + value: subscription.id, + }, + ], + }); - return ctx.json(newSub); - } catch (error) { - ctx.context.logger.error("Error restoring subscription", error); - throw new APIError("BAD_REQUEST", { - message: STRIPE_ERROR_CODES.UNABLE_TO_CREATE_CUSTOMER, - }); - } - }, - ), - /** - * ### Endpoint - * - * GET `/subscription/list` - * - * ### API Methods - * - * **server:** - * `auth.api.listActiveSubscriptions` - * - * **client:** - * `authClient.subscription.list` - * - * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/stripe#api-method-subscription-list) - */ - listActiveSubscriptions: createAuthEndpoint( - "/subscription/list", - { - method: "GET", - query: z.optional( - z.object({ - referenceId: z - .string() - .meta({ - description: - "Reference id of the subscription to list. Eg: '123'", - }) - .optional(), - }), - ), - use: [sessionMiddleware, referenceMiddleware("list-subscription")], - }, - async (ctx) => { - const subscriptions = await ctx.context.adapter.findMany({ - model: "subscription", - where: [ - { - field: "referenceId", - value: ctx.query?.referenceId || ctx.context.session.user.id, - }, - ], - }); - if (!subscriptions.length) { - return []; - } - const plans = await getPlans(options); - if (!plans) { - return []; - } - const subs = subscriptions - .map((sub) => { - const plan = plans.find( - (p) => p.name.toLowerCase() === sub.plan.toLowerCase(), - ); - return { - ...sub, - limits: plan?.limits, - priceId: plan?.priceId, - }; - }) - .filter((sub) => { - return sub.status === "active" || sub.status === "trialing"; - }); - return ctx.json(subs); - }, - ), - subscriptionSuccess: createAuthEndpoint( - "/subscription/success", - { - method: "GET", - query: z.record(z.string(), z.any()).optional(), - use: [originCheck((ctx) => ctx.query.callbackURL)], - }, - async (ctx) => { - if (!ctx.query || !ctx.query.callbackURL || !ctx.query.subscriptionId) { - throw ctx.redirect(getUrl(ctx, ctx.query?.callbackURL || "/")); - } - const session = await getSessionFromCtx<{ stripeCustomerId: string }>( - ctx, - ); - if (!session) { - throw ctx.redirect(getUrl(ctx, ctx.query?.callbackURL || "/")); - } - const {user} = session; - const {callbackURL, subscriptionId} = ctx.query; + return ctx.json(newSub); + } catch (error) { + ctx.context.logger.error("Error restoring subscription", error); + throw new APIError("BAD_REQUEST", { + message: STRIPE_ERROR_CODES.UNABLE_TO_CREATE_CUSTOMER, + }); + } + }, + ), + /** + * ### Endpoint + * + * GET `/subscription/list` + * + * ### API Methods + * + * **server:** + * `auth.api.listActiveSubscriptions` + * + * **client:** + * `authClient.subscription.list` + * + * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/stripe#api-method-subscription-list) + */ + listActiveSubscriptions: createAuthEndpoint( + "/subscription/list", + { + method: "GET", + query: z.optional( + z.object({ + referenceId: z + .string() + .meta({ + description: + "Reference id of the subscription to list. Eg: '123'", + }) + .optional(), + }), + ), + use: [sessionMiddleware, referenceMiddleware("list-subscription")], + }, + async (ctx) => { + const subscriptions = await ctx.context.adapter.findMany({ + model: "subscription", + where: [ + { + field: "referenceId", + value: ctx.query?.referenceId || ctx.context.session.user.id, + }, + ], + }); + if (!subscriptions.length) { + return []; + } + const plans = await getPlans(options); + if (!plans) { + return []; + } + const subs = subscriptions + .map((sub) => { + const plan = plans.find( + (p) => p.name.toLowerCase() === sub.plan.toLowerCase(), + ); + return { + ...sub, + limits: plan?.limits, + priceId: plan?.priceId, + }; + }) + .filter((sub) => { + return sub.status === "active" || sub.status === "trialing"; + }); + return ctx.json(subs); + }, + ), + subscriptionSuccess: createAuthEndpoint( + "/subscription/success", + { + method: "GET", + query: z.record(z.string(), z.any()).optional(), + use: [originCheck((ctx) => ctx.query.callbackURL)], + }, + async (ctx) => { + if (!ctx.query || !ctx.query.callbackURL || !ctx.query.subscriptionId) { + throw ctx.redirect(getUrl(ctx, ctx.query?.callbackURL || "/")); + } + const session = await getSessionFromCtx<{ stripeCustomerId: string }>( + ctx, + ); + if (!session) { + throw ctx.redirect(getUrl(ctx, ctx.query?.callbackURL || "/")); + } + const { user } = session; + const { callbackURL, subscriptionId } = ctx.query; - const subscription = await ctx.context.adapter.findOne({ - model: "subscription", - where: [ - { - field: "id", - value: subscriptionId, - }, - ], - }); + const subscription = await ctx.context.adapter.findOne({ + model: "subscription", + where: [ + { + field: "id", + value: subscriptionId, + }, + ], + }); - if ( - subscription?.status === "active" || - subscription?.status === "trialing" - ) { - return ctx.redirect(getUrl(ctx, callbackURL)); - } - const customerId = - subscription?.stripeCustomerId || user.stripeCustomerId; + if ( + subscription?.status === "active" || + subscription?.status === "trialing" + ) { + return ctx.redirect(getUrl(ctx, callbackURL)); + } + const customerId = + subscription?.stripeCustomerId || user.stripeCustomerId; - if (customerId) { - try { - const stripeSubscription = await client.subscriptions - .list({ - customer: customerId, - status: "active", - }) - .then((res) => res.data[0]); + if (customerId) { + try { + const stripeSubscription = await client.subscriptions + .list({ + customer: customerId, + status: "active", + }) + .then((res) => res.data[0]); - if (stripeSubscription) { - const plan = await getPlanByPriceInfo( - options, - stripeSubscription.items.data[0]?.price.id!, - stripeSubscription.items.data[0]?.price.lookup_key!, - ); + if (stripeSubscription) { + const plan = await getPlanByPriceInfo( + options, + stripeSubscription.items.data[0]?.price.id!, + stripeSubscription.items.data[0]?.price.lookup_key!, + ); - if (plan && subscription) { - await ctx.context.adapter.update({ - model: "subscription", - update: { - status: stripeSubscription.status, - seats: stripeSubscription.items.data[0]?.quantity || 1, - plan: plan.name.toLowerCase(), - periodEnd: new Date( - stripeSubscription.items.data[0]?.current_period_end! * - 1000, - ), - periodStart: new Date( - stripeSubscription.items.data[0]?.current_period_start! * - 1000, - ), - stripeSubscriptionId: stripeSubscription.id, - ...(stripeSubscription.trial_start && - stripeSubscription.trial_end - ? { - trialStart: new Date( - stripeSubscription.trial_start * 1000, - ), - trialEnd: new Date( - stripeSubscription.trial_end * 1000, - ), - } - : {}), - }, - where: [ - { - field: "id", - value: subscription.id, - }, - ], - }); - } - } - } catch (error) { - ctx.context.logger.error( - "Error fetching subscription from Stripe", - error, - ); - } - } - throw ctx.redirect(getUrl(ctx, callbackURL)); - }, - ), - createBillingPortal: createAuthEndpoint( - "/subscription/billing-portal", - { - method: "POST", - body: z.object({ - locale: z - .custom((localization) => { - return typeof localization === "string"; - }) - .optional(), - referenceId: z.string().optional(), - returnUrl: z.string().default("/"), - }), - use: [ - sessionMiddleware, - originCheck((ctx) => ctx.body.returnUrl), - referenceMiddleware("billing-portal"), - ], - }, - async (ctx) => { - const {user} = ctx.context.session; - const referenceId = ctx.body.referenceId || user.id; + if (plan && subscription) { + await ctx.context.adapter.update({ + model: "subscription", + update: { + status: stripeSubscription.status, + seats: stripeSubscription.items.data[0]?.quantity || 1, + plan: plan.name.toLowerCase(), + periodEnd: new Date( + stripeSubscription.items.data[0]?.current_period_end! * + 1000, + ), + periodStart: new Date( + stripeSubscription.items.data[0]?.current_period_start! * + 1000, + ), + stripeSubscriptionId: stripeSubscription.id, + ...(stripeSubscription.trial_start && + stripeSubscription.trial_end + ? { + trialStart: new Date( + stripeSubscription.trial_start * 1000, + ), + trialEnd: new Date( + stripeSubscription.trial_end * 1000, + ), + } + : {}), + }, + where: [ + { + field: "id", + value: subscription.id, + }, + ], + }); + } + } + } catch (error) { + ctx.context.logger.error( + "Error fetching subscription from Stripe", + error, + ); + } + } + throw ctx.redirect(getUrl(ctx, callbackURL)); + }, + ), + createBillingPortal: createAuthEndpoint( + "/subscription/billing-portal", + { + method: "POST", + body: z.object({ + locale: z + .custom((localization) => { + return typeof localization === "string"; + }) + .optional(), + referenceId: z.string().optional(), + returnUrl: z.string().default("/"), + }), + use: [ + sessionMiddleware, + originCheck((ctx) => ctx.body.returnUrl), + referenceMiddleware("billing-portal"), + ], + }, + async (ctx) => { + const { user } = ctx.context.session; + const referenceId = ctx.body.referenceId || user.id; - let customerId = user.stripeCustomerId; + let customerId = user.stripeCustomerId; - if (!customerId) { - const subscription = await ctx.context.adapter - .findMany({ - model: "subscription", - where: [ - { - field: "referenceId", - value: referenceId, - }, - ], - }) - .then((subs) => - subs.find( - (sub) => sub.status === "active" || sub.status === "trialing", - ), - ); + if (!customerId) { + const subscription = await ctx.context.adapter + .findMany({ + model: "subscription", + where: [ + { + field: "referenceId", + value: referenceId, + }, + ], + }) + .then((subs) => + subs.find( + (sub) => sub.status === "active" || sub.status === "trialing", + ), + ); - customerId = subscription?.stripeCustomerId; - } + customerId = subscription?.stripeCustomerId; + } - if (!customerId) { - throw new APIError("BAD_REQUEST", { - message: "No Stripe customer found for this user", - }); - } + if (!customerId) { + throw new APIError("BAD_REQUEST", { + message: "No Stripe customer found for this user", + }); + } - try { - const {url} = await client.billingPortal.sessions.create({ - locale: ctx.body.locale, - customer: customerId, - return_url: getUrl(ctx, ctx.body.returnUrl), - }); + try { + const { url } = await client.billingPortal.sessions.create({ + locale: ctx.body.locale, + customer: customerId, + return_url: getUrl(ctx, ctx.body.returnUrl), + }); - return ctx.json({ - url, - redirect: true, - }); - } catch (error: any) { - ctx.context.logger.error( - "Error creating billing portal session", - error, - ); - throw new APIError("BAD_REQUEST", { - message: error.message, - }); - } - }, - ), + return ctx.json({ + url, + redirect: true, + }); + } catch (error: any) { + ctx.context.logger.error( + "Error creating billing portal session", + error, + ); + throw new APIError("BAD_REQUEST", { + message: error.message, + }); + } + }, + ), reportBillingMeterEvent: createAuthEndpoint( "/subscription/meter-event", // https://docs.stripe.com/api/billing/meter-event/create { @@ -1229,10 +1228,14 @@ export const stripe = (options: O) : BetterAuthPlugin = }), identifier: z.string().optional(), referenceId: z.string().optional(), - }) + }), + use: [ + sessionMiddleware, + referenceMiddleware("meter-event"), + ] }, async (ctx) => { - const {user} = ctx.context.session; + const { user } = ctx.context.session; let customerId = user.stripeCustomerId; const referenceId = ctx.body.referenceId || user.id; @@ -1290,190 +1293,190 @@ export const stripe = (options: O) : BetterAuthPlugin = } } ), - } as const; - return { - id: "stripe", - endpoints: { - stripeWebhook: createAuthEndpoint( - "/stripe/webhook", - { - method: "POST", - metadata: { - isAction: false, - }, - cloneRequest: true, - //don't parse the body - disableBody: true, - }, - async (ctx) => { - if (!ctx.request?.body) { - throw new APIError("INTERNAL_SERVER_ERROR"); - } - const buf = await ctx.request.text(); - const sig = ctx.request.headers.get("stripe-signature") as string; - const webhookSecret = options.stripeWebhookSecret; - let event: Stripe.Event; - try { - if (!sig || !webhookSecret) { - throw new APIError("BAD_REQUEST", { - message: "Stripe webhook secret not found", - }); - } - event = await client.webhooks.constructEventAsync( - buf, - sig, - webhookSecret, - ); - } catch (err: any) { - ctx.context.logger.error(`${err.message}`); - throw new APIError("BAD_REQUEST", { - message: `Webhook Error: ${err.message}`, - }); - } - if (!event) { - throw new APIError("BAD_REQUEST", { - message: "Failed to construct event", - }); - } - try { - switch (event.type) { - case "checkout.session.completed": - await onCheckoutSessionCompleted(ctx, options, event); - await options.onEvent?.(event); - break; - case "customer.subscription.updated": - await onSubscriptionUpdated(ctx, options, event); - await options.onEvent?.(event); - break; - case "customer.subscription.deleted": - await onSubscriptionDeleted(ctx, options, event); - await options.onEvent?.(event); - break; - default: - await options.onEvent?.(event); - break; - } - } catch (e: any) { - ctx.context.logger.error( - `Stripe webhook failed. Error: ${e.message}`, - ); - throw new APIError("BAD_REQUEST", { - message: "Webhook error: See server logs for more information.", - }); - } - return ctx.json({success: true}); - }, - ), - ...((options.subscription?.enabled - ? subscriptionEndpoints - : {}) as O["subscription"] extends { - enabled: boolean; - } - ? typeof subscriptionEndpoints - : {}), - }, - init(ctx) { - return { - options: { - databaseHooks: { - user: { - create: { - async after(user, ctx) { - if (ctx && options.createCustomerOnSignUp) { - let extraCreateParams: Partial = - {}; - if (options.getCustomerCreateParams) { - extraCreateParams = await options.getCustomerCreateParams( - user, - ctx, - ); - } + } as const; + return { + id: "stripe", + endpoints: { + stripeWebhook: createAuthEndpoint( + "/stripe/webhook", + { + method: "POST", + metadata: { + isAction: false, + }, + cloneRequest: true, + //don't parse the body + disableBody: true, + }, + async (ctx) => { + if (!ctx.request?.body) { + throw new APIError("INTERNAL_SERVER_ERROR"); + } + const buf = await ctx.request.text(); + const sig = ctx.request.headers.get("stripe-signature") as string; + const webhookSecret = options.stripeWebhookSecret; + let event: Stripe.Event; + try { + if (!sig || !webhookSecret) { + throw new APIError("BAD_REQUEST", { + message: "Stripe webhook secret not found", + }); + } + event = await client.webhooks.constructEventAsync( + buf, + sig, + webhookSecret, + ); + } catch (err: any) { + ctx.context.logger.error(`${err.message}`); + throw new APIError("BAD_REQUEST", { + message: `Webhook Error: ${err.message}`, + }); + } + if (!event) { + throw new APIError("BAD_REQUEST", { + message: "Failed to construct event", + }); + } + try { + switch (event.type) { + case "checkout.session.completed": + await onCheckoutSessionCompleted(ctx, options, event); + await options.onEvent?.(event); + break; + case "customer.subscription.updated": + await onSubscriptionUpdated(ctx, options, event); + await options.onEvent?.(event); + break; + case "customer.subscription.deleted": + await onSubscriptionDeleted(ctx, options, event); + await options.onEvent?.(event); + break; + default: + await options.onEvent?.(event); + break; + } + } catch (e: any) { + ctx.context.logger.error( + `Stripe webhook failed. Error: ${e.message}`, + ); + throw new APIError("BAD_REQUEST", { + message: "Webhook error: See server logs for more information.", + }); + } + return ctx.json({ success: true }); + }, + ), + ...((options.subscription?.enabled + ? subscriptionEndpoints + : {}) as O["subscription"] extends { + enabled: boolean; + } + ? typeof subscriptionEndpoints + : {}), + }, + init(ctx) { + return { + options: { + databaseHooks: { + user: { + create: { + async after(user, ctx) { + if (ctx && options.createCustomerOnSignUp) { + let extraCreateParams: Partial = + {}; + if (options.getCustomerCreateParams) { + extraCreateParams = await options.getCustomerCreateParams( + user, + ctx, + ); + } - const params: Stripe.CustomerCreateParams = defu( - { - email: user.email, - name: user.name, - metadata: { - userId: user.id, - }, - }, - extraCreateParams, - ); - const stripeCustomer = - await client.customers.create(params); - await ctx.context.internalAdapter.updateUser(user.id, { - stripeCustomerId: stripeCustomer.id, - }); - await options.onCustomerCreate?.( - { - stripeCustomer, - user: { - ...user, - stripeCustomerId: stripeCustomer.id, - }, - }, - ctx, - ); - } - }, - }, - update: { - async after(user, ctx) { - if (!ctx) return; + const params: Stripe.CustomerCreateParams = defu( + { + email: user.email, + name: user.name, + metadata: { + userId: user.id, + }, + }, + extraCreateParams, + ); + const stripeCustomer = + await client.customers.create(params); + await ctx.context.internalAdapter.updateUser(user.id, { + stripeCustomerId: stripeCustomer.id, + }); + await options.onCustomerCreate?.( + { + stripeCustomer, + user: { + ...user, + stripeCustomerId: stripeCustomer.id, + }, + }, + ctx, + ); + } + }, + }, + update: { + async after(user, ctx) { + if (!ctx) return; - try { - // Cast user to include stripeCustomerId (added by the stripe plugin schema) - const userWithStripe = user as typeof user & { - stripeCustomerId?: string; - }; + try { + // Cast user to include stripeCustomerId (added by the stripe plugin schema) + const userWithStripe = user as typeof user & { + stripeCustomerId?: string; + }; - // Only proceed if user has a Stripe customer ID - if (!userWithStripe.stripeCustomerId) return; + // Only proceed if user has a Stripe customer ID + if (!userWithStripe.stripeCustomerId) return; - // Get the user from the database to check if email actually changed - // The 'user' parameter here is the freshly updated user - // We need to check if the Stripe customer's email matches - const stripeCustomer = await client.customers.retrieve( - userWithStripe.stripeCustomerId, - ); + // Get the user from the database to check if email actually changed + // The 'user' parameter here is the freshly updated user + // We need to check if the Stripe customer's email matches + const stripeCustomer = await client.customers.retrieve( + userWithStripe.stripeCustomerId, + ); - // Check if customer was deleted - if (stripeCustomer.deleted) { - ctx.context.logger.warn( - `Stripe customer ${userWithStripe.stripeCustomerId} was deleted, cannot update email`, - ); - return; - } + // Check if customer was deleted + if (stripeCustomer.deleted) { + ctx.context.logger.warn( + `Stripe customer ${userWithStripe.stripeCustomerId} was deleted, cannot update email`, + ); + return; + } - // If Stripe customer email doesn't match the user's current email, update it - if (stripeCustomer.email !== user.email) { - await client.customers.update( - userWithStripe.stripeCustomerId, - { - email: user.email, - }, - ); - ctx.context.logger.info( - `Updated Stripe customer email from ${stripeCustomer.email} to ${user.email}`, - ); - } - } catch (e: any) { - // Ignore errors - this is a best-effort sync - // Email might have been deleted or Stripe customer might not exist - ctx.context.logger.error( - `Failed to sync email to Stripe customer: ${e.message}`, - e, - ); - } - }, - }, - }, - }, - }, - }; - }, - schema: getSchema(options), - } satisfies BetterAuthPlugin; + // If Stripe customer email doesn't match the user's current email, update it + if (stripeCustomer.email !== user.email) { + await client.customers.update( + userWithStripe.stripeCustomerId, + { + email: user.email, + }, + ); + ctx.context.logger.info( + `Updated Stripe customer email from ${stripeCustomer.email} to ${user.email}`, + ); + } + } catch (e: any) { + // Ignore errors - this is a best-effort sync + // Email might have been deleted or Stripe customer might not exist + ctx.context.logger.error( + `Failed to sync email to Stripe customer: ${e.message}`, + e, + ); + } + }, + }, + }, + }, + }, + }; + }, + schema: getSchema(options), + } satisfies BetterAuthPlugin; }; -export type {Subscription, StripePlan}; +export type { Subscription, StripePlan }; diff --git a/tsconfig.json b/tsconfig.json index 51c9211..713d96d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,35 @@ { - "extends": "../../tsconfig.json", "compilerOptions": { + "strict": true, + "target": "esnext", + "module": "esnext", + "moduleResolution": "bundler", + "downlevelIteration": true, + "baseUrl": ".", + "esModuleInterop": true, + "skipLibCheck": true, + "verbatimModuleSyntax": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": false, + "incremental": true, + "noErrorTruncation": true, + "types": [ + "node", + "bun" + ], "rootDir": "./src", "outDir": "./dist", - "lib": ["esnext", "dom", "dom.iterable"], - "moduleResolution": "bundler" + "lib": [ + "esnext", + "dom", + "dom.iterable" + ] }, - "include": ["src"] + "exclude": [ + "**/dist/**", + "**/node_modules/**" + ], + "include": [ + "src" + ] }