From 2ff4a999faf5c3135e91b1afea411af6e1177635 Mon Sep 17 00:00:00 2001
From: Yudi Xiao <463708580@qq.com>
Date: Mon, 13 Oct 2025 17:02:16 +0800
Subject: [PATCH] modify stripe client
---
.gitignore | 2 +
.idea/.gitignore | 8 +
.idea/modules.xml | 8 +
.idea/stripe-bw.iml | 12 +
.idea/vcs.xml | 6 +
CHANGELOG.md | 22 +
build.config.ts | 12 +
bun.lock | 561 ++++++++++++++
package.json | 61 ++
src/client.ts | 36 +
src/hooks.ts | 258 +++++++
src/index.ts | 1479 ++++++++++++++++++++++++++++++++++++
src/schema.ts | 92 +++
src/stripe.test.ts | 1742 +++++++++++++++++++++++++++++++++++++++++++
src/types.ts | 331 ++++++++
src/utils.ts | 30 +
tsconfig.json | 10 +
vitest.config.ts | 9 +
18 files changed, 4679 insertions(+)
create mode 100644 .gitignore
create mode 100644 .idea/.gitignore
create mode 100644 .idea/modules.xml
create mode 100644 .idea/stripe-bw.iml
create mode 100644 .idea/vcs.xml
create mode 100644 CHANGELOG.md
create mode 100644 build.config.ts
create mode 100644 bun.lock
create mode 100644 package.json
create mode 100644 src/client.ts
create mode 100644 src/hooks.ts
create mode 100644 src/index.ts
create mode 100644 src/schema.ts
create mode 100644 src/stripe.test.ts
create mode 100644 src/types.ts
create mode 100644 src/utils.ts
create mode 100644 tsconfig.json
create mode 100644 vitest.config.ts
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b0a5c34
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/node_modules/
+/dist/
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..35410ca
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
+# 基于编辑器的 HTTP 客户端请求
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..af7841a
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/stripe-bw.iml b/.idea/stripe-bw.iml
new file mode 100644
index 0000000..24643cc
--- /dev/null
+++ b/.idea/stripe-bw.iml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..9e9eb3e
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,22 @@
+# @better-auth/stripe
+
+## 1.3.4
+
+### Patch Changes
+
+- ac6baba: chore: fix typo on `freeTrial`
+- c2fb1aa: Fix duplicate trials when switching plans
+- 2bd2fa9: Added support for listing organization members with pagination, sorting, and filtering, and improved client inference for additional organization fields. Also fixed date handling in rate limits and tokens, improved Notion OAuth user extraction, and ensured session is always set in context.
+
+ Organization
+
+ - Added listMembers API with pagination, sorting, and filtering.
+ - Added membersLimit param to getFullOrganization.
+ - Improved client inference for additional fields in organization schemas.
+ - Bug Fixes
+ - Fixed date handling by casting DB values to Date objects before using date methods.
+ - Fixed Notion OAuth to extract user info correctly.
+ - Ensured session is set in context when reading from cookie cach
+
+- Updated dependencies [2bd2fa9]
+ - better-auth@1.3.4
diff --git a/build.config.ts b/build.config.ts
new file mode 100644
index 0000000..2b1a140
--- /dev/null
+++ b/build.config.ts
@@ -0,0 +1,12 @@
+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"],
+});
diff --git a/bun.lock b/bun.lock
new file mode 100644
index 0000000..7658a71
--- /dev/null
+++ b/bun.lock
@@ -0,0 +1,561 @@
+{
+ "lockfileVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "@better-auth-bowong/stripe",
+ "dependencies": {
+ "defu": "^6.1.4",
+ "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",
+ },
+ "peerDependencies": {
+ "@better-auth/core": "^1.4.0-beta.9",
+ "better-auth": "^1.4.0-beta.9",
+ "stripe": "^19",
+ },
+ },
+ },
+ "packages": {
+ "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
+
+ "@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/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="],
+
+ "@better-fetch/fetch": ["@better-fetch/fetch@1.1.18", "", {}, "sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA=="],
+
+ "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.10", "", { "os": "aix", "cpu": "ppc64" }, "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw=="],
+
+ "@esbuild/android-arm": ["@esbuild/android-arm@0.25.10", "", { "os": "android", "cpu": "arm" }, "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w=="],
+
+ "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.10", "", { "os": "android", "cpu": "arm64" }, "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg=="],
+
+ "@esbuild/android-x64": ["@esbuild/android-x64@0.25.10", "", { "os": "android", "cpu": "x64" }, "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg=="],
+
+ "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA=="],
+
+ "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg=="],
+
+ "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.10", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg=="],
+
+ "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.10", "", { "os": "freebsd", "cpu": "x64" }, "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA=="],
+
+ "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.10", "", { "os": "linux", "cpu": "arm" }, "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg=="],
+
+ "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ=="],
+
+ "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.10", "", { "os": "linux", "cpu": "ia32" }, "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ=="],
+
+ "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.10", "", { "os": "linux", "cpu": "none" }, "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg=="],
+
+ "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.10", "", { "os": "linux", "cpu": "none" }, "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA=="],
+
+ "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.10", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA=="],
+
+ "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.10", "", { "os": "linux", "cpu": "none" }, "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA=="],
+
+ "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.10", "", { "os": "linux", "cpu": "s390x" }, "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew=="],
+
+ "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.10", "", { "os": "linux", "cpu": "x64" }, "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA=="],
+
+ "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.10", "", { "os": "none", "cpu": "arm64" }, "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A=="],
+
+ "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.10", "", { "os": "none", "cpu": "x64" }, "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig=="],
+
+ "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.10", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw=="],
+
+ "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.10", "", { "os": "openbsd", "cpu": "x64" }, "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw=="],
+
+ "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.10", "", { "os": "none", "cpu": "arm64" }, "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag=="],
+
+ "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.10", "", { "os": "sunos", "cpu": "x64" }, "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ=="],
+
+ "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw=="],
+
+ "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.10", "", { "os": "win32", "cpu": "ia32" }, "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw=="],
+
+ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.10", "", { "os": "win32", "cpu": "x64" }, "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw=="],
+
+ "@hexagon/base64": ["@hexagon/base64@1.1.28", "", {}, "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw=="],
+
+ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
+
+ "@levischuck/tiny-cbor": ["@levischuck/tiny-cbor@0.2.11", "", {}, "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow=="],
+
+ "@noble/ciphers": ["@noble/ciphers@2.0.1", "", {}, "sha512-xHK3XHPUW8DTAobU+G0XT+/w+JLM7/8k1UFdB5xg/zTFPnFCobhftzw8wl4Lw2aq/Rvir5pxfZV5fEazmeCJ2g=="],
+
+ "@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="],
+
+ "@peculiar/asn1-android": ["@peculiar/asn1-android@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-t8A83hgghWQkcneRsgGs2ebAlRe54ns88p7ouv8PW2tzF1nAW4yHcL4uZKrFpIU+uszIRzTkcCuie37gpkId0A=="],
+
+ "@peculiar/asn1-cms": ["@peculiar/asn1-cms@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "@peculiar/asn1-x509-attr": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-p0SjJ3TuuleIvjPM4aYfvYw8Fk1Hn/zAVyPJZTtZ2eE9/MIer6/18ROxX6N/e6edVSfvuZBqhxAj3YgsmSjQ/A=="],
+
+ "@peculiar/asn1-csr": ["@peculiar/asn1-csr@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-ioigvA6WSYN9h/YssMmmoIwgl3RvZlAYx4A/9jD2qaqXZwGcNlAxaw54eSx2QG1Yu7YyBC5Rku3nNoHrQ16YsQ=="],
+
+ "@peculiar/asn1-ecc": ["@peculiar/asn1-ecc@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-t4eYGNhXtLRxaP50h3sfO6aJebUCDGQACoeexcelL4roMFRRVgB20yBIu2LxsPh/tdW9I282gNgMOyg3ywg/mg=="],
+
+ "@peculiar/asn1-pfx": ["@peculiar/asn1-pfx@2.5.0", "", { "dependencies": { "@peculiar/asn1-cms": "^2.5.0", "@peculiar/asn1-pkcs8": "^2.5.0", "@peculiar/asn1-rsa": "^2.5.0", "@peculiar/asn1-schema": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-Vj0d0wxJZA+Ztqfb7W+/iu8Uasw6hhKtCdLKXLG/P3kEPIQpqGI4P4YXlROfl7gOCqFIbgsj1HzFIFwQ5s20ug=="],
+
+ "@peculiar/asn1-pkcs8": ["@peculiar/asn1-pkcs8@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-L7599HTI2SLlitlpEP8oAPaJgYssByI4eCwQq2C9eC90otFpm8MRn66PpbKviweAlhinWQ3ZjDD2KIVtx7PaVw=="],
+
+ "@peculiar/asn1-pkcs9": ["@peculiar/asn1-pkcs9@2.5.0", "", { "dependencies": { "@peculiar/asn1-cms": "^2.5.0", "@peculiar/asn1-pfx": "^2.5.0", "@peculiar/asn1-pkcs8": "^2.5.0", "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "@peculiar/asn1-x509-attr": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-UgqSMBLNLR5TzEZ5ZzxR45Nk6VJrammxd60WMSkofyNzd3DQLSNycGWSK5Xg3UTYbXcDFyG8pA/7/y/ztVCa6A=="],
+
+ "@peculiar/asn1-rsa": ["@peculiar/asn1-rsa@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-qMZ/vweiTHy9syrkkqWFvbT3eLoedvamcUdnnvwyyUNv5FgFXA3KP8td+ATibnlZ0EANW5PYRm8E6MJzEB/72Q=="],
+
+ "@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.5.0", "", { "dependencies": { "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-YM/nFfskFJSlHqv59ed6dZlLZqtZQwjRVJ4bBAiWV08Oc+1rSd5lDZcBEx0lGDHfSoH3UziI2pXt2UM33KerPQ=="],
+
+ "@peculiar/asn1-x509": ["@peculiar/asn1-x509@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-CpwtMCTJvfvYTFMuiME5IH+8qmDe3yEWzKHe7OOADbGfq7ohxeLaXwQo0q4du3qs0AII3UbLCvb9NF/6q0oTKQ=="],
+
+ "@peculiar/asn1-x509-attr": ["@peculiar/asn1-x509-attr@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-9f0hPOxiJDoG/bfNLAFven+Bd4gwz/VzrCIIWc1025LEI4BXO0U5fOCTNDPbbp2ll+UzqKsZ3g61mpBp74gk9A=="],
+
+ "@peculiar/x509": ["@peculiar/x509@1.14.0", "", { "dependencies": { "@peculiar/asn1-cms": "^2.5.0", "@peculiar/asn1-csr": "^2.5.0", "@peculiar/asn1-ecc": "^2.5.0", "@peculiar/asn1-pkcs9": "^2.5.0", "@peculiar/asn1-rsa": "^2.5.0", "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "pvtsutils": "^1.3.6", "reflect-metadata": "^0.2.2", "tslib": "^2.8.1", "tsyringe": "^4.10.0" } }, "sha512-Yc4PDxN3OrxUPiXgU63c+ZRXKGE8YKF2McTciYhUHFtHVB0KMnjeFSU0qpztGhsp4P0uKix4+J2xEpIEDu8oXg=="],
+
+ "@rollup/plugin-alias": ["@rollup/plugin-alias@5.1.1", "", { "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ=="],
+
+ "@rollup/plugin-commonjs": ["@rollup/plugin-commonjs@28.0.6", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", "fdir": "^6.2.0", "is-reference": "1.2.1", "magic-string": "^0.30.3", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-XSQB1K7FUU5QP+3lOQmVCE3I0FcbbNvmNT4VJSj93iUjayaARrTQeoRdiYQoftAJBLrR9t2agwAd3ekaTgHNlw=="],
+
+ "@rollup/plugin-json": ["@rollup/plugin-json@6.1.0", "", { "dependencies": { "@rollup/pluginutils": "^5.1.0" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA=="],
+
+ "@rollup/plugin-node-resolve": ["@rollup/plugin-node-resolve@16.0.3", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", "deepmerge": "^4.2.2", "is-module": "^1.0.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.78.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg=="],
+
+ "@rollup/plugin-replace": ["@rollup/plugin-replace@6.0.2", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "magic-string": "^0.30.3" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-7QaYCf8bqF04dOy7w/eHmJeNExxTYwvKAmlSAH/EaWWUzbT0h5sbF6bktFoX/0F/0qwng5/dWFMyf3gzaM8DsQ=="],
+
+ "@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="],
+
+ "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.4", "", { "os": "android", "cpu": "arm" }, "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA=="],
+
+ "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.52.4", "", { "os": "android", "cpu": "arm64" }, "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w=="],
+
+ "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.52.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg=="],
+
+ "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.52.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw=="],
+
+ "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.52.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ=="],
+
+ "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.52.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw=="],
+
+ "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.52.4", "", { "os": "linux", "cpu": "arm" }, "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ=="],
+
+ "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.52.4", "", { "os": "linux", "cpu": "arm" }, "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q=="],
+
+ "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.52.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg=="],
+
+ "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.52.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g=="],
+
+ "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.52.4", "", { "os": "linux", "cpu": "none" }, "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ=="],
+
+ "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.52.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g=="],
+
+ "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.52.4", "", { "os": "linux", "cpu": "none" }, "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg=="],
+
+ "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.52.4", "", { "os": "linux", "cpu": "none" }, "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA=="],
+
+ "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.52.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA=="],
+
+ "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.52.4", "", { "os": "linux", "cpu": "x64" }, "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg=="],
+
+ "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.52.4", "", { "os": "linux", "cpu": "x64" }, "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw=="],
+
+ "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.52.4", "", { "os": "none", "cpu": "arm64" }, "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA=="],
+
+ "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.52.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ=="],
+
+ "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.52.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw=="],
+
+ "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.52.4", "", { "os": "win32", "cpu": "x64" }, "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ=="],
+
+ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.4", "", { "os": "win32", "cpu": "x64" }, "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w=="],
+
+ "@simplewebauthn/browser": ["@simplewebauthn/browser@13.2.2", "", {}, "sha512-FNW1oLQpTJyqG5kkDg5ZsotvWgmBaC6jCHR7Ej0qUNep36Wl9tj2eZu7J5rP+uhXgHaLk+QQ3lqcw2vS5MX1IA=="],
+
+ "@simplewebauthn/server": ["@simplewebauthn/server@13.2.2", "", { "dependencies": { "@hexagon/base64": "^1.1.27", "@levischuck/tiny-cbor": "^0.2.2", "@peculiar/asn1-android": "^2.3.10", "@peculiar/asn1-ecc": "^2.3.8", "@peculiar/asn1-rsa": "^2.3.8", "@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-x509": "^2.3.8", "@peculiar/x509": "^1.13.0" } }, "sha512-HcWLW28yTMGXpwE9VLx9J+N2KEUaELadLrkPEEI9tpI5la70xNEVEsu/C+m3u7uoq4FulLqZQhgBCzR9IZhFpA=="],
+
+ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
+
+ "@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="],
+
+ "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
+
+ "asn1js": ["asn1js@3.0.6", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" } }, "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA=="],
+
+ "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-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=="],
+
+ "caniuse-api": ["caniuse-api@3.0.0", "", { "dependencies": { "browserslist": "^4.0.0", "caniuse-lite": "^1.0.0", "lodash.memoize": "^4.1.2", "lodash.uniq": "^4.5.0" } }, "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw=="],
+
+ "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=="],
+
+ "commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
+
+ "commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="],
+
+ "confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
+
+ "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
+
+ "css-declaration-sorter": ["css-declaration-sorter@7.3.0", "", { "peerDependencies": { "postcss": "^8.0.9" } }, "sha512-LQF6N/3vkAMYF4xoHLJfG718HRJh34Z8BnNhd6bosOMIVjMlhuZK5++oZa3uYAgrI5+7x2o27gUqTR2U/KjUOQ=="],
+
+ "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="],
+
+ "css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="],
+
+ "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="],
+
+ "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
+
+ "cssnano": ["cssnano@7.1.1", "", { "dependencies": { "cssnano-preset-default": "^7.0.9", "lilconfig": "^3.1.3" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-fm4D8ti0dQmFPeF8DXSAA//btEmqCOgAc/9Oa3C1LW94h5usNrJEfrON7b4FkPZgnDEn6OUs5NdxiJZmAtGOpQ=="],
+
+ "cssnano-preset-default": ["cssnano-preset-default@7.0.9", "", { "dependencies": { "browserslist": "^4.25.1", "css-declaration-sorter": "^7.2.0", "cssnano-utils": "^5.0.1", "postcss-calc": "^10.1.1", "postcss-colormin": "^7.0.4", "postcss-convert-values": "^7.0.7", "postcss-discard-comments": "^7.0.4", "postcss-discard-duplicates": "^7.0.2", "postcss-discard-empty": "^7.0.1", "postcss-discard-overridden": "^7.0.1", "postcss-merge-longhand": "^7.0.5", "postcss-merge-rules": "^7.0.6", "postcss-minify-font-values": "^7.0.1", "postcss-minify-gradients": "^7.0.1", "postcss-minify-params": "^7.0.4", "postcss-minify-selectors": "^7.0.5", "postcss-normalize-charset": "^7.0.1", "postcss-normalize-display-values": "^7.0.1", "postcss-normalize-positions": "^7.0.1", "postcss-normalize-repeat-style": "^7.0.1", "postcss-normalize-string": "^7.0.1", "postcss-normalize-timing-functions": "^7.0.1", "postcss-normalize-unicode": "^7.0.4", "postcss-normalize-url": "^7.0.1", "postcss-normalize-whitespace": "^7.0.1", "postcss-ordered-values": "^7.0.2", "postcss-reduce-initial": "^7.0.4", "postcss-reduce-transforms": "^7.0.1", "postcss-svgo": "^7.1.0", "postcss-unique-selectors": "^7.0.4" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-tCD6AAFgYBOVpMBX41KjbvRh9c2uUjLXRyV7KHSIrwHiq5Z9o0TFfUCoM3TwVrRsRteN3sVXGNvjVNxYzkpTsA=="],
+
+ "cssnano-utils": ["cssnano-utils@5.0.1", "", { "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-ZIP71eQgG9JwjVZsTPSqhc6GHgEr53uJ7tK5///VfyWj6Xp2DBmixWHqJgPno+PqATzn48pL42ww9x5SSGmhZg=="],
+
+ "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=="],
+
+ "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
+
+ "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
+
+ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
+
+ "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=="],
+
+ "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
+
+ "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
+
+ "esbuild": ["esbuild@0.25.10", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.10", "@esbuild/android-arm": "0.25.10", "@esbuild/android-arm64": "0.25.10", "@esbuild/android-x64": "0.25.10", "@esbuild/darwin-arm64": "0.25.10", "@esbuild/darwin-x64": "0.25.10", "@esbuild/freebsd-arm64": "0.25.10", "@esbuild/freebsd-x64": "0.25.10", "@esbuild/linux-arm": "0.25.10", "@esbuild/linux-arm64": "0.25.10", "@esbuild/linux-ia32": "0.25.10", "@esbuild/linux-loong64": "0.25.10", "@esbuild/linux-mips64el": "0.25.10", "@esbuild/linux-ppc64": "0.25.10", "@esbuild/linux-riscv64": "0.25.10", "@esbuild/linux-s390x": "0.25.10", "@esbuild/linux-x64": "0.25.10", "@esbuild/netbsd-arm64": "0.25.10", "@esbuild/netbsd-x64": "0.25.10", "@esbuild/openbsd-arm64": "0.25.10", "@esbuild/openbsd-x64": "0.25.10", "@esbuild/openharmony-arm64": "0.25.10", "@esbuild/sunos-x64": "0.25.10", "@esbuild/win32-arm64": "0.25.10", "@esbuild/win32-ia32": "0.25.10", "@esbuild/win32-x64": "0.25.10" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ=="],
+
+ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
+
+ "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=="],
+
+ "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
+
+ "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=="],
+
+ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
+
+ "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=="],
+
+ "is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="],
+
+ "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
+
+ "jose": ["jose@6.1.0", "", {}, "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA=="],
+
+ "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
+
+ "knitwork": ["knitwork@1.2.0", "", {}, "sha512-xYSH7AvuQ6nXkq42x0v5S8/Iry+cfulBz/DJQzhIyESdLD7425jXsPy4vn5cCXU+HhRN2kVw51Vd1K6/By4BQg=="],
+
+ "kysely": ["kysely@0.28.8", "", {}, "sha512-QUOgl5ZrS9IRuhq5FvOKFSsD/3+IA6MLE81/bOOTRA/YQpKDza2sFdN5g6JCB9BOpqMJDGefLCQ9F12hRS13TA=="],
+
+ "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
+
+ "lodash.memoize": ["lodash.memoize@4.1.2", "", {}, "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag=="],
+
+ "lodash.uniq": ["lodash.uniq@4.5.0", "", {}, "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ=="],
+
+ "magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="],
+
+ "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
+
+ "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=="],
+
+ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
+
+ "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=="],
+
+ "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
+
+ "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=="],
+
+ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
+
+ "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
+
+ "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="],
+
+ "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
+
+ "postcss-calc": ["postcss-calc@10.1.1", "", { "dependencies": { "postcss-selector-parser": "^7.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.38" } }, "sha512-NYEsLHh8DgG/PRH2+G9BTuUdtf9ViS+vdoQ0YA5OQdGsfN4ztiwtDWNtBl9EKeqNMFnIu8IKZ0cLxEQ5r5KVMw=="],
+
+ "postcss-colormin": ["postcss-colormin@7.0.4", "", { "dependencies": { "browserslist": "^4.25.1", "caniuse-api": "^3.0.0", "colord": "^2.9.3", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-ziQuVzQZBROpKpfeDwmrG+Vvlr0YWmY/ZAk99XD+mGEBuEojoFekL41NCsdhyNUtZI7DPOoIWIR7vQQK9xwluw=="],
+
+ "postcss-convert-values": ["postcss-convert-values@7.0.7", "", { "dependencies": { "browserslist": "^4.25.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-HR9DZLN04Xbe6xugRH6lS4ZQH2zm/bFh/ZyRkpedZozhvh+awAfbA0P36InO4fZfDhvYfNJeNvlTf1sjwGbw/A=="],
+
+ "postcss-discard-comments": ["postcss-discard-comments@7.0.4", "", { "dependencies": { "postcss-selector-parser": "^7.1.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-6tCUoql/ipWwKtVP/xYiFf1U9QgJ0PUvxN7pTcsQ8Ns3Fnwq1pU5D5s1MhT/XySeLq6GXNvn37U46Ded0TckWg=="],
+
+ "postcss-discard-duplicates": ["postcss-discard-duplicates@7.0.2", "", { "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-eTonaQvPZ/3i1ASDHOKkYwAybiM45zFIc7KXils4mQmHLqIswXD9XNOKEVxtTFnsmwYzF66u4LMgSr0abDlh5w=="],
+
+ "postcss-discard-empty": ["postcss-discard-empty@7.0.1", "", { "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-cFrJKZvcg/uxB6Ijr4l6qmn3pXQBna9zyrPC+sK0zjbkDUZew+6xDltSF7OeB7rAtzaaMVYSdbod+sZOCWnMOg=="],
+
+ "postcss-discard-overridden": ["postcss-discard-overridden@7.0.1", "", { "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-7c3MMjjSZ/qYrx3uc1940GSOzN1Iqjtlqe8uoSg+qdVPYyRb0TILSqqmtlSFuE4mTDECwsm397Ya7iXGzfF7lg=="],
+
+ "postcss-merge-longhand": ["postcss-merge-longhand@7.0.5", "", { "dependencies": { "postcss-value-parser": "^4.2.0", "stylehacks": "^7.0.5" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-Kpu5v4Ys6QI59FxmxtNB/iHUVDn9Y9sYw66D6+SZoIk4QTz1prC4aYkhIESu+ieG1iylod1f8MILMs1Em3mmIw=="],
+
+ "postcss-merge-rules": ["postcss-merge-rules@7.0.6", "", { "dependencies": { "browserslist": "^4.25.1", "caniuse-api": "^3.0.0", "cssnano-utils": "^5.0.1", "postcss-selector-parser": "^7.1.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-2jIPT4Tzs8K87tvgCpSukRQ2jjd+hH6Bb8rEEOUDmmhOeTcqDg5fEFK8uKIu+Pvc3//sm3Uu6FRqfyv7YF7+BQ=="],
+
+ "postcss-minify-font-values": ["postcss-minify-font-values@7.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-2m1uiuJeTplll+tq4ENOQSzB8LRnSUChBv7oSyFLsJRtUgAAJGP6LLz0/8lkinTgxrmJSPOEhgY1bMXOQ4ZXhQ=="],
+
+ "postcss-minify-gradients": ["postcss-minify-gradients@7.0.1", "", { "dependencies": { "colord": "^2.9.3", "cssnano-utils": "^5.0.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-X9JjaysZJwlqNkJbUDgOclyG3jZEpAMOfof6PUZjPnPrePnPG62pS17CjdM32uT1Uq1jFvNSff9l7kNbmMSL2A=="],
+
+ "postcss-minify-params": ["postcss-minify-params@7.0.4", "", { "dependencies": { "browserslist": "^4.25.1", "cssnano-utils": "^5.0.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-3OqqUddfH8c2e7M35W6zIwv7jssM/3miF9cbCSb1iJiWvtguQjlxZGIHK9JRmc8XAKmE2PFGtHSM7g/VcW97sw=="],
+
+ "postcss-minify-selectors": ["postcss-minify-selectors@7.0.5", "", { "dependencies": { "cssesc": "^3.0.0", "postcss-selector-parser": "^7.1.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-x2/IvofHcdIrAm9Q+p06ZD1h6FPcQ32WtCRVodJLDR+WMn8EVHI1kvLxZuGKz/9EY5nAmI6lIQIrpo4tBy5+ug=="],
+
+ "postcss-nested": ["postcss-nested@7.0.2", "", { "dependencies": { "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-5osppouFc0VR9/VYzYxO03VaDa3e8F23Kfd6/9qcZTUI8P58GIYlArOET2Wq0ywSl2o2PjELhYOFI4W7l5QHKw=="],
+
+ "postcss-normalize-charset": ["postcss-normalize-charset@7.0.1", "", { "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-sn413ofhSQHlZFae//m9FTOfkmiZ+YQXsbosqOWRiVQncU2BA3daX3n0VF3cG6rGLSFVc5Di/yns0dFfh8NFgQ=="],
+
+ "postcss-normalize-display-values": ["postcss-normalize-display-values@7.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-E5nnB26XjSYz/mGITm6JgiDpAbVuAkzXwLzRZtts19jHDUBFxZ0BkXAehy0uimrOjYJbocby4FVswA/5noOxrQ=="],
+
+ "postcss-normalize-positions": ["postcss-normalize-positions@7.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-pB/SzrIP2l50ZIYu+yQZyMNmnAcwyYb9R1fVWPRxm4zcUFCY2ign7rcntGFuMXDdd9L2pPNUgoODDk91PzRZuQ=="],
+
+ "postcss-normalize-repeat-style": ["postcss-normalize-repeat-style@7.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-NsSQJ8zj8TIDiF0ig44Byo3Jk9e4gNt9x2VIlJudnQQ5DhWAHJPF4Tr1ITwyHio2BUi/I6Iv0HRO7beHYOloYQ=="],
+
+ "postcss-normalize-string": ["postcss-normalize-string@7.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-QByrI7hAhsoze992kpbMlJSbZ8FuCEc1OT9EFbZ6HldXNpsdpZr+YXC5di3UEv0+jeZlHbZcoCADgb7a+lPmmQ=="],
+
+ "postcss-normalize-timing-functions": ["postcss-normalize-timing-functions@7.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-bHifyuuSNdKKsnNJ0s8fmfLMlvsQwYVxIoUBnowIVl2ZAdrkYQNGVB4RxjfpvkMjipqvbz0u7feBZybkl/6NJg=="],
+
+ "postcss-normalize-unicode": ["postcss-normalize-unicode@7.0.4", "", { "dependencies": { "browserslist": "^4.25.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-LvIURTi1sQoZqj8mEIE8R15yvM+OhbR1avynMtI9bUzj5gGKR/gfZFd8O7VMj0QgJaIFzxDwxGl/ASMYAkqO8g=="],
+
+ "postcss-normalize-url": ["postcss-normalize-url@7.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-sUcD2cWtyK1AOL/82Fwy1aIVm/wwj5SdZkgZ3QiUzSzQQofrbq15jWJ3BA7Z+yVRwamCjJgZJN0I9IS7c6tgeQ=="],
+
+ "postcss-normalize-whitespace": ["postcss-normalize-whitespace@7.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-vsbgFHMFQrJBJKrUFJNZ2pgBeBkC2IvvoHjz1to0/0Xk7sII24T0qFOiJzG6Fu3zJoq/0yI4rKWi7WhApW+EFA=="],
+
+ "postcss-ordered-values": ["postcss-ordered-values@7.0.2", "", { "dependencies": { "cssnano-utils": "^5.0.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-AMJjt1ECBffF7CEON/Y0rekRLS6KsePU6PRP08UqYW4UGFRnTXNrByUzYK1h8AC7UWTZdQ9O3Oq9kFIhm0SFEw=="],
+
+ "postcss-reduce-initial": ["postcss-reduce-initial@7.0.4", "", { "dependencies": { "browserslist": "^4.25.1", "caniuse-api": "^3.0.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-rdIC9IlMBn7zJo6puim58Xd++0HdbvHeHaPgXsimMfG1ijC5A9ULvNLSE0rUKVJOvNMcwewW4Ga21ngyJjY/+Q=="],
+
+ "postcss-reduce-transforms": ["postcss-reduce-transforms@7.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-MhyEbfrm+Mlp/36hvZ9mT9DaO7dbncU0CvWI8V93LRkY6IYlu38OPg3FObnuKTUxJ4qA8HpurdQOo5CyqqO76g=="],
+
+ "postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
+
+ "postcss-svgo": ["postcss-svgo@7.1.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0", "svgo": "^4.0.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-KnAlfmhtoLz6IuU3Sij2ycusNs4jPW+QoFE5kuuUOK8awR6tMxZQrs5Ey3BUz7nFCzT3eqyFgqkyrHiaU2xx3w=="],
+
+ "postcss-unique-selectors": ["postcss-unique-selectors@7.0.4", "", { "dependencies": { "postcss-selector-parser": "^7.1.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-pmlZjsmEAG7cHd7uK3ZiNSW6otSZ13RHuZ/4cDN/bVglS5EpF2r2oxY99SuOHa8m7AWoBCelTS3JPpzsIs8skQ=="],
+
+ "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=="],
+
+ "rollup": ["rollup@4.52.4", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.4", "@rollup/rollup-android-arm64": "4.52.4", "@rollup/rollup-darwin-arm64": "4.52.4", "@rollup/rollup-darwin-x64": "4.52.4", "@rollup/rollup-freebsd-arm64": "4.52.4", "@rollup/rollup-freebsd-x64": "4.52.4", "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", "@rollup/rollup-linux-arm-musleabihf": "4.52.4", "@rollup/rollup-linux-arm64-gnu": "4.52.4", "@rollup/rollup-linux-arm64-musl": "4.52.4", "@rollup/rollup-linux-loong64-gnu": "4.52.4", "@rollup/rollup-linux-ppc64-gnu": "4.52.4", "@rollup/rollup-linux-riscv64-gnu": "4.52.4", "@rollup/rollup-linux-riscv64-musl": "4.52.4", "@rollup/rollup-linux-s390x-gnu": "4.52.4", "@rollup/rollup-linux-x64-gnu": "4.52.4", "@rollup/rollup-linux-x64-musl": "4.52.4", "@rollup/rollup-openharmony-arm64": "4.52.4", "@rollup/rollup-win32-arm64-msvc": "4.52.4", "@rollup/rollup-win32-ia32-msvc": "4.52.4", "@rollup/rollup-win32-x64-gnu": "4.52.4", "@rollup/rollup-win32-x64-msvc": "4.52.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ=="],
+
+ "rollup-plugin-dts": ["rollup-plugin-dts@6.2.3", "", { "dependencies": { "magic-string": "^0.30.17" }, "optionalDependencies": { "@babel/code-frame": "^7.27.1" }, "peerDependencies": { "rollup": "^3.29.4 || ^4", "typescript": "^4.5 || ^5.0" } }, "sha512-UgnEsfciXSPpASuOelix7m4DrmyQgiaWBnvI0TM4GxuDh5FkqW8E5hu57bCxXB90VvR1WNfLV80yEDN18UogSA=="],
+
+ "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=="],
+
+ "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
+
+ "set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="],
+
+ "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
+
+ "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
+
+ "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
+
+ "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=="],
+
+ "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=="],
+
+ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
+
+ "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=="],
+
+ "unbuild": ["unbuild@3.6.1", "", { "dependencies": { "@rollup/plugin-alias": "^5.1.1", "@rollup/plugin-commonjs": "^28.0.6", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.1", "@rollup/plugin-replace": "^6.0.2", "@rollup/pluginutils": "^5.2.0", "citty": "^0.1.6", "consola": "^3.4.2", "defu": "^6.1.4", "esbuild": "^0.25.9", "fix-dts-default-cjs-exports": "^1.0.1", "hookable": "^5.5.3", "jiti": "^2.5.1", "magic-string": "^0.30.17", "mkdist": "^2.3.0", "mlly": "^1.7.4", "pathe": "^2.0.3", "pkg-types": "^2.2.0", "pretty-bytes": "^7.0.1", "rollup": "^4.46.2", "rollup-plugin-dts": "^6.2.1", "scule": "^1.3.0", "tinyglobby": "^0.2.14", "untyped": "^2.0.0" }, "peerDependencies": { "typescript": "^5.9.2" }, "optionalPeers": ["typescript"], "bin": { "unbuild": "dist/cli.mjs" } }, "sha512-+U5CdtrdjfWkZhuO4N9l5UhyiccoeMEXIc2Lbs30Haxb+tRwB3VwB8AoZRxlAzORXunenSo+j6lh45jx+xkKgg=="],
+
+ "uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
+
+ "untyped": ["untyped@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "defu": "^6.1.4", "jiti": "^2.4.2", "knitwork": "^1.2.0", "scule": "^1.3.0" }, "bin": { "untyped": "dist/cli.mjs" } }, "sha512-nwNCjxJTjNuLCgFr42fEak5OcLuB3ecca+9ksPFNvtfYSLpjf+iJqSIaSnIile6ZPbKYxI5k2AfXqeopGudK/g=="],
+
+ "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
+
+ "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=="],
+
+ "mkdist/jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="],
+
+ "mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
+
+ "tsyringe/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="],
+
+ "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
+
+ "mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..1c31da0
--- /dev/null
+++ b/package.json
@@ -0,0 +1,61 @@
+{
+ "name": "@better-auth-bowong/stripe",
+ "author": "Bereket Engida",
+ "version": "1.4.1",
+ "main": "dist/index.cjs",
+ "license": "MIT",
+ "keywords": [
+ "stripe",
+ "auth",
+ "stripe"
+ ],
+ "module": "dist/index.mjs",
+ "description": "Stripe plugin for Better Auth",
+ "scripts": {
+ "test": "vitest",
+ "build": "unbuild",
+ "dev": "unbuild --watch",
+ "typecheck": "tsc --project tsconfig.json"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "import": "./dist/index.mjs",
+ "require": "./dist/index.cjs"
+ },
+ "./client": {
+ "types": "./dist/client.d.ts",
+ "import": "./dist/client.mjs",
+ "require": "./dist/client.cjs"
+ }
+ },
+ "typesVersions": {
+ "*": {
+ "*": [
+ "./dist/index.d.ts"
+ ],
+ "client": [
+ "./dist/client.d.ts"
+ ]
+ }
+ },
+ "dependencies": {
+ "defu": "^6.1.4",
+ "zod": "^4.1.5"
+ },
+ "peerDependencies": {
+ "@better-auth/core": "^1.4.0-beta.9",
+ "better-auth": "^1.4.0-beta.9",
+ "stripe": "^19"
+ },
+ "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"
+ }
+}
diff --git a/src/client.ts b/src/client.ts
new file mode 100644
index 0000000..410ee0c
--- /dev/null
+++ b/src/client.ts
@@ -0,0 +1,36 @@
+import type { BetterAuthClientPlugin } from "better-auth";
+import type { stripe } from "./index";
+
+export const stripeClient = <
+ O extends {
+ subscription: boolean;
+ },
+>(
+ 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",
+ "/subscription/meter-event": "POST",
+ },
+ } satisfies BetterAuthClientPlugin;
+};
diff --git a/src/hooks.ts b/src/hooks.ts
new file mode 100644
index 0000000..23239e9
--- /dev/null
+++ b/src/hooks.ts
@@ -0,0 +1,258 @@
+import { type GenericEndpointContext, logger } from "better-auth";
+import type Stripe from "stripe";
+import type { InputSubscription, StripeOptions, Subscription } from "./types";
+import { getPlanByPriceInfo } from "./utils";
+
+export async function onCheckoutSessionCompleted(
+ ctx: GenericEndpointContext,
+ options: StripeOptions,
+ event: Stripe.Event,
+) {
+ try {
+ const client = options.stripeClient;
+ const checkoutSession = event.data.object as Stripe.Checkout.Session;
+ if (checkoutSession.mode === "setup" || !options.subscription?.enabled) {
+ return;
+ }
+ const subscription = await client.subscriptions.retrieve(
+ checkoutSession.subscription as string,
+ );
+ const priceId = subscription.items.data[0]?.price.id;
+ const priceLookupKey = subscription.items.data[0]?.price.lookup_key || null;
+ const plan = await getPlanByPriceInfo(
+ options,
+ priceId as string,
+ priceLookupKey,
+ );
+ if (plan) {
+ const referenceId =
+ checkoutSession?.client_reference_id ||
+ checkoutSession?.metadata?.referenceId;
+ const subscriptionId = checkoutSession?.metadata?.subscriptionId;
+ const seats = subscription.items.data[0]!.quantity;
+ if (referenceId && subscriptionId) {
+ const trial =
+ subscription.trial_start && subscription.trial_end
+ ? {
+ trialStart: new Date(subscription.trial_start * 1000),
+ trialEnd: new Date(subscription.trial_end * 1000),
+ }
+ : {};
+
+ let dbSubscription =
+ await ctx.context.adapter.update({
+ model: "subscription",
+ update: {
+ plan: plan.name.toLowerCase(),
+ status: subscription.status,
+ updatedAt: new Date(),
+ periodStart: new Date(
+ subscription.items.data[0]!.current_period_start * 1000,
+ ),
+ periodEnd: new Date(
+ subscription.items.data[0]!.current_period_end * 1000,
+ ),
+ stripeSubscriptionId: checkoutSession.subscription as string,
+ seats,
+ ...trial,
+ },
+ where: [
+ {
+ field: "id",
+ value: subscriptionId,
+ },
+ ],
+ });
+
+ if (trial.trialStart && plan.freeTrial?.onTrialStart) {
+ await plan.freeTrial.onTrialStart(dbSubscription as Subscription);
+ }
+
+ if (!dbSubscription) {
+ dbSubscription = await ctx.context.adapter.findOne({
+ model: "subscription",
+ where: [
+ {
+ field: "id",
+ value: subscriptionId,
+ },
+ ],
+ });
+ }
+ await options.subscription?.onSubscriptionComplete?.(
+ {
+ event,
+ subscription: dbSubscription as Subscription,
+ stripeSubscription: subscription,
+ plan,
+ },
+ ctx,
+ );
+ return;
+ }
+ }
+ } catch (e: any) {
+ logger.error(`Stripe webhook failed. Error: ${e.message}`);
+ }
+}
+
+export async function onSubscriptionUpdated(
+ ctx: GenericEndpointContext,
+ options: StripeOptions,
+ event: Stripe.Event,
+) {
+ try {
+ if (!options.subscription?.enabled) {
+ return;
+ }
+ const subscriptionUpdated = event.data.object as Stripe.Subscription;
+ const priceId = subscriptionUpdated.items.data[0]!.price.id;
+ const priceLookupKey =
+ subscriptionUpdated.items.data[0]!.price.lookup_key || null;
+ const plan = await getPlanByPriceInfo(options, priceId, priceLookupKey);
+
+ const subscriptionId = subscriptionUpdated.metadata?.subscriptionId;
+ const customerId = subscriptionUpdated.customer?.toString();
+ let subscription = await ctx.context.adapter.findOne({
+ model: "subscription",
+ where: subscriptionId
+ ? [{ field: "id", value: subscriptionId }]
+ : [{ field: "stripeSubscriptionId", value: subscriptionUpdated.id }],
+ });
+ if (!subscription) {
+ const subs = await ctx.context.adapter.findMany({
+ model: "subscription",
+ where: [{ field: "stripeCustomerId", value: customerId }],
+ });
+ if (subs.length > 1) {
+ const activeSub = subs.find(
+ (sub: Subscription) =>
+ sub.status === "active" || sub.status === "trialing",
+ );
+ if (!activeSub) {
+ logger.warn(
+ `Stripe webhook error: Multiple subscriptions found for customerId: ${customerId} and no active subscription is found`,
+ );
+ return;
+ }
+ subscription = activeSub;
+ } else {
+ subscription = subs[0]!;
+ }
+ }
+
+ const seats = subscriptionUpdated.items.data[0]!.quantity;
+ await ctx.context.adapter.update({
+ model: "subscription",
+ update: {
+ ...(plan
+ ? {
+ plan: plan.name.toLowerCase(),
+ limits: plan.limits,
+ }
+ : {}),
+ updatedAt: new Date(),
+ status: subscriptionUpdated.status,
+ periodStart: new Date(
+ subscriptionUpdated.items.data[0]!.current_period_start * 1000,
+ ),
+ periodEnd: new Date(
+ subscriptionUpdated.items.data[0]!.current_period_end * 1000,
+ ),
+ cancelAtPeriodEnd: subscriptionUpdated.cancel_at_period_end,
+ seats,
+ stripeSubscriptionId: subscriptionUpdated.id,
+ },
+ where: [
+ {
+ field: "id",
+ value: subscription.id,
+ },
+ ],
+ });
+ const subscriptionCanceled =
+ subscriptionUpdated.status === "active" &&
+ subscriptionUpdated.cancel_at_period_end &&
+ !subscription.cancelAtPeriodEnd; //if this is true, it means the subscription was canceled before the event was triggered
+ if (subscriptionCanceled) {
+ await options.subscription.onSubscriptionCancel?.({
+ subscription,
+ cancellationDetails:
+ subscriptionUpdated.cancellation_details || undefined,
+ stripeSubscription: subscriptionUpdated,
+ event,
+ });
+ }
+ await options.subscription.onSubscriptionUpdate?.({
+ event,
+ subscription,
+ });
+ if (plan) {
+ if (
+ subscriptionUpdated.status === "active" &&
+ subscription.status === "trialing" &&
+ plan.freeTrial?.onTrialEnd
+ ) {
+ await plan.freeTrial.onTrialEnd({ subscription }, ctx);
+ }
+ if (
+ subscriptionUpdated.status === "incomplete_expired" &&
+ subscription.status === "trialing" &&
+ plan.freeTrial?.onTrialExpired
+ ) {
+ await plan.freeTrial.onTrialExpired(subscription, ctx);
+ }
+ }
+ } catch (error: any) {
+ logger.error(`Stripe webhook failed. Error: ${error}`);
+ }
+}
+
+export async function onSubscriptionDeleted(
+ ctx: GenericEndpointContext,
+ options: StripeOptions,
+ event: Stripe.Event,
+) {
+ if (!options.subscription?.enabled) {
+ return;
+ }
+ try {
+ const subscriptionDeleted = event.data.object as Stripe.Subscription;
+ const subscriptionId = subscriptionDeleted.id;
+ const subscription = await ctx.context.adapter.findOne({
+ model: "subscription",
+ where: [
+ {
+ field: "stripeSubscriptionId",
+ value: subscriptionId,
+ },
+ ],
+ });
+ if (subscription) {
+ await ctx.context.adapter.update({
+ model: "subscription",
+ where: [
+ {
+ field: "id",
+ value: subscription.id,
+ },
+ ],
+ update: {
+ status: "canceled",
+ updatedAt: new Date(),
+ },
+ });
+ await options.subscription.onSubscriptionDeleted?.({
+ event,
+ stripeSubscription: subscriptionDeleted,
+ subscription,
+ });
+ } else {
+ logger.warn(
+ `Stripe webhook error: Subscription not found for subscriptionId: ${subscriptionId}`,
+ );
+ }
+ } catch (error: any) {
+ logger.error(`Stripe webhook failed. Error: ${error}`);
+ }
+}
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..1c480f4
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,1479 @@
+import {
+ type GenericEndpointContext,
+ type BetterAuthPlugin,
+ logger,
+} from "better-auth";
+import {createAuthEndpoint, createAuthMiddleware} from "better-auth/plugins";
+import Stripe from "stripe";
+import {type Stripe as StripeType} from "stripe";
+import * as z from "zod/v4";
+import {
+ sessionMiddleware,
+ APIError,
+ originCheck,
+ getSessionFromCtx,
+} from "better-auth/api";
+import {
+ onCheckoutSessionCompleted,
+ onSubscriptionDeleted,
+ onSubscriptionUpdated,
+} from "./hooks";
+import type {
+ 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";
+
+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 getUrl = (ctx: GenericEndpointContext, url: string) => {
+ if (url.startsWith("http")) {
+ return url;
+ }
+ return `${ctx.context.options.baseURL}${
+ url.startsWith("/") ? url : `/${url}`
+ }`;
+};
+
+async function resolvePriceIdFromLookupKey(
+ 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;
+}
+
+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;
+
+ 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;
+
+ if (ctx.body.subscriptionId && !subscriptionToUpdate) {
+ throw new APIError("BAD_REQUEST", {
+ message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
+ });
+ }
+
+ 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,
+ });
+
+ 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,
+ },
+ });
+ }
+
+ // 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,
+ });
+ }
+ }
+
+ 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 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;
+ });
+
+ // 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 (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;
+ }
+
+ // 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",
+ });
+ }
+
+ 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;
+
+ 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) {
+ 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 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;
+
+ 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));
+ }
+
+ 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;
+
+ 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,
+ });
+ }
+
+ 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,
+ },
+ ],
+ });
+
+ 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,
+ },
+ ],
+ });
+
+ 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 (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;
+
+ 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",
+ ),
+ );
+
+ customerId = subscription?.stripeCustomerId;
+ }
+
+ 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),
+ });
+
+ 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
+ {
+ method: "POST",
+ body: z.object({
+ meter_id: z.string(),
+ event_name: z.string(),
+ payload: z.object({
+ value: z.string(),
+ }),
+ identifier: z.string().optional(),
+ referenceId: z.string().optional(),
+ })
+ },
+ async (ctx) => {
+ const {user} = ctx.context.session;
+ let customerId = user.stripeCustomerId;
+ const referenceId = ctx.body.referenceId || user.id;
+
+ 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;
+ }
+
+ if (!customerId) {
+ throw new APIError("BAD_REQUEST", {
+ message: "No Stripe customer found for this user",
+ });
+ }
+
+ const submitPayload = {
+ value: ctx.body.payload.value,
+ stripe_customer_id: customerId,
+ }
+
+ try {
+ const {identifier, payload, timestamp} = await client.billing.meterEvents.create({
+ event_name: ctx.body.event_name,
+ payload: submitPayload,
+ identifier: ctx.body.identifier
+ });
+
+ return ctx.json({
+ identifier,
+ timestamp,
+ payload,
+ });
+ } catch (error: any) {
+ ctx.context.logger.error(
+ "Error submitting meter event",
+ error,
+ );
+ throw new APIError("BAD_REQUEST", {
+ message: error.message,
+ });
+ }
+ }
+ ),
+ } 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;
+
+ 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;
+
+ // 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;
+ }
+
+ // 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};
diff --git a/src/schema.ts b/src/schema.ts
new file mode 100644
index 0000000..c38485d
--- /dev/null
+++ b/src/schema.ts
@@ -0,0 +1,92 @@
+import type { BetterAuthPluginDBSchema } from "@better-auth/core/db";
+import type { StripeOptions } from "./types";
+import { mergeSchema } from "better-auth/db";
+
+export const subscriptions = {
+ subscription: {
+ fields: {
+ plan: {
+ type: "string",
+ required: true,
+ },
+ referenceId: {
+ type: "string",
+ required: true,
+ },
+ stripeCustomerId: {
+ type: "string",
+ required: false,
+ },
+ stripeSubscriptionId: {
+ type: "string",
+ required: false,
+ },
+ status: {
+ type: "string",
+ defaultValue: "incomplete",
+ },
+ periodStart: {
+ type: "date",
+ required: false,
+ },
+ periodEnd: {
+ type: "date",
+ required: false,
+ },
+ trialStart: {
+ type: "date",
+ required: false,
+ },
+ trialEnd: {
+ type: "date",
+ required: false,
+ },
+ cancelAtPeriodEnd: {
+ type: "boolean",
+ required: false,
+ defaultValue: false,
+ },
+ seats: {
+ type: "number",
+ required: false,
+ },
+ },
+ },
+} satisfies BetterAuthPluginDBSchema;
+
+export const user = {
+ user: {
+ fields: {
+ stripeCustomerId: {
+ type: "string",
+ required: false,
+ },
+ },
+ },
+} satisfies BetterAuthPluginDBSchema;
+
+export const getSchema = (options: StripeOptions) => {
+ let baseSchema = {};
+
+ if (options.subscription?.enabled) {
+ baseSchema = {
+ ...subscriptions,
+ ...user,
+ };
+ } else {
+ baseSchema = {
+ ...user,
+ };
+ }
+
+ if (
+ options.schema &&
+ !options.subscription?.enabled &&
+ "subscription" in options.schema
+ ) {
+ const { subscription, ...restSchema } = options.schema;
+ return mergeSchema(baseSchema, restSchema);
+ }
+
+ return mergeSchema(baseSchema, options.schema);
+};
diff --git a/src/stripe.test.ts b/src/stripe.test.ts
new file mode 100644
index 0000000..36f6344
--- /dev/null
+++ b/src/stripe.test.ts
@@ -0,0 +1,1742 @@
+import { betterAuth, type User } from "better-auth";
+import { memoryAdapter } from "better-auth/adapters/memory";
+import { createAuthClient } from "better-auth/client";
+import { setCookieToHeader } from "better-auth/cookies";
+import { bearer } from "better-auth/plugins";
+import Stripe from "stripe";
+import { vi } from "vitest";
+import { stripe } from ".";
+import { stripeClient } from "./client";
+import type { StripeOptions, Subscription } from "./types";
+import { expect, describe, it, beforeEach } from "vitest";
+
+describe("stripe", async () => {
+ const mockStripe = {
+ prices: {
+ list: vi.fn().mockResolvedValue({ data: [{ id: "price_lookup_123" }] }),
+ },
+ customers: {
+ create: vi.fn().mockResolvedValue({ id: "cus_mock123" }),
+ list: vi.fn().mockResolvedValue({ data: [] }),
+ retrieve: vi.fn().mockResolvedValue({
+ id: "cus_mock123",
+ email: "test@email.com",
+ deleted: false,
+ }),
+ update: vi.fn().mockResolvedValue({
+ id: "cus_mock123",
+ email: "newemail@example.com",
+ }),
+ },
+ checkout: {
+ sessions: {
+ create: vi.fn().mockResolvedValue({
+ url: "https://checkout.stripe.com/mock",
+ id: "",
+ }),
+ },
+ },
+ billingPortal: {
+ sessions: {
+ create: vi
+ .fn()
+ .mockResolvedValue({ url: "https://billing.stripe.com/mock" }),
+ },
+ },
+ subscriptions: {
+ retrieve: vi.fn(),
+ list: vi.fn().mockResolvedValue({ data: [] }),
+ update: vi.fn(),
+ },
+ webhooks: {
+ constructEvent: vi.fn(),
+ },
+ };
+
+ const _stripe = mockStripe as unknown as Stripe;
+ const data = {
+ user: [],
+ session: [],
+ verification: [],
+ account: [],
+ customer: [],
+ subscription: [],
+ };
+ const memory = memoryAdapter(data);
+ const stripeOptions = {
+ stripeClient: _stripe,
+ stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
+ createCustomerOnSignUp: true,
+ subscription: {
+ enabled: true,
+ plans: [
+ {
+ priceId: process.env.STRIPE_PRICE_ID_1!,
+ name: "starter",
+ lookupKey: "lookup_key_123",
+ },
+ {
+ priceId: process.env.STRIPE_PRICE_ID_2!,
+ name: "premium",
+ lookupKey: "lookup_key_234",
+ },
+ ],
+ },
+ } satisfies StripeOptions;
+ const auth = betterAuth({
+ database: memory,
+ baseURL: "http://localhost:3000",
+ // database: new Database(":memory:"),
+ emailAndPassword: {
+ enabled: true,
+ },
+ plugins: [stripe(stripeOptions)],
+ });
+ const ctx = await auth.$context;
+ const authClient = createAuthClient({
+ baseURL: "http://localhost:3000",
+ plugins: [
+ bearer(),
+ stripeClient({
+ subscription: true,
+ }),
+ ],
+ fetchOptions: {
+ customFetchImpl: async (url, init) => {
+ return auth.handler(new Request(url, init));
+ },
+ },
+ });
+
+ const testUser = {
+ email: "test@email.com",
+ password: "password",
+ name: "Test User",
+ };
+
+ beforeEach(() => {
+ data.user = [];
+ data.session = [];
+ data.verification = [];
+ data.account = [];
+ data.customer = [];
+ data.subscription = [];
+
+ vi.clearAllMocks();
+ });
+
+ async function getHeader() {
+ const headers = new Headers();
+ const userRes = await authClient.signIn.email(testUser, {
+ throw: true,
+ onSuccess: setCookieToHeader(headers),
+ });
+ return {
+ headers,
+ response: userRes,
+ };
+ }
+
+ it("should create a customer on sign up", async () => {
+ const userRes = await authClient.signUp.email(testUser, {
+ throw: true,
+ });
+ const res = await ctx.adapter.findOne({
+ model: "user",
+ where: [
+ {
+ field: "id",
+ value: userRes.user.id,
+ },
+ ],
+ });
+ expect(res).toMatchObject({
+ id: expect.any(String),
+ stripeCustomerId: expect.any(String),
+ });
+ });
+
+ it("should create a subscription", async () => {
+ const userRes = await authClient.signUp.email(testUser, {
+ throw: true,
+ });
+
+ const headers = new Headers();
+ await authClient.signIn.email(testUser, {
+ throw: true,
+ onSuccess: setCookieToHeader(headers),
+ });
+
+ const res = await authClient.subscription.upgrade({
+ plan: "starter",
+ fetchOptions: {
+ headers,
+ },
+ });
+ expect(res.data?.url).toBeDefined();
+ const subscription = await ctx.adapter.findOne({
+ model: "subscription",
+ where: [
+ {
+ field: "referenceId",
+ value: userRes.user.id,
+ },
+ ],
+ });
+ expect(subscription).toMatchObject({
+ id: expect.any(String),
+ plan: "starter",
+ referenceId: userRes.user.id,
+ stripeCustomerId: expect.any(String),
+ status: "incomplete",
+ periodStart: undefined,
+ cancelAtPeriodEnd: false,
+ trialStart: undefined,
+ trialEnd: undefined,
+ });
+ });
+
+ it("should list active subscriptions", async () => {
+ const userRes = await authClient.signUp.email(
+ {
+ ...testUser,
+ email: "list-test@email.com",
+ },
+ {
+ throw: true,
+ },
+ );
+ const userId = userRes.user.id;
+
+ const headers = new Headers();
+ await authClient.signIn.email(
+ {
+ ...testUser,
+ email: "list-test@email.com",
+ },
+ {
+ throw: true,
+ onSuccess: setCookieToHeader(headers),
+ },
+ );
+
+ const listRes = await authClient.subscription.list({
+ fetchOptions: {
+ headers,
+ },
+ });
+
+ expect(Array.isArray(listRes.data)).toBe(true);
+
+ await authClient.subscription.upgrade({
+ plan: "starter",
+ fetchOptions: {
+ headers,
+ },
+ });
+ const listBeforeActive = await authClient.subscription.list({
+ fetchOptions: {
+ headers,
+ },
+ });
+ expect(listBeforeActive.data?.length).toBe(0);
+ // Update the subscription status to active
+ await ctx.adapter.update({
+ model: "subscription",
+ update: {
+ status: "active",
+ },
+ where: [
+ {
+ field: "referenceId",
+ value: userId,
+ },
+ ],
+ });
+ const listAfterRes = await authClient.subscription.list({
+ fetchOptions: {
+ headers,
+ },
+ });
+ expect(listAfterRes.data?.length).toBeGreaterThan(0);
+ });
+
+ it("should handle subscription webhook events", async () => {
+ const { id: testReferenceId } = await ctx.adapter.create({
+ model: "user",
+ data: {
+ email: "test@email.com",
+ },
+ });
+ const { id: testSubscriptionId } = await ctx.adapter.create({
+ model: "subscription",
+ data: {
+ referenceId: testReferenceId,
+ stripeCustomerId: "cus_mock123",
+ status: "active",
+ plan: "starter",
+ },
+ });
+ const mockCheckoutSessionEvent = {
+ type: "checkout.session.completed",
+ data: {
+ object: {
+ mode: "subscription",
+ subscription: testSubscriptionId,
+ metadata: {
+ referenceId: testReferenceId,
+ subscriptionId: testSubscriptionId,
+ },
+ },
+ },
+ };
+
+ const mockSubscription = {
+ id: testSubscriptionId,
+ status: "active",
+ items: {
+ data: [
+ {
+ price: { id: process.env.STRIPE_PRICE_ID_1 },
+ quantity: 1,
+ },
+ ],
+ },
+ current_period_start: Math.floor(Date.now() / 1000),
+ current_period_end: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
+ };
+
+ const stripeForTest = {
+ ...stripeOptions.stripeClient,
+ subscriptions: {
+ ...stripeOptions.stripeClient.subscriptions,
+ retrieve: vi.fn().mockResolvedValue(mockSubscription),
+ },
+ webhooks: {
+ constructEventAsync: vi
+ .fn()
+ .mockResolvedValue(mockCheckoutSessionEvent),
+ },
+ };
+
+ const testOptions = {
+ ...stripeOptions,
+ stripeClient: stripeForTest as unknown as Stripe,
+ stripeWebhookSecret: "test_secret",
+ };
+
+ const testAuth = betterAuth({
+ baseURL: "http://localhost:3000",
+ database: memory,
+ emailAndPassword: {
+ enabled: true,
+ },
+ plugins: [stripe(testOptions)],
+ });
+
+ const testCtx = await testAuth.$context;
+
+ const mockRequest = new Request(
+ "http://localhost:3000/api/auth/stripe/webhook",
+ {
+ method: "POST",
+ headers: {
+ "stripe-signature": "test_signature",
+ },
+ body: JSON.stringify(mockCheckoutSessionEvent),
+ },
+ );
+ const response = await testAuth.handler(mockRequest);
+ expect(response.status).toBe(200);
+
+ const updatedSubscription = await testCtx.adapter.findOne({
+ model: "subscription",
+ where: [
+ {
+ field: "id",
+ value: testSubscriptionId,
+ },
+ ],
+ });
+
+ expect(updatedSubscription).toMatchObject({
+ id: testSubscriptionId,
+ status: "active",
+ periodStart: expect.any(Date),
+ periodEnd: expect.any(Date),
+ plan: "starter",
+ });
+ });
+
+ it("should handle subscription webhook events with trial", async () => {
+ const { id: testReferenceId } = await ctx.adapter.create({
+ model: "user",
+ data: {
+ email: "test@email.com",
+ },
+ });
+ const { id: testSubscriptionId } = await ctx.adapter.create({
+ model: "subscription",
+ data: {
+ referenceId: testReferenceId,
+ stripeCustomerId: "cus_mock123",
+ status: "incomplete",
+ plan: "starter",
+ },
+ });
+ const mockCheckoutSessionEvent = {
+ type: "checkout.session.completed",
+ data: {
+ object: {
+ mode: "subscription",
+ subscription: testSubscriptionId,
+ metadata: {
+ referenceId: testReferenceId,
+ subscriptionId: testSubscriptionId,
+ },
+ },
+ },
+ };
+
+ const mockSubscription = {
+ id: testSubscriptionId,
+ status: "active",
+ items: {
+ data: [
+ {
+ price: { id: process.env.STRIPE_PRICE_ID_1 },
+ quantity: 1,
+ },
+ ],
+ },
+ current_period_start: Math.floor(Date.now() / 1000),
+ current_period_end: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
+ trial_start: Math.floor(Date.now() / 1000),
+ trial_end: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
+ };
+
+ const stripeForTest = {
+ ...stripeOptions.stripeClient,
+ subscriptions: {
+ ...stripeOptions.stripeClient.subscriptions,
+ retrieve: vi.fn().mockResolvedValue(mockSubscription),
+ },
+ webhooks: {
+ constructEventAsync: vi
+ .fn()
+ .mockResolvedValue(mockCheckoutSessionEvent),
+ },
+ };
+
+ const testOptions = {
+ ...stripeOptions,
+ stripeClient: stripeForTest as unknown as Stripe,
+ stripeWebhookSecret: "test_secret",
+ };
+
+ const testAuth = betterAuth({
+ baseURL: "http://localhost:3000",
+ database: memory,
+ emailAndPassword: {
+ enabled: true,
+ },
+ plugins: [stripe(testOptions)],
+ });
+
+ const testCtx = await testAuth.$context;
+
+ const mockRequest = new Request(
+ "http://localhost:3000/api/auth/stripe/webhook",
+ {
+ method: "POST",
+ headers: {
+ "stripe-signature": "test_signature",
+ },
+ body: JSON.stringify(mockCheckoutSessionEvent),
+ },
+ );
+ const response = await testAuth.handler(mockRequest);
+ expect(response.status).toBe(200);
+
+ const updatedSubscription = await testCtx.adapter.findOne({
+ model: "subscription",
+ where: [
+ {
+ field: "id",
+ value: testSubscriptionId,
+ },
+ ],
+ });
+
+ expect(updatedSubscription).toMatchObject({
+ id: testSubscriptionId,
+ status: "active",
+ periodStart: expect.any(Date),
+ periodEnd: expect.any(Date),
+ plan: "starter",
+ trialStart: expect.any(Date),
+ trialEnd: expect.any(Date),
+ });
+ });
+
+ const { id: userId } = await ctx.adapter.create({
+ model: "user",
+ data: {
+ email: "delete-test@email.com",
+ },
+ });
+
+ it("should handle subscription deletion webhook", async () => {
+ const subId = "test_sub_delete";
+
+ await ctx.adapter.create({
+ model: "subscription",
+ data: {
+ referenceId: userId,
+ stripeCustomerId: "cus_delete_test",
+ status: "active",
+ plan: "starter",
+ stripeSubscriptionId: "sub_delete_test",
+ },
+ });
+
+ const subscription = await ctx.adapter.findOne({
+ model: "subscription",
+ where: [
+ {
+ field: "referenceId",
+ value: userId,
+ },
+ ],
+ });
+
+ const mockDeleteEvent = {
+ type: "customer.subscription.deleted",
+ data: {
+ object: {
+ id: "sub_delete_test",
+ customer: subscription?.stripeCustomerId,
+ status: "canceled",
+ metadata: {
+ referenceId: subscription?.referenceId,
+ subscriptionId: subscription?.id,
+ },
+ },
+ },
+ };
+
+ const stripeForTest = {
+ ...stripeOptions.stripeClient,
+ webhooks: {
+ constructEventAsync: vi.fn().mockResolvedValue(mockDeleteEvent),
+ },
+ subscriptions: {
+ retrieve: vi.fn().mockResolvedValue({
+ status: "canceled",
+ id: subId,
+ }),
+ },
+ };
+
+ const testOptions = {
+ ...stripeOptions,
+ stripeClient: stripeForTest as unknown as Stripe,
+ stripeWebhookSecret: "test_secret",
+ };
+
+ const testAuth = betterAuth({
+ baseURL: "http://localhost:3000",
+ emailAndPassword: {
+ enabled: true,
+ },
+ database: memory,
+ plugins: [stripe(testOptions)],
+ });
+
+ const mockRequest = new Request(
+ "http://localhost:3000/api/auth/stripe/webhook",
+ {
+ method: "POST",
+ headers: {
+ "stripe-signature": "test_signature",
+ },
+ body: JSON.stringify(mockDeleteEvent),
+ },
+ );
+
+ const response = await testAuth.handler(mockRequest);
+ expect(response.status).toBe(200);
+
+ if (subscription) {
+ const updatedSubscription = await ctx.adapter.findOne({
+ model: "subscription",
+ where: [
+ {
+ field: "id",
+ value: subscription.id,
+ },
+ ],
+ });
+ expect(updatedSubscription?.status).toBe("canceled");
+ }
+ });
+
+ it("should execute subscription event handlers", async () => {
+ const onSubscriptionComplete = vi.fn();
+ const onSubscriptionUpdate = vi.fn();
+ const onSubscriptionCancel = vi.fn();
+ const onSubscriptionDeleted = vi.fn();
+
+ const testOptions = {
+ ...stripeOptions,
+ subscription: {
+ ...stripeOptions.subscription,
+ onSubscriptionComplete,
+ onSubscriptionUpdate,
+ onSubscriptionCancel,
+ onSubscriptionDeleted,
+ },
+ stripeWebhookSecret: "test_secret",
+ } as unknown as StripeOptions;
+
+ const testAuth = betterAuth({
+ baseURL: "http://localhost:3000",
+ database: memory,
+ emailAndPassword: {
+ enabled: true,
+ },
+ plugins: [stripe(testOptions)],
+ });
+
+ // Test subscription complete handler
+ const completeEvent = {
+ type: "checkout.session.completed",
+ data: {
+ object: {
+ mode: "subscription",
+ subscription: "sub_123",
+ metadata: {
+ referenceId: "user_123",
+ subscriptionId: "sub_123",
+ },
+ },
+ },
+ };
+
+ const mockSubscription = {
+ status: "active",
+ items: {
+ data: [{ price: { id: process.env.STRIPE_PRICE_ID_1 } }],
+ },
+ current_period_start: Math.floor(Date.now() / 1000),
+ current_period_end: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
+ };
+
+ const mockStripeForEvents = {
+ ...testOptions.stripeClient,
+ subscriptions: {
+ retrieve: vi.fn().mockResolvedValue(mockSubscription),
+ },
+ webhooks: {
+ constructEventAsync: vi.fn().mockResolvedValue(completeEvent),
+ },
+ };
+
+ const eventTestOptions = {
+ ...testOptions,
+ stripeClient: mockStripeForEvents as unknown as Stripe,
+ };
+
+ const eventTestAuth = betterAuth({
+ baseURL: "http://localhost:3000",
+ database: memory,
+ emailAndPassword: { enabled: true },
+ plugins: [stripe(eventTestOptions)],
+ });
+
+ const { id: testSubscriptionId } = await ctx.adapter.create({
+ model: "subscription",
+ data: {
+ referenceId: userId,
+ stripeCustomerId: "cus_123",
+ stripeSubscriptionId: "sub_123",
+ status: "incomplete",
+ plan: "starter",
+ },
+ });
+
+ const webhookRequest = new Request(
+ "http://localhost:3000/api/auth/stripe/webhook",
+ {
+ method: "POST",
+ headers: {
+ "stripe-signature": "test_signature",
+ },
+ body: JSON.stringify(completeEvent),
+ },
+ );
+
+ await eventTestAuth.handler(webhookRequest);
+
+ expect(onSubscriptionComplete).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.any(Object),
+ subscription: expect.any(Object),
+ stripeSubscription: expect.any(Object),
+ plan: expect.any(Object),
+ }),
+ expect.objectContaining({
+ context: expect.any(Object),
+ _flag: expect.any(String),
+ }),
+ );
+
+ const updateEvent = {
+ type: "customer.subscription.updated",
+ data: {
+ object: {
+ id: testSubscriptionId,
+ customer: "cus_123",
+ status: "active",
+ items: {
+ data: [{ price: { id: process.env.STRIPE_PRICE_ID_1 } }],
+ },
+ current_period_start: Math.floor(Date.now() / 1000),
+ current_period_end: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
+ },
+ },
+ };
+
+ const updateRequest = new Request(
+ "http://localhost:3000/api/auth/stripe/webhook",
+ {
+ method: "POST",
+ headers: {
+ "stripe-signature": "test_signature",
+ },
+ body: JSON.stringify(updateEvent),
+ },
+ );
+
+ mockStripeForEvents.webhooks.constructEventAsync.mockReturnValue(
+ updateEvent,
+ );
+ await eventTestAuth.handler(updateRequest);
+ expect(onSubscriptionUpdate).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.any(Object),
+ subscription: expect.any(Object),
+ }),
+ );
+
+ const userCancelEvent = {
+ type: "customer.subscription.updated",
+ data: {
+ object: {
+ id: testSubscriptionId,
+ customer: "cus_123",
+ status: "active",
+ cancel_at_period_end: true,
+ cancellation_details: {
+ reason: "cancellation_requested",
+ comment: "Customer canceled subscription",
+ },
+ items: {
+ data: [{ price: { id: process.env.STRIPE_PRICE_ID_1 } }],
+ },
+ current_period_start: Math.floor(Date.now() / 1000),
+ current_period_end: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
+ },
+ },
+ };
+
+ const userCancelRequest = new Request(
+ "http://localhost:3000/api/auth/stripe/webhook",
+ {
+ method: "POST",
+ headers: {
+ "stripe-signature": "test_signature",
+ },
+ body: JSON.stringify(userCancelEvent),
+ },
+ );
+
+ mockStripeForEvents.webhooks.constructEventAsync.mockReturnValue(
+ userCancelEvent,
+ );
+ await eventTestAuth.handler(userCancelRequest);
+ const cancelEvent = {
+ type: "customer.subscription.updated",
+ data: {
+ object: {
+ id: testSubscriptionId,
+ customer: "cus_123",
+ status: "active",
+ cancel_at_period_end: true,
+ items: {
+ data: [{ price: { id: process.env.STRIPE_PRICE_ID_1 } }],
+ },
+ current_period_start: Math.floor(Date.now() / 1000),
+ current_period_end: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
+ },
+ },
+ };
+
+ const cancelRequest = new Request(
+ "http://localhost:3000/api/auth/stripe/webhook",
+ {
+ method: "POST",
+ headers: {
+ "stripe-signature": "test_signature",
+ },
+ body: JSON.stringify(cancelEvent),
+ },
+ );
+
+ mockStripeForEvents.webhooks.constructEventAsync.mockReturnValue(
+ cancelEvent,
+ );
+ await eventTestAuth.handler(cancelRequest);
+
+ expect(onSubscriptionCancel).toHaveBeenCalled();
+
+ const deleteEvent = {
+ type: "customer.subscription.deleted",
+ data: {
+ object: {
+ id: testSubscriptionId,
+ customer: "cus_123",
+ status: "canceled",
+ metadata: {
+ referenceId: userId,
+ subscriptionId: testSubscriptionId,
+ },
+ },
+ },
+ };
+
+ const deleteRequest = new Request(
+ "http://localhost:3000/api/auth/stripe/webhook",
+ {
+ method: "POST",
+ headers: {
+ "stripe-signature": "test_signature",
+ },
+ body: JSON.stringify(deleteEvent),
+ },
+ );
+
+ mockStripeForEvents.webhooks.constructEventAsync.mockReturnValue(
+ deleteEvent,
+ );
+ await eventTestAuth.handler(deleteRequest);
+
+ expect(onSubscriptionDeleted).toHaveBeenCalled();
+ });
+
+ it("should allow seat upgrades for the same plan", async () => {
+ const userRes = await authClient.signUp.email(
+ {
+ ...testUser,
+ email: "seat-upgrade@email.com",
+ },
+ {
+ throw: true,
+ },
+ );
+
+ const headers = new Headers();
+ await authClient.signIn.email(
+ {
+ ...testUser,
+ email: "seat-upgrade@email.com",
+ },
+ {
+ throw: true,
+ onSuccess: setCookieToHeader(headers),
+ },
+ );
+
+ await authClient.subscription.upgrade({
+ plan: "starter",
+ seats: 1,
+ fetchOptions: {
+ headers,
+ },
+ });
+
+ await ctx.adapter.update({
+ model: "subscription",
+ update: {
+ status: "active",
+ },
+ where: [
+ {
+ field: "referenceId",
+ value: userRes.user.id,
+ },
+ ],
+ });
+
+ const upgradeRes = await authClient.subscription.upgrade({
+ plan: "starter",
+ seats: 5,
+ fetchOptions: {
+ headers,
+ },
+ });
+
+ expect(upgradeRes.data?.url).toBeDefined();
+ });
+
+ it("should prevent duplicate subscriptions with same plan and same seats", async () => {
+ const userRes = await authClient.signUp.email(
+ {
+ ...testUser,
+ email: "duplicate-prevention@email.com",
+ },
+ {
+ throw: true,
+ },
+ );
+
+ const headers = new Headers();
+ await authClient.signIn.email(
+ {
+ ...testUser,
+ email: "duplicate-prevention@email.com",
+ },
+ {
+ throw: true,
+ onSuccess: setCookieToHeader(headers),
+ },
+ );
+
+ await authClient.subscription.upgrade({
+ plan: "starter",
+ seats: 3,
+ fetchOptions: {
+ headers,
+ },
+ });
+
+ await ctx.adapter.update({
+ model: "subscription",
+ update: {
+ status: "active",
+ seats: 3,
+ },
+ where: [
+ {
+ field: "referenceId",
+ value: userRes.user.id,
+ },
+ ],
+ });
+
+ const upgradeRes = await authClient.subscription.upgrade({
+ plan: "starter",
+ seats: 3,
+ fetchOptions: {
+ headers,
+ },
+ });
+
+ expect(upgradeRes.error).toBeDefined();
+ expect(upgradeRes.error?.message).toContain("already subscribed");
+ });
+
+ it("should only call Stripe customers.create once for signup and upgrade", async () => {
+ const userRes = await authClient.signUp.email(
+ { ...testUser, email: "single-create@email.com" },
+ { throw: true },
+ );
+
+ const headers = new Headers();
+ await authClient.signIn.email(
+ { ...testUser, email: "single-create@email.com" },
+ {
+ throw: true,
+ onSuccess: setCookieToHeader(headers),
+ },
+ );
+
+ await authClient.subscription.upgrade({
+ plan: "starter",
+ fetchOptions: { headers },
+ });
+
+ expect(mockStripe.customers.create).toHaveBeenCalledTimes(1);
+ });
+
+ it("should create billing portal session", async () => {
+ await authClient.signUp.email(
+ {
+ ...testUser,
+ email: "billing-portal@email.com",
+ },
+ {
+ throw: true,
+ },
+ );
+
+ const headers = new Headers();
+ await authClient.signIn.email(
+ {
+ ...testUser,
+ email: "billing-portal@email.com",
+ },
+ {
+ throw: true,
+ onSuccess: setCookieToHeader(headers),
+ },
+ );
+ const billingPortalRes = await authClient.subscription.billingPortal({
+ returnUrl: "/dashboard",
+ fetchOptions: {
+ headers,
+ },
+ });
+ expect(billingPortalRes.data?.url).toBe("https://billing.stripe.com/mock");
+ expect(billingPortalRes.data?.redirect).toBe(true);
+ expect(mockStripe.billingPortal.sessions.create).toHaveBeenCalledWith({
+ customer: expect.any(String),
+ return_url: "http://localhost:3000/dashboard",
+ });
+ });
+
+ it("should not update personal subscription when upgrading with an org referenceId", async () => {
+ const orgId = "org_b67GF32Cljh7u588AuEblmLVobclDRcP";
+
+ const testOptions = {
+ ...stripeOptions,
+ stripeClient: _stripe,
+ subscription: {
+ ...stripeOptions.subscription,
+ authorizeReference: async () => true,
+ },
+ } as unknown as StripeOptions;
+
+ const testAuth = betterAuth({
+ baseURL: "http://localhost:3000",
+ database: memory,
+ emailAndPassword: { enabled: true },
+ plugins: [stripe(testOptions)],
+ });
+ const testCtx = await testAuth.$context;
+
+ const testAuthClient = createAuthClient({
+ baseURL: "http://localhost:3000",
+ plugins: [bearer(), stripeClient({ subscription: true })],
+ fetchOptions: {
+ customFetchImpl: async (url, init) =>
+ testAuth.handler(new Request(url, init)),
+ },
+ });
+
+ // Sign up and sign in the user
+ const userRes = await testAuthClient.signUp.email(
+ { ...testUser, email: "org-ref@email.com" },
+ { throw: true },
+ );
+ const headers = new Headers();
+ await testAuthClient.signIn.email(
+ { ...testUser, email: "org-ref@email.com" },
+ { throw: true, onSuccess: setCookieToHeader(headers) },
+ );
+
+ // Create a personal subscription (referenceId = user id)
+ await testAuthClient.subscription.upgrade({
+ plan: "starter",
+ fetchOptions: { headers },
+ });
+
+ const personalSub = await testCtx.adapter.findOne({
+ model: "subscription",
+ where: [{ field: "referenceId", value: userRes.user.id }],
+ });
+ expect(personalSub).toBeTruthy();
+
+ await testCtx.adapter.update({
+ model: "subscription",
+ update: {
+ status: "active",
+ stripeSubscriptionId: "sub_personal_active_123",
+ },
+ where: [{ field: "id", value: personalSub!.id }],
+ });
+
+ mockStripe.subscriptions.list.mockResolvedValue({
+ data: [
+ {
+ id: "sub_personal_active_123",
+ status: "active",
+ items: {
+ data: [
+ {
+ id: "si_1",
+ price: { id: process.env.STRIPE_PRICE_ID_1 },
+ quantity: 1,
+ },
+ ],
+ },
+ },
+ ],
+ });
+
+ // Attempt to upgrade using an org referenceId
+ const upgradeRes = await testAuthClient.subscription.upgrade({
+ plan: "starter",
+ referenceId: orgId,
+ fetchOptions: { headers },
+ });
+ console.log(upgradeRes);
+
+ // // It should NOT go through billing portal (which would update the personal sub)
+ expect(mockStripe.billingPortal.sessions.create).not.toHaveBeenCalled();
+ expect(upgradeRes.data?.url).toBeDefined();
+
+ const orgSub = await testCtx.adapter.findOne({
+ model: "subscription",
+ where: [{ field: "referenceId", value: orgId }],
+ });
+ expect(orgSub).toMatchObject({
+ referenceId: orgId,
+ status: "incomplete",
+ plan: "starter",
+ });
+
+ const personalAfter = await testCtx.adapter.findOne({
+ model: "subscription",
+ where: [{ field: "id", value: personalSub!.id }],
+ });
+ expect(personalAfter?.status).toBe("active");
+ });
+
+ it("should prevent multiple free trials for the same user", async () => {
+ // Create a user
+ const userRes = await authClient.signUp.email(
+ { ...testUser, email: "trial-prevention@email.com" },
+ { throw: true },
+ );
+
+ const headers = new Headers();
+ await authClient.signIn.email(
+ { ...testUser, email: "trial-prevention@email.com" },
+ {
+ throw: true,
+ onSuccess: setCookieToHeader(headers),
+ },
+ );
+
+ // First subscription with trial
+ const firstUpgradeRes = await authClient.subscription.upgrade({
+ plan: "starter",
+ fetchOptions: { headers },
+ });
+
+ expect(firstUpgradeRes.data?.url).toBeDefined();
+
+ // Simulate the subscription being created with trial data
+ await ctx.adapter.update({
+ model: "subscription",
+ update: {
+ status: "trialing",
+ trialStart: new Date(),
+ trialEnd: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days from now
+ },
+ where: [
+ {
+ field: "referenceId",
+ value: userRes.user.id,
+ },
+ ],
+ });
+
+ // Cancel the subscription
+ await ctx.adapter.update({
+ model: "subscription",
+ update: {
+ status: "canceled",
+ },
+ where: [
+ {
+ field: "referenceId",
+ value: userRes.user.id,
+ },
+ ],
+ });
+
+ // Try to subscribe again - should NOT get a trial
+ const secondUpgradeRes = await authClient.subscription.upgrade({
+ plan: "starter",
+ fetchOptions: { headers },
+ });
+
+ expect(secondUpgradeRes.data?.url).toBeDefined();
+
+ // Verify that the checkout session was created without trial_period_days
+ // We can't directly test the Stripe session, but we can verify the logic
+ // by checking that the user has trial history
+ const subscriptions = (await ctx.adapter.findMany({
+ model: "subscription",
+ where: [
+ {
+ field: "referenceId",
+ value: userRes.user.id,
+ },
+ ],
+ })) as Subscription[];
+
+ // Should have 2 subscriptions (first canceled, second new)
+ expect(subscriptions).toHaveLength(2);
+
+ // At least one should have trial data
+ const hasTrialData = subscriptions.some(
+ (s: Subscription) => s.trialStart || s.trialEnd,
+ );
+ expect(hasTrialData).toBe(true);
+ });
+
+ it("should upgrade existing subscription instead of creating new one", async () => {
+ // Reset mocks for this test
+ vi.clearAllMocks();
+
+ // Create a user
+ const userRes = await authClient.signUp.email(
+ { ...testUser, email: "upgrade-existing@email.com" },
+ { throw: true },
+ );
+
+ const headers = new Headers();
+ await authClient.signIn.email(
+ { ...testUser, email: "upgrade-existing@email.com" },
+ {
+ throw: true,
+ onSuccess: setCookieToHeader(headers),
+ },
+ );
+
+ // Mock customers.list to find existing customer
+ mockStripe.customers.list.mockResolvedValueOnce({
+ data: [{ id: "cus_test_123" }],
+ });
+
+ // First create a starter subscription
+ await authClient.subscription.upgrade({
+ plan: "starter",
+ fetchOptions: { headers },
+ });
+
+ // Simulate the subscription being active
+ const starterSub = await ctx.adapter.findOne({
+ model: "subscription",
+ where: [
+ {
+ field: "referenceId",
+ value: userRes.user.id,
+ },
+ ],
+ });
+
+ await ctx.adapter.update({
+ model: "subscription",
+ update: {
+ status: "active",
+ stripeSubscriptionId: "sub_active_test_123",
+ stripeCustomerId: "cus_mock123", // Use the same customer ID as the mock
+ },
+ where: [
+ {
+ field: "id",
+ value: starterSub!.id,
+ },
+ ],
+ });
+
+ // Also update the user with the Stripe customer ID
+ await ctx.adapter.update({
+ model: "user",
+ update: {
+ stripeCustomerId: "cus_mock123",
+ },
+ where: [
+ {
+ field: "id",
+ value: userRes.user.id,
+ },
+ ],
+ });
+
+ // Mock Stripe subscriptions.list to return the active subscription
+ mockStripe.subscriptions.list.mockResolvedValueOnce({
+ data: [
+ {
+ id: "sub_active_test_123",
+ status: "active",
+ items: {
+ data: [
+ {
+ id: "si_test_123",
+ price: { id: process.env.STRIPE_PRICE_ID_1 },
+ quantity: 1,
+ current_period_start: Math.floor(Date.now() / 1000),
+ current_period_end:
+ Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
+ },
+ ],
+ },
+ },
+ ],
+ });
+
+ // Clear mock calls before the upgrade
+ mockStripe.checkout.sessions.create.mockClear();
+ mockStripe.billingPortal.sessions.create.mockClear();
+
+ // Now upgrade to premium plan - should use billing portal to update existing subscription
+ const upgradeRes = await authClient.subscription.upgrade({
+ plan: "premium",
+ fetchOptions: { headers },
+ });
+
+ // Verify that billing portal was called (indicating update, not new subscription)
+ expect(mockStripe.billingPortal.sessions.create).toHaveBeenCalledWith(
+ expect.objectContaining({
+ customer: "cus_mock123",
+ flow_data: expect.objectContaining({
+ type: "subscription_update_confirm",
+ subscription_update_confirm: expect.objectContaining({
+ subscription: "sub_active_test_123",
+ }),
+ }),
+ }),
+ );
+
+ // Should not create a new checkout session
+ expect(mockStripe.checkout.sessions.create).not.toHaveBeenCalled();
+
+ // Verify the response has a redirect URL
+ expect(upgradeRes.data?.url).toBe("https://billing.stripe.com/mock");
+ expect(upgradeRes.data?.redirect).toBe(true);
+
+ // Verify no new subscription was created in the database
+ const allSubs = await ctx.adapter.findMany({
+ model: "subscription",
+ where: [
+ {
+ field: "referenceId",
+ value: userRes.user.id,
+ },
+ ],
+ });
+ expect(allSubs).toHaveLength(1); // Should still have only one subscription
+ });
+
+ it("should prevent multiple free trials across different plans", async () => {
+ // Create a user
+ const userRes = await authClient.signUp.email(
+ { ...testUser, email: "cross-plan-trial@email.com" },
+ { throw: true },
+ );
+
+ const headers = new Headers();
+ await authClient.signIn.email(
+ { ...testUser, email: "cross-plan-trial@email.com" },
+ {
+ throw: true,
+ onSuccess: setCookieToHeader(headers),
+ },
+ );
+
+ // First subscription with trial on starter plan
+ const firstUpgradeRes = await authClient.subscription.upgrade({
+ plan: "starter",
+ fetchOptions: { headers },
+ });
+
+ expect(firstUpgradeRes.data?.url).toBeDefined();
+
+ // Simulate the subscription being created with trial data
+ await ctx.adapter.update({
+ model: "subscription",
+ update: {
+ status: "trialing",
+ trialStart: new Date(),
+ trialEnd: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
+ },
+ where: [
+ {
+ field: "referenceId",
+ value: userRes.user.id,
+ },
+ ],
+ });
+
+ // Cancel the subscription
+ await ctx.adapter.update({
+ model: "subscription",
+ update: {
+ status: "canceled",
+ },
+ where: [
+ {
+ field: "referenceId",
+ value: userRes.user.id,
+ },
+ ],
+ });
+
+ // Try to subscribe to a different plan - should NOT get a trial
+ const secondUpgradeRes = await authClient.subscription.upgrade({
+ plan: "premium",
+ fetchOptions: { headers },
+ });
+
+ expect(secondUpgradeRes.data?.url).toBeDefined();
+
+ // Verify that the user has trial history from the first plan
+ const subscriptions = (await ctx.adapter.findMany({
+ model: "subscription",
+ where: [
+ {
+ field: "referenceId",
+ value: userRes.user.id,
+ },
+ ],
+ })) as Subscription[];
+
+ // Should have at least 1 subscription (the starter with trial data)
+ expect(subscriptions.length).toBeGreaterThanOrEqual(1);
+
+ // The starter subscription should have trial data
+ const starterSub = subscriptions.find(
+ (s: Subscription) => s.plan === "starter",
+ ) as Subscription | undefined;
+ expect(starterSub?.trialStart).toBeDefined();
+ expect(starterSub?.trialEnd).toBeDefined();
+
+ // Verify that the trial eligibility logic is working by checking
+ // that the user has ever had a trial (which should prevent future trials)
+ const hasEverTrialed = subscriptions.some((s: Subscription) => {
+ const hadTrial =
+ !!(s.trialStart || s.trialEnd) || s.status === "trialing";
+ return hadTrial;
+ });
+ expect(hasEverTrialed).toBe(true);
+ });
+
+ it("should update stripe customer email when user email changes", async () => {
+ // Setup mock for customer retrieve and update
+ mockStripe.customers.retrieve = vi.fn().mockResolvedValue({
+ id: "cus_mock123",
+ email: "test@email.com",
+ deleted: false,
+ });
+ mockStripe.customers.update = vi.fn().mockResolvedValue({
+ id: "cus_mock123",
+ email: "newemail@example.com",
+ });
+
+ // Sign up a user
+ const userRes = await authClient.signUp.email(testUser, {
+ throw: true,
+ });
+
+ expect(userRes.user).toBeDefined();
+
+ // Verify customer was created during signup
+ expect(mockStripe.customers.create).toHaveBeenCalledWith({
+ email: testUser.email,
+ name: testUser.name,
+ metadata: {
+ userId: userRes.user.id,
+ },
+ });
+
+ // Clear mocks to track the update
+ vi.clearAllMocks();
+
+ // Re-setup the retrieve mock for the update flow
+ mockStripe.customers.retrieve = vi.fn().mockResolvedValue({
+ id: "cus_mock123",
+ email: "test@email.com",
+ deleted: false,
+ });
+ mockStripe.customers.update = vi.fn().mockResolvedValue({
+ id: "cus_mock123",
+ email: "newemail@example.com",
+ });
+
+ // Create a mock request context
+ const mockRequest = new Request("http://localhost:3000/api/test", {
+ method: "POST",
+ headers: {
+ "content-type": "application/json",
+ },
+ });
+
+ // Update the user's email using internal adapter (which triggers hooks)
+ await ctx.internalAdapter.updateUserByEmail(
+ testUser.email,
+ {
+ email: "newemail@example.com",
+ },
+ {
+ request: mockRequest,
+ context: ctx,
+ } as any,
+ );
+
+ // Verify that Stripe customer.retrieve was called
+ expect(mockStripe.customers.retrieve).toHaveBeenCalledWith("cus_mock123");
+
+ // Verify that Stripe customer.update was called with the new email
+ expect(mockStripe.customers.update).toHaveBeenCalledWith("cus_mock123", {
+ email: "newemail@example.com",
+ });
+ });
+
+ describe("getCustomerCreateParams", () => {
+ it("should call getCustomerCreateParams and merge with default params", async () => {
+ const getCustomerCreateParamsMock = vi
+ .fn()
+ .mockResolvedValue({ metadata: { customField: "customValue" } });
+
+ const testOptions = {
+ ...stripeOptions,
+ createCustomerOnSignUp: true,
+ getCustomerCreateParams: getCustomerCreateParamsMock,
+ } satisfies StripeOptions;
+
+ const testAuth = betterAuth({
+ database: memory,
+ baseURL: "http://localhost:3000",
+ emailAndPassword: {
+ enabled: true,
+ },
+ plugins: [stripe(testOptions)],
+ });
+
+ const testAuthClient = createAuthClient({
+ baseURL: "http://localhost:3000",
+ plugins: [bearer(), stripeClient({ subscription: true })],
+ fetchOptions: {
+ customFetchImpl: async (url, init) =>
+ testAuth.handler(new Request(url, init)),
+ },
+ });
+
+ // Sign up a user
+ const userRes = await testAuthClient.signUp.email(
+ {
+ email: "custom-params@email.com",
+ password: "password",
+ name: "Custom User",
+ },
+ {
+ throw: true,
+ },
+ );
+
+ // Verify getCustomerCreateParams was called
+ expect(getCustomerCreateParamsMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ id: userRes.user.id,
+ email: "custom-params@email.com",
+ name: "Custom User",
+ }),
+ expect.objectContaining({
+ context: expect.any(Object),
+ }),
+ );
+
+ // Verify customer was created with merged params
+ expect(mockStripe.customers.create).toHaveBeenCalledWith(
+ expect.objectContaining({
+ email: "custom-params@email.com",
+ name: "Custom User",
+ metadata: expect.objectContaining({
+ userId: userRes.user.id,
+ customField: "customValue",
+ }),
+ }),
+ );
+ });
+
+ it("should use getCustomerCreateParams to add custom address", async () => {
+ const getCustomerCreateParamsMock = vi.fn().mockResolvedValue({
+ address: {
+ line1: "123 Main St",
+ city: "San Francisco",
+ state: "CA",
+ postal_code: "94111",
+ country: "US",
+ },
+ });
+
+ const testOptions = {
+ ...stripeOptions,
+ createCustomerOnSignUp: true,
+ getCustomerCreateParams: getCustomerCreateParamsMock,
+ } satisfies StripeOptions;
+
+ const testAuth = betterAuth({
+ database: memory,
+ baseURL: "http://localhost:3000",
+ emailAndPassword: {
+ enabled: true,
+ },
+ plugins: [stripe(testOptions)],
+ });
+
+ const testAuthClient = createAuthClient({
+ baseURL: "http://localhost:3000",
+ plugins: [bearer(), stripeClient({ subscription: true })],
+ fetchOptions: {
+ customFetchImpl: async (url, init) =>
+ testAuth.handler(new Request(url, init)),
+ },
+ });
+
+ // Sign up a user
+ await testAuthClient.signUp.email(
+ {
+ email: "address-user@email.com",
+ password: "password",
+ name: "Address User",
+ },
+ {
+ throw: true,
+ },
+ );
+
+ // Verify customer was created with address
+ expect(mockStripe.customers.create).toHaveBeenCalledWith(
+ expect.objectContaining({
+ email: "address-user@email.com",
+ name: "Address User",
+ address: {
+ line1: "123 Main St",
+ city: "San Francisco",
+ state: "CA",
+ postal_code: "94111",
+ country: "US",
+ },
+ metadata: expect.objectContaining({
+ userId: expect.any(String),
+ }),
+ }),
+ );
+ });
+
+ it("should properly merge nested objects using defu", async () => {
+ const getCustomerCreateParamsMock = vi.fn().mockResolvedValue({
+ metadata: {
+ customField: "customValue",
+ anotherField: "anotherValue",
+ },
+ phone: "+1234567890",
+ });
+
+ const testOptions = {
+ ...stripeOptions,
+ createCustomerOnSignUp: true,
+ getCustomerCreateParams: getCustomerCreateParamsMock,
+ } satisfies StripeOptions;
+
+ const testAuth = betterAuth({
+ database: memory,
+ baseURL: "http://localhost:3000",
+ emailAndPassword: {
+ enabled: true,
+ },
+ plugins: [stripe(testOptions)],
+ });
+
+ const testAuthClient = createAuthClient({
+ baseURL: "http://localhost:3000",
+ plugins: [bearer(), stripeClient({ subscription: true })],
+ fetchOptions: {
+ customFetchImpl: async (url, init) =>
+ testAuth.handler(new Request(url, init)),
+ },
+ });
+
+ // Sign up a user
+ const userRes = await testAuthClient.signUp.email(
+ {
+ email: "merge-test@email.com",
+ password: "password",
+ name: "Merge User",
+ },
+ {
+ throw: true,
+ },
+ );
+
+ // Verify customer was created with properly merged params
+ // defu merges objects and preserves all fields
+ expect(mockStripe.customers.create).toHaveBeenCalledWith(
+ expect.objectContaining({
+ email: "merge-test@email.com",
+ name: "Merge User",
+ phone: "+1234567890",
+ metadata: {
+ userId: userRes.user.id,
+ customField: "customValue",
+ anotherField: "anotherValue",
+ },
+ }),
+ );
+ });
+
+ it("should work without getCustomerCreateParams", async () => {
+ // This test ensures backward compatibility
+ const testOptions = {
+ ...stripeOptions,
+ createCustomerOnSignUp: true,
+ // No getCustomerCreateParams provided
+ } satisfies StripeOptions;
+
+ const testAuth = betterAuth({
+ database: memory,
+ baseURL: "http://localhost:3000",
+ emailAndPassword: {
+ enabled: true,
+ },
+ plugins: [stripe(testOptions)],
+ });
+
+ const testAuthClient = createAuthClient({
+ baseURL: "http://localhost:3000",
+ plugins: [bearer(), stripeClient({ subscription: true })],
+ fetchOptions: {
+ customFetchImpl: async (url, init) =>
+ testAuth.handler(new Request(url, init)),
+ },
+ });
+
+ // Sign up a user
+ const userRes = await testAuthClient.signUp.email(
+ {
+ email: "no-custom-params@email.com",
+ password: "password",
+ name: "Default User",
+ },
+ {
+ throw: true,
+ },
+ );
+
+ // Verify customer was created with default params only
+ expect(mockStripe.customers.create).toHaveBeenCalledWith({
+ email: "no-custom-params@email.com",
+ name: "Default User",
+ metadata: {
+ userId: userRes.user.id,
+ },
+ });
+ });
+ });
+});
diff --git a/src/types.ts b/src/types.ts
new file mode 100644
index 0000000..fe3da25
--- /dev/null
+++ b/src/types.ts
@@ -0,0 +1,331 @@
+import type {
+ GenericEndpointContext,
+ InferOptionSchema,
+ Session,
+ User,
+} from "better-auth";
+import type Stripe from "stripe";
+import type { subscriptions, user } from "./schema";
+
+export type StripePlan = {
+ /**
+ * Monthly price id
+ */
+ priceId?: string;
+ /**
+ * To use lookup key instead of price id
+ *
+ * https://docs.stripe.com/products-prices/
+ * manage-prices#lookup-keys
+ */
+ lookupKey?: string;
+ /**
+ * A yearly discount price id
+ *
+ * useful when you want to offer a discount for
+ * yearly subscription
+ */
+ annualDiscountPriceId?: string;
+ /**
+ * To use lookup key instead of price id
+ *
+ * https://docs.stripe.com/products-prices/
+ * manage-prices#lookup-keys
+ */
+ annualDiscountLookupKey?: string;
+ /**
+ * Plan name
+ */
+ name: string;
+ /**
+ * Limits for the plan
+ */
+ limits?: Record;
+ /**
+ * Plan group name
+ *
+ * useful when you want to group plans or
+ * when a user can subscribe to multiple plans.
+ */
+ group?: string;
+ /**
+ * Free trial days
+ */
+ freeTrial?: {
+ /**
+ * Number of days
+ */
+ days: number;
+ /**
+ * A function that will be called when the trial
+ * starts.
+ *
+ * @param subscription
+ * @returns
+ */
+ onTrialStart?: (subscription: Subscription) => Promise;
+ /**
+ * A function that will be called when the trial
+ * ends
+ *
+ * @param subscription - Subscription
+ * @returns
+ */
+ onTrialEnd?: (
+ data: {
+ subscription: Subscription;
+ },
+ ctx: GenericEndpointContext,
+ ) => Promise;
+ /**
+ * A function that will be called when the trial
+ * expired.
+ * @param subscription - Subscription
+ * @returns
+ */
+ onTrialExpired?: (
+ subscription: Subscription,
+ ctx: GenericEndpointContext,
+ ) => Promise;
+ };
+};
+
+export interface Subscription {
+ /**
+ * Database identifier
+ */
+ id: string;
+ /**
+ * The plan name
+ */
+ plan: string;
+ /**
+ * Stripe customer id
+ */
+ stripeCustomerId?: string;
+ /**
+ * Stripe subscription id
+ */
+ stripeSubscriptionId?: string;
+ /**
+ * Trial start date
+ */
+ trialStart?: Date;
+ /**
+ * Trial end date
+ */
+ trialEnd?: Date;
+ /**
+ * Price Id for the subscription
+ */
+ priceId?: string;
+ /**
+ * To what reference id the subscription belongs to
+ * @example
+ * - userId for a user
+ * - workspace id for a saas platform
+ * - website id for a hosting platform
+ *
+ * @default - userId
+ */
+ referenceId: string;
+ /**
+ * Subscription status
+ */
+ status:
+ | "active"
+ | "canceled"
+ | "incomplete"
+ | "incomplete_expired"
+ | "past_due"
+ | "paused"
+ | "trialing"
+ | "unpaid";
+ /**
+ * The billing cycle start date
+ */
+ periodStart?: Date;
+ /**
+ * The billing cycle end date
+ */
+ periodEnd?: Date;
+ /**
+ * Cancel at period end
+ */
+ cancelAtPeriodEnd?: boolean;
+ /**
+ * A field to group subscriptions so you can have multiple subscriptions
+ * for one reference id
+ */
+ groupId?: string;
+ /**
+ * Number of seats for the subscription (useful for team plans)
+ */
+ seats?: number;
+}
+
+export interface StripeOptions {
+ /**
+ * Stripe Client
+ */
+ stripeClient: Stripe;
+ /**
+ * Stripe Webhook Secret
+ *
+ * @description Stripe webhook secret key
+ */
+ stripeWebhookSecret: string;
+ /**
+ * Enable customer creation when a user signs up
+ */
+ createCustomerOnSignUp?: boolean;
+ /**
+ * A callback to run after a customer has been created
+ * @param customer - Customer Data
+ * @param stripeCustomer - Stripe Customer Data
+ * @returns
+ */
+ onCustomerCreate?: (
+ data: {
+ stripeCustomer: Stripe.Customer;
+ user: User & { stripeCustomerId: string };
+ },
+ ctx: GenericEndpointContext,
+ ) => Promise;
+ /**
+ * A custom function to get the customer create
+ * params
+ * @param data - data containing user and session
+ * @returns
+ */
+ getCustomerCreateParams?: (
+ user: User,
+ ctx: GenericEndpointContext,
+ ) => Promise>;
+ /**
+ * Subscriptions
+ */
+ subscription?: {
+ enabled: boolean;
+ /**
+ * Subscription Configuration
+ */
+ /**
+ * List of plan
+ */
+ plans: StripePlan[] | (() => StripePlan[] | Promise);
+ /**
+ * Require email verification before a user is allowed to upgrade
+ * their subscriptions
+ *
+ * @default false
+ */
+ requireEmailVerification?: boolean;
+ /**
+ * A callback to run after a user has subscribed to a package
+ * @param event - Stripe Event
+ * @param subscription - Subscription Data
+ * @returns
+ */
+ onSubscriptionComplete?: (
+ data: {
+ event: Stripe.Event;
+ stripeSubscription: Stripe.Subscription;
+ subscription: Subscription;
+ plan: StripePlan;
+ },
+ ctx: GenericEndpointContext,
+ ) => Promise;
+ /**
+ * A callback to run after a user is about to cancel their subscription
+ * @returns
+ */
+ onSubscriptionUpdate?: (data: {
+ event: Stripe.Event;
+ subscription: Subscription;
+ }) => Promise;
+ /**
+ * A callback to run after a user is about to cancel their subscription
+ * @returns
+ */
+ onSubscriptionCancel?: (data: {
+ event?: Stripe.Event;
+ subscription: Subscription;
+ stripeSubscription: Stripe.Subscription;
+ cancellationDetails?: Stripe.Subscription.CancellationDetails | null;
+ }) => Promise;
+ /**
+ * A function to check if the reference id is valid
+ * and belongs to the user
+ *
+ * @param data - data containing user, session and referenceId
+ * @param ctx - the context object
+ * @returns
+ */
+ authorizeReference?: (
+ data: {
+ user: User & Record;
+ session: Session & Record;
+ referenceId: string;
+ action:
+ | "upgrade-subscription"
+ | "list-subscription"
+ | "cancel-subscription"
+ | "restore-subscription"
+ | "meter-event"
+ | "billing-portal";
+ },
+ ctx: GenericEndpointContext,
+ ) => Promise;
+ /**
+ * A callback to run after a user has deleted their subscription
+ * @returns
+ */
+ onSubscriptionDeleted?: (data: {
+ event: Stripe.Event;
+ stripeSubscription: Stripe.Subscription;
+ subscription: Subscription;
+ }) => Promise;
+ /**
+ * parameters for session create params
+ *
+ * @param data - data containing user, session and plan
+ * @param ctx - the context object
+ */
+ getCheckoutSessionParams?: (
+ data: {
+ user: User & Record;
+ session: Session & Record;
+ plan: StripePlan;
+ subscription: Subscription;
+ },
+ ctx: GenericEndpointContext,
+ ) =>
+ | Promise<{
+ params?: Stripe.Checkout.SessionCreateParams;
+ options?: Stripe.RequestOptions;
+ }>
+ | {
+ params?: Stripe.Checkout.SessionCreateParams;
+ options?: Stripe.RequestOptions;
+ };
+ /**
+ * Enable organization subscription
+ */
+ organization?: {
+ enabled: boolean;
+ };
+ };
+ /**
+ * A callback to run after a stripe event is received
+ * @param event - Stripe Event
+ * @returns
+ */
+ onEvent?: (event: Stripe.Event) => Promise;
+ /**
+ * Schema for the stripe plugin
+ */
+ schema?: InferOptionSchema;
+}
+
+export interface InputSubscription extends Omit {}
diff --git a/src/utils.ts b/src/utils.ts
new file mode 100644
index 0000000..3f6907d
--- /dev/null
+++ b/src/utils.ts
@@ -0,0 +1,30 @@
+import type { StripeOptions } from "./types";
+
+export async function getPlans(options: StripeOptions) {
+ return typeof options?.subscription?.plans === "function"
+ ? await options.subscription?.plans()
+ : options.subscription?.plans;
+}
+
+export async function getPlanByPriceInfo(
+ options: StripeOptions,
+ priceId: string,
+ priceLookupKey: string | null,
+) {
+ return await getPlans(options).then((res) =>
+ res?.find(
+ (plan) =>
+ plan.priceId === priceId ||
+ plan.annualDiscountPriceId === priceId ||
+ (priceLookupKey &&
+ (plan.lookupKey === priceLookupKey ||
+ plan.annualDiscountLookupKey === priceLookupKey)),
+ ),
+ );
+}
+
+export async function getPlanByName(options: StripeOptions, name: string) {
+ return await getPlans(options).then((res) =>
+ res?.find((plan) => plan.name.toLowerCase() === name.toLowerCase()),
+ );
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..51c9211
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "rootDir": "./src",
+ "outDir": "./dist",
+ "lib": ["esnext", "dom", "dom.iterable"],
+ "moduleResolution": "bundler"
+ },
+ "include": ["src"]
+}
diff --git a/vitest.config.ts b/vitest.config.ts
new file mode 100644
index 0000000..784805e
--- /dev/null
+++ b/vitest.config.ts
@@ -0,0 +1,9 @@
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ root: ".",
+ test: {
+ clearMocks: true,
+ globals: true,
+ },
+});