@stripe/link-integrations-better-auth 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/dist/chunk-POKWE5ZI.js +11 -0
- package/dist/chunk-POKWE5ZI.js.map +1 -0
- package/dist/client.d.ts +15 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +19 -0
- package/dist/client.js.map +1 -0
- package/dist/error-codes.d.ts +5 -0
- package/dist/error-codes.d.ts.map +1 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +171 -0
- package/dist/index.js.map +1 -0
- package/dist/routes.d.ts +52 -0
- package/dist/routes.d.ts.map +1 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Stripe, LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Link for Better Auth
|
|
2
|
+
|
|
3
|
+
Sign in with Link or connect a Link wallet to an existing user. Better Auth manages OAuth, token storage, and refresh.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
Requires Node.js 22+ and Better Auth 1.7.5+.
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pnpm add @stripe/link-integrations-better-auth better-auth
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
[Register a Link OAuth client](https://docs.stripe.com/agentic-commerce/link-cli/oauth) with the callback URL `https://yourapp.com/api/auth/callback/link`. Adjust the path if you use a custom Better Auth base path, and register any local development callback too.
|
|
14
|
+
|
|
15
|
+
Add the plugin to your server configuration:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { betterAuth } from 'better-auth';
|
|
19
|
+
import { link } from '@stripe/link-integrations-better-auth';
|
|
20
|
+
|
|
21
|
+
export const auth = betterAuth({
|
|
22
|
+
account: {
|
|
23
|
+
encryptOAuthTokens: true,
|
|
24
|
+
accountLinking: {
|
|
25
|
+
trustedProviders: ['link'],
|
|
26
|
+
// Allow the wallet email to differ from the app user's email.
|
|
27
|
+
allowDifferentEmails: true,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
plugins: [
|
|
31
|
+
link({
|
|
32
|
+
clientId: process.env.LINK_CLIENT_ID!,
|
|
33
|
+
clientSecret: process.env.LINK_CLIENT_SECRET!,
|
|
34
|
+
publishableKey: process.env.STRIPE_PUBLISHABLE_KEY!,
|
|
35
|
+
}),
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Then register the client plugin:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { linkClient } from '@stripe/link-integrations-better-auth/client';
|
|
44
|
+
import { createAuthClient } from 'better-auth/react';
|
|
45
|
+
|
|
46
|
+
export const authClient = createAuthClient({ plugins: [linkClient()] });
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Default scopes are `payment_methods.agentic` and `userinfo:read`. For sign-in only, set `scopes: ['userinfo:read']` in `link(...)`. Keep `userinfo:read` when customizing scopes.
|
|
50
|
+
|
|
51
|
+
## Connect a wallet
|
|
52
|
+
|
|
53
|
+
After the user signs in to your app:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
await authClient.link.connect({ callbackURL: '/settings' });
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
For server-side usage:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
await auth.api.connectLink({
|
|
63
|
+
body: { callbackURL: '/settings' },
|
|
64
|
+
headers: await headers(),
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
This starts Better Auth's OAuth linking flow and returns the user to `/settings` after authorization.
|
|
69
|
+
|
|
70
|
+
`connect` follows Better Auth's configured client error handling.
|
|
71
|
+
|
|
72
|
+
## Disconnect a wallet
|
|
73
|
+
|
|
74
|
+
Find the connected account and pass its Better Auth record ID:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
const { data: accounts, error } = await authClient.listAccounts();
|
|
78
|
+
if (error) throw new Error(error.message);
|
|
79
|
+
|
|
80
|
+
const account = accounts?.find((account) => account.providerId === 'link');
|
|
81
|
+
if (account) {
|
|
82
|
+
const { error } = await authClient.link.disconnect({ accountId: account.id });
|
|
83
|
+
if (error) throw new Error(error.message);
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Disconnect requires a fresh session and protects the user's last sign-in method by default. It revokes Link access before deleting the local account; failed revocation keeps the connection intact for retry.
|
|
88
|
+
|
|
89
|
+
Use `account.id` for account operations. `account.accountId` is the stable Link user ID.
|
|
90
|
+
|
|
91
|
+
## Sign in with Link
|
|
92
|
+
|
|
93
|
+
To let users sign in or create an app account with Link:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
await authClient.signIn.social({
|
|
97
|
+
provider: 'link',
|
|
98
|
+
callbackURL: '/dashboard',
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Better Auth's account-linking and email-verification policies apply. Use `authClient.accountInfo()` for the connected profile and server-side `auth.api.getAccessToken()` for Link API calls.
|
|
103
|
+
|
|
104
|
+
## Example
|
|
105
|
+
|
|
106
|
+
See the [Next.js example](example/README.md) for a runnable app with sign-in, wallet connection, and disconnect.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// src/error-codes.ts
|
|
2
|
+
import { defineErrorCodes } from "better-auth";
|
|
3
|
+
var LINK_ERROR_CODES = defineErrorCodes({
|
|
4
|
+
LINK_REFRESH_TOKEN_NOT_FOUND: "Link refresh token is missing. The account remains connected.",
|
|
5
|
+
LINK_REVOCATION_FAILED: "Unable to revoke Link access. The account remains connected; try again."
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
LINK_ERROR_CODES
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=chunk-POKWE5ZI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/error-codes.ts"],"sourcesContent":["import { defineErrorCodes } from 'better-auth';\n\nexport const LINK_ERROR_CODES = defineErrorCodes({\n LINK_REFRESH_TOKEN_NOT_FOUND:\n 'Link refresh token is missing. The account remains connected.',\n LINK_REVOCATION_FAILED:\n 'Unable to revoke Link access. The account remains connected; try again.',\n});\n"],"mappings":";AAAA,SAAS,wBAAwB;AAE1B,IAAM,mBAAmB,iBAAiB;AAAA,EAC/C,8BACE;AAAA,EACF,wBACE;AACJ,CAAC;","names":[]}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { link } from './index';
|
|
2
|
+
export declare const linkClient: () => {
|
|
3
|
+
id: "link";
|
|
4
|
+
$InferServerPlugin: ReturnType<typeof link>;
|
|
5
|
+
pathMethods: {
|
|
6
|
+
'/link/connect': "POST";
|
|
7
|
+
'/link/disconnect': "POST";
|
|
8
|
+
};
|
|
9
|
+
$ERROR_CODES: {
|
|
10
|
+
LINK_REFRESH_TOKEN_NOT_FOUND: import("better-auth").RawError<"LINK_REFRESH_TOKEN_NOT_FOUND">;
|
|
11
|
+
LINK_REVOCATION_FAILED: import("better-auth").RawError<"LINK_REVOCATION_FAILED">;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
export { LINK_ERROR_CODES } from './error-codes';
|
|
15
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAEpC,eAAO,MAAM,UAAU;;wBAGO,UAAU,CAAC,OAAO,IAAI,CAAC;;;;;;;;;CAMhB,CAAC;AAEtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LINK_ERROR_CODES
|
|
3
|
+
} from "./chunk-POKWE5ZI.js";
|
|
4
|
+
|
|
5
|
+
// src/client.ts
|
|
6
|
+
var linkClient = () => ({
|
|
7
|
+
id: "link",
|
|
8
|
+
$InferServerPlugin: {},
|
|
9
|
+
pathMethods: {
|
|
10
|
+
"/link/connect": "POST",
|
|
11
|
+
"/link/disconnect": "POST"
|
|
12
|
+
},
|
|
13
|
+
$ERROR_CODES: LINK_ERROR_CODES
|
|
14
|
+
});
|
|
15
|
+
export {
|
|
16
|
+
LINK_ERROR_CODES,
|
|
17
|
+
linkClient
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client.ts"],"sourcesContent":["import type { BetterAuthClientPlugin } from 'better-auth/client';\nimport { LINK_ERROR_CODES } from './error-codes';\nimport type { link } from './index';\n\nexport const linkClient = () =>\n ({\n id: 'link',\n $InferServerPlugin: {} as ReturnType<typeof link>,\n pathMethods: {\n '/link/connect': 'POST',\n '/link/disconnect': 'POST',\n },\n $ERROR_CODES: LINK_ERROR_CODES,\n }) satisfies BetterAuthClientPlugin;\n\nexport { LINK_ERROR_CODES } from './error-codes';\n"],"mappings":";;;;;AAIO,IAAM,aAAa,OACvB;AAAA,EACC,IAAI;AAAA,EACJ,oBAAoB,CAAC;AAAA,EACrB,aAAa;AAAA,IACX,iBAAiB;AAAA,IACjB,oBAAoB;AAAA,EACtB;AAAA,EACA,cAAc;AAChB;","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-codes.d.ts","sourceRoot":"","sources":["../src/error-codes.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,gBAAgB;;;CAK3B,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
declare module '@better-auth/core' {
|
|
3
|
+
interface BetterAuthPluginRegistry<AuthOptions, Options> {
|
|
4
|
+
link: {
|
|
5
|
+
creator: typeof link;
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export interface LinkOptions {
|
|
10
|
+
clientId: string;
|
|
11
|
+
clientSecret: string;
|
|
12
|
+
publishableKey: string;
|
|
13
|
+
scopes?: string[];
|
|
14
|
+
redirectURI?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function link(options: LinkOptions): {
|
|
17
|
+
version: string;
|
|
18
|
+
init: (ctx: import("better-auth").AuthContext) => Promise<{
|
|
19
|
+
context: {
|
|
20
|
+
socialProviders: import("better-auth").OAuthProvider<object, Partial<import("better-auth").ProviderOptions<object>>>[];
|
|
21
|
+
};
|
|
22
|
+
}>;
|
|
23
|
+
options: import("better-auth/plugins/generic-oauth").GenericOAuthOptions<"link">;
|
|
24
|
+
id: "link";
|
|
25
|
+
endpoints: {
|
|
26
|
+
connectLink: import("better-auth").StrictEndpoint<"/link/connect", {
|
|
27
|
+
method: "POST";
|
|
28
|
+
requireHeaders: true;
|
|
29
|
+
body: z.ZodObject<{
|
|
30
|
+
callbackURL: z.ZodOptional<z.ZodString>;
|
|
31
|
+
requestSignUp: z.ZodOptional<z.ZodBoolean>;
|
|
32
|
+
scopes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
33
|
+
errorCallbackURL: z.ZodOptional<z.ZodString>;
|
|
34
|
+
disableRedirect: z.ZodOptional<z.ZodBoolean>;
|
|
35
|
+
loginHint: z.ZodOptional<z.ZodString>;
|
|
36
|
+
additionalParams: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
37
|
+
additionalData: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
|
|
38
|
+
}, z.core.$strip>;
|
|
39
|
+
}, {
|
|
40
|
+
url: string;
|
|
41
|
+
redirect: boolean;
|
|
42
|
+
}>;
|
|
43
|
+
disconnectLink: import("better-auth").StrictEndpoint<"/link/disconnect", {
|
|
44
|
+
method: "POST";
|
|
45
|
+
requireHeaders: true;
|
|
46
|
+
body: z.ZodObject<{
|
|
47
|
+
accountId: z.ZodString;
|
|
48
|
+
}, z.core.$strict>;
|
|
49
|
+
use: import("better-auth").Middleware<import("better-auth").MiddlewareOptions, (inputContext: import("better-auth").MiddlewareInputContext<import("better-auth").MiddlewareOptions>) => Promise<{
|
|
50
|
+
session: {
|
|
51
|
+
session: Record<string, any> & {
|
|
52
|
+
id: string;
|
|
53
|
+
createdAt: Date;
|
|
54
|
+
updatedAt: Date;
|
|
55
|
+
userId: string;
|
|
56
|
+
expiresAt: Date;
|
|
57
|
+
token: string;
|
|
58
|
+
ipAddress?: string | null | undefined;
|
|
59
|
+
userAgent?: string | null | undefined;
|
|
60
|
+
};
|
|
61
|
+
user: Record<string, any> & {
|
|
62
|
+
id: string;
|
|
63
|
+
createdAt: Date;
|
|
64
|
+
updatedAt: Date;
|
|
65
|
+
email: string;
|
|
66
|
+
emailVerified: boolean;
|
|
67
|
+
name: string;
|
|
68
|
+
image?: string | null | undefined;
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
}>>[];
|
|
72
|
+
}, {
|
|
73
|
+
status: boolean;
|
|
74
|
+
}>;
|
|
75
|
+
};
|
|
76
|
+
$ERROR_CODES: {
|
|
77
|
+
LINK_REFRESH_TOKEN_NOT_FOUND: import("better-auth").RawError<"LINK_REFRESH_TOKEN_NOT_FOUND">;
|
|
78
|
+
LINK_REVOCATION_FAILED: import("better-auth").RawError<"LINK_REVOCATION_FAILED">;
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
export { LINK_ERROR_CODES } from './error-codes';
|
|
82
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,QAAQ,mBAAmB,CAAC;IACjC,UAAU,wBAAwB,CAAC,WAAW,EAAE,OAAO;QACrD,IAAI,EAAE;YACJ,OAAO,EAAE,OAAO,IAAI,CAAC;SACtB,CAAC;KACH;CACF;AAMD,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,wBAAgB,IAAI,CAAC,OAAO,EAAE,WAAW;;;;;;;;;;QA+CnC,WAAW;;;;;;;;;;;;;;;;;QACX,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAInB;AAED,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LINK_ERROR_CODES
|
|
3
|
+
} from "./chunk-POKWE5ZI.js";
|
|
4
|
+
|
|
5
|
+
// src/index.ts
|
|
6
|
+
import { genericOAuth } from "better-auth/plugins/generic-oauth";
|
|
7
|
+
import { z as z2 } from "zod";
|
|
8
|
+
|
|
9
|
+
// src/routes.ts
|
|
10
|
+
import { BASE_ERROR_CODES } from "better-auth";
|
|
11
|
+
import {
|
|
12
|
+
APIError,
|
|
13
|
+
createAuthEndpoint,
|
|
14
|
+
freshSessionMiddleware,
|
|
15
|
+
linkSocialAccount,
|
|
16
|
+
sensitiveSessionMiddleware
|
|
17
|
+
} from "better-auth/api";
|
|
18
|
+
import { decryptOAuthToken } from "better-auth/oauth2";
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
var connectLink = () => createAuthEndpoint(
|
|
21
|
+
"/link/connect",
|
|
22
|
+
{
|
|
23
|
+
method: "POST",
|
|
24
|
+
requireHeaders: true,
|
|
25
|
+
body: linkSocialAccount.options.body.omit({
|
|
26
|
+
provider: true,
|
|
27
|
+
idToken: true
|
|
28
|
+
})
|
|
29
|
+
},
|
|
30
|
+
async (ctx) => {
|
|
31
|
+
const { headers, response } = await linkSocialAccount({
|
|
32
|
+
body: { ...ctx.body, provider: "link" },
|
|
33
|
+
context: ctx.context,
|
|
34
|
+
headers: ctx.headers,
|
|
35
|
+
...ctx.request ? { request: ctx.request } : {},
|
|
36
|
+
returnHeaders: true
|
|
37
|
+
});
|
|
38
|
+
for (const cookie of headers.getSetCookie()) {
|
|
39
|
+
ctx.responseHeaders.append("set-cookie", cookie);
|
|
40
|
+
}
|
|
41
|
+
headers.forEach((value, key) => {
|
|
42
|
+
if (key.toLowerCase() !== "set-cookie") {
|
|
43
|
+
ctx.responseHeaders.set(key, value);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return ctx.json(response);
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
var disconnectLink = (options) => createAuthEndpoint(
|
|
50
|
+
"/link/disconnect",
|
|
51
|
+
{
|
|
52
|
+
method: "POST",
|
|
53
|
+
requireHeaders: true,
|
|
54
|
+
body: z.strictObject({ accountId: z.string().min(1) }),
|
|
55
|
+
use: [sensitiveSessionMiddleware, freshSessionMiddleware]
|
|
56
|
+
},
|
|
57
|
+
async (ctx) => {
|
|
58
|
+
const accounts = await ctx.context.internalAdapter.findAccounts(
|
|
59
|
+
ctx.context.session.user.id
|
|
60
|
+
);
|
|
61
|
+
const account = accounts.find(
|
|
62
|
+
(candidate) => candidate.id === ctx.body.accountId && candidate.providerId === "link"
|
|
63
|
+
);
|
|
64
|
+
if (!account) {
|
|
65
|
+
throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.ACCOUNT_NOT_FOUND);
|
|
66
|
+
}
|
|
67
|
+
if (accounts.length === 1 && !ctx.context.options.account?.accountLinking?.allowUnlinkingAll) {
|
|
68
|
+
throw APIError.from(
|
|
69
|
+
"BAD_REQUEST",
|
|
70
|
+
BASE_ERROR_CODES.FAILED_TO_UNLINK_LAST_ACCOUNT
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
if (!account.refreshToken) {
|
|
74
|
+
throw APIError.from(
|
|
75
|
+
"BAD_REQUEST",
|
|
76
|
+
LINK_ERROR_CODES.LINK_REFRESH_TOKEN_NOT_FOUND
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
let response;
|
|
80
|
+
try {
|
|
81
|
+
const token = await decryptOAuthToken(
|
|
82
|
+
account.refreshToken,
|
|
83
|
+
ctx.context
|
|
84
|
+
);
|
|
85
|
+
response = await fetch("https://login.link.com/auth/revoke", {
|
|
86
|
+
method: "POST",
|
|
87
|
+
redirect: "error",
|
|
88
|
+
signal: AbortSignal.timeout(1e4),
|
|
89
|
+
headers: {
|
|
90
|
+
Authorization: `Bearer ${options.publishableKey}`,
|
|
91
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
92
|
+
},
|
|
93
|
+
body: new URLSearchParams({
|
|
94
|
+
client_id: options.clientId,
|
|
95
|
+
client_secret: options.clientSecret,
|
|
96
|
+
token,
|
|
97
|
+
token_type_hint: "refresh_token"
|
|
98
|
+
})
|
|
99
|
+
});
|
|
100
|
+
} catch {
|
|
101
|
+
throw APIError.from(
|
|
102
|
+
"BAD_GATEWAY",
|
|
103
|
+
LINK_ERROR_CODES.LINK_REVOCATION_FAILED
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
throw APIError.from(
|
|
108
|
+
"BAD_GATEWAY",
|
|
109
|
+
LINK_ERROR_CODES.LINK_REVOCATION_FAILED
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
await ctx.context.internalAdapter.deleteAccount(account.id);
|
|
113
|
+
return ctx.json({ status: true });
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// src/index.ts
|
|
118
|
+
var linkProfileSchema = z2.object({
|
|
119
|
+
id: z2.string().min(1)
|
|
120
|
+
});
|
|
121
|
+
function link(options) {
|
|
122
|
+
for (const field of ["clientId", "clientSecret", "publishableKey"]) {
|
|
123
|
+
if (!options[field]?.trim()) throw new Error(`Link ${field} is required.`);
|
|
124
|
+
}
|
|
125
|
+
const oauth = genericOAuth({
|
|
126
|
+
config: [
|
|
127
|
+
{
|
|
128
|
+
providerId: "link",
|
|
129
|
+
name: "Link",
|
|
130
|
+
clientId: options.clientId,
|
|
131
|
+
clientSecret: options.clientSecret,
|
|
132
|
+
redirectURI: options.redirectURI,
|
|
133
|
+
authorizationUrl: "https://login.link.com/auth",
|
|
134
|
+
tokenUrl: "https://login.link.com/auth/token",
|
|
135
|
+
userInfoUrl: "https://api.link.com/userinfo",
|
|
136
|
+
authorizationUrlParams: { key: options.publishableKey },
|
|
137
|
+
scopes: options.scopes ?? ["payment_methods.agentic", "userinfo:read"],
|
|
138
|
+
pkce: true,
|
|
139
|
+
accessTokenExpiresIn: 3600,
|
|
140
|
+
tokenEndpointAuth: {
|
|
141
|
+
method: "custom",
|
|
142
|
+
customizeRequest({ body, headers }) {
|
|
143
|
+
body.set("client_id", options.clientId);
|
|
144
|
+
body.set("client_secret", options.clientSecret);
|
|
145
|
+
headers.authorization = `Bearer ${options.publishableKey}`;
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
accountSubject: ({ profile }) => linkProfileSchema.parse(profile).id,
|
|
149
|
+
mapProfileToUser(profile) {
|
|
150
|
+
return {
|
|
151
|
+
name: profile.name || [profile.first_name, profile.last_name].filter((part) => typeof part === "string").join(" ") || "Link"
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
]
|
|
156
|
+
});
|
|
157
|
+
return {
|
|
158
|
+
...oauth,
|
|
159
|
+
id: "link",
|
|
160
|
+
endpoints: {
|
|
161
|
+
connectLink: connectLink(),
|
|
162
|
+
disconnectLink: disconnectLink(options)
|
|
163
|
+
},
|
|
164
|
+
$ERROR_CODES: LINK_ERROR_CODES
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
export {
|
|
168
|
+
LINK_ERROR_CODES,
|
|
169
|
+
link
|
|
170
|
+
};
|
|
171
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/routes.ts"],"sourcesContent":["import type { UserInfo } from '@stripe/link-sdk';\nimport type { BetterAuthPlugin } from 'better-auth';\nimport { genericOAuth } from 'better-auth/plugins/generic-oauth';\nimport { z } from 'zod';\nimport { LINK_ERROR_CODES } from './error-codes';\nimport { connectLink, disconnectLink } from './routes';\n\ndeclare module '@better-auth/core' {\n interface BetterAuthPluginRegistry<AuthOptions, Options> {\n link: {\n creator: typeof link;\n };\n }\n}\n\nconst linkProfileSchema = z.object({\n id: z.string().min(1),\n}) satisfies z.ZodType<Pick<UserInfo, 'id'>>;\n\nexport interface LinkOptions {\n clientId: string;\n clientSecret: string;\n publishableKey: string;\n scopes?: string[];\n redirectURI?: string;\n}\n\nexport function link(options: LinkOptions) {\n for (const field of ['clientId', 'clientSecret', 'publishableKey'] as const) {\n if (!options[field]?.trim()) throw new Error(`Link ${field} is required.`);\n }\n\n const oauth = genericOAuth({\n config: [\n {\n providerId: 'link',\n name: 'Link',\n clientId: options.clientId,\n clientSecret: options.clientSecret,\n redirectURI: options.redirectURI,\n authorizationUrl: 'https://login.link.com/auth',\n tokenUrl: 'https://login.link.com/auth/token',\n userInfoUrl: 'https://api.link.com/userinfo',\n authorizationUrlParams: { key: options.publishableKey },\n scopes: options.scopes ?? ['payment_methods.agentic', 'userinfo:read'],\n pkce: true,\n accessTokenExpiresIn: 3600,\n tokenEndpointAuth: {\n method: 'custom',\n customizeRequest({ body, headers }) {\n body.set('client_id', options.clientId);\n body.set('client_secret', options.clientSecret);\n headers.authorization = `Bearer ${options.publishableKey}`;\n },\n },\n accountSubject: ({ profile }) => linkProfileSchema.parse(profile).id,\n mapProfileToUser(profile) {\n return {\n name:\n profile.name ||\n [profile.first_name, profile.last_name]\n .filter((part) => typeof part === 'string')\n .join(' ') ||\n 'Link',\n };\n },\n },\n ],\n });\n\n return {\n ...oauth,\n id: 'link',\n endpoints: {\n connectLink: connectLink(),\n disconnectLink: disconnectLink(options),\n },\n $ERROR_CODES: LINK_ERROR_CODES,\n } satisfies BetterAuthPlugin;\n}\n\nexport { LINK_ERROR_CODES } from './error-codes';\n","import { BASE_ERROR_CODES } from 'better-auth';\nimport {\n APIError,\n createAuthEndpoint,\n freshSessionMiddleware,\n linkSocialAccount,\n sensitiveSessionMiddleware,\n} from 'better-auth/api';\nimport { decryptOAuthToken } from 'better-auth/oauth2';\nimport { z } from 'zod';\nimport { LINK_ERROR_CODES } from './error-codes';\nimport type { LinkOptions } from './index';\n\nexport const connectLink = () =>\n createAuthEndpoint(\n '/link/connect',\n {\n method: 'POST',\n requireHeaders: true,\n body: linkSocialAccount.options.body.omit({\n provider: true,\n idToken: true,\n }),\n },\n async (ctx) => {\n const { headers, response } = await linkSocialAccount({\n body: { ...ctx.body, provider: 'link' },\n context: ctx.context,\n headers: ctx.headers,\n ...(ctx.request ? { request: ctx.request } : {}),\n returnHeaders: true,\n });\n\n // Preserve OAuth state cookies and response headers from linkSocialAccount.\n for (const cookie of headers.getSetCookie()) {\n ctx.responseHeaders.append('set-cookie', cookie);\n }\n headers.forEach((value, key) => {\n if (key.toLowerCase() !== 'set-cookie') {\n ctx.responseHeaders.set(key, value);\n }\n });\n\n return ctx.json(response);\n },\n );\n\nexport const disconnectLink = (options: LinkOptions) =>\n createAuthEndpoint(\n '/link/disconnect',\n {\n method: 'POST',\n requireHeaders: true,\n body: z.strictObject({ accountId: z.string().min(1) }),\n use: [sensitiveSessionMiddleware, freshSessionMiddleware],\n },\n async (ctx) => {\n const accounts = await ctx.context.internalAdapter.findAccounts(\n ctx.context.session.user.id,\n );\n const account = accounts.find(\n (candidate) =>\n candidate.id === ctx.body.accountId &&\n candidate.providerId === 'link',\n );\n if (!account) {\n throw APIError.from('BAD_REQUEST', BASE_ERROR_CODES.ACCOUNT_NOT_FOUND);\n }\n if (\n accounts.length === 1 &&\n !ctx.context.options.account?.accountLinking?.allowUnlinkingAll\n ) {\n throw APIError.from(\n 'BAD_REQUEST',\n BASE_ERROR_CODES.FAILED_TO_UNLINK_LAST_ACCOUNT,\n );\n }\n if (!account.refreshToken) {\n throw APIError.from(\n 'BAD_REQUEST',\n LINK_ERROR_CODES.LINK_REFRESH_TOKEN_NOT_FOUND,\n );\n }\n\n let response: Response;\n try {\n const token = await decryptOAuthToken(\n account.refreshToken,\n ctx.context,\n );\n response = await fetch('https://login.link.com/auth/revoke', {\n method: 'POST',\n redirect: 'error',\n signal: AbortSignal.timeout(10_000),\n headers: {\n Authorization: `Bearer ${options.publishableKey}`,\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n client_id: options.clientId,\n client_secret: options.clientSecret,\n token,\n token_type_hint: 'refresh_token',\n }),\n });\n } catch {\n throw APIError.from(\n 'BAD_GATEWAY',\n LINK_ERROR_CODES.LINK_REVOCATION_FAILED,\n );\n }\n if (!response.ok) {\n throw APIError.from(\n 'BAD_GATEWAY',\n LINK_ERROR_CODES.LINK_REVOCATION_FAILED,\n );\n }\n\n await ctx.context.internalAdapter.deleteAccount(account.id);\n return ctx.json({ status: true });\n },\n );\n"],"mappings":";;;;;AAEA,SAAS,oBAAoB;AAC7B,SAAS,KAAAA,UAAS;;;ACHlB,SAAS,wBAAwB;AACjC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,yBAAyB;AAClC,SAAS,SAAS;AAIX,IAAM,cAAc,MACzB;AAAA,EACE;AAAA,EACA;AAAA,IACE,QAAQ;AAAA,IACR,gBAAgB;AAAA,IAChB,MAAM,kBAAkB,QAAQ,KAAK,KAAK;AAAA,MACxC,UAAU;AAAA,MACV,SAAS;AAAA,IACX,CAAC;AAAA,EACH;AAAA,EACA,OAAO,QAAQ;AACb,UAAM,EAAE,SAAS,SAAS,IAAI,MAAM,kBAAkB;AAAA,MACpD,MAAM,EAAE,GAAG,IAAI,MAAM,UAAU,OAAO;AAAA,MACtC,SAAS,IAAI;AAAA,MACb,SAAS,IAAI;AAAA,MACb,GAAI,IAAI,UAAU,EAAE,SAAS,IAAI,QAAQ,IAAI,CAAC;AAAA,MAC9C,eAAe;AAAA,IACjB,CAAC;AAGD,eAAW,UAAU,QAAQ,aAAa,GAAG;AAC3C,UAAI,gBAAgB,OAAO,cAAc,MAAM;AAAA,IACjD;AACA,YAAQ,QAAQ,CAAC,OAAO,QAAQ;AAC9B,UAAI,IAAI,YAAY,MAAM,cAAc;AACtC,YAAI,gBAAgB,IAAI,KAAK,KAAK;AAAA,MACpC;AAAA,IACF,CAAC;AAED,WAAO,IAAI,KAAK,QAAQ;AAAA,EAC1B;AACF;AAEK,IAAM,iBAAiB,CAAC,YAC7B;AAAA,EACE;AAAA,EACA;AAAA,IACE,QAAQ;AAAA,IACR,gBAAgB;AAAA,IAChB,MAAM,EAAE,aAAa,EAAE,WAAW,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC;AAAA,IACrD,KAAK,CAAC,4BAA4B,sBAAsB;AAAA,EAC1D;AAAA,EACA,OAAO,QAAQ;AACb,UAAM,WAAW,MAAM,IAAI,QAAQ,gBAAgB;AAAA,MACjD,IAAI,QAAQ,QAAQ,KAAK;AAAA,IAC3B;AACA,UAAM,UAAU,SAAS;AAAA,MACvB,CAAC,cACC,UAAU,OAAO,IAAI,KAAK,aAC1B,UAAU,eAAe;AAAA,IAC7B;AACA,QAAI,CAAC,SAAS;AACZ,YAAM,SAAS,KAAK,eAAe,iBAAiB,iBAAiB;AAAA,IACvE;AACA,QACE,SAAS,WAAW,KACpB,CAAC,IAAI,QAAQ,QAAQ,SAAS,gBAAgB,mBAC9C;AACA,YAAM,SAAS;AAAA,QACb;AAAA,QACA,iBAAiB;AAAA,MACnB;AAAA,IACF;AACA,QAAI,CAAC,QAAQ,cAAc;AACzB,YAAM,SAAS;AAAA,QACb;AAAA,QACA,iBAAiB;AAAA,MACnB;AAAA,IACF;AAEA,QAAI;AACJ,QAAI;AACF,YAAM,QAAQ,MAAM;AAAA,QAClB,QAAQ;AAAA,QACR,IAAI;AAAA,MACN;AACA,iBAAW,MAAM,MAAM,sCAAsC;AAAA,QAC3D,QAAQ;AAAA,QACR,UAAU;AAAA,QACV,QAAQ,YAAY,QAAQ,GAAM;AAAA,QAClC,SAAS;AAAA,UACP,eAAe,UAAU,QAAQ,cAAc;AAAA,UAC/C,gBAAgB;AAAA,QAClB;AAAA,QACA,MAAM,IAAI,gBAAgB;AAAA,UACxB,WAAW,QAAQ;AAAA,UACnB,eAAe,QAAQ;AAAA,UACvB;AAAA,UACA,iBAAiB;AAAA,QACnB,CAAC;AAAA,MACH,CAAC;AAAA,IACH,QAAQ;AACN,YAAM,SAAS;AAAA,QACb;AAAA,QACA,iBAAiB;AAAA,MACnB;AAAA,IACF;AACA,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,SAAS;AAAA,QACb;AAAA,QACA,iBAAiB;AAAA,MACnB;AAAA,IACF;AAEA,UAAM,IAAI,QAAQ,gBAAgB,cAAc,QAAQ,EAAE;AAC1D,WAAO,IAAI,KAAK,EAAE,QAAQ,KAAK,CAAC;AAAA,EAClC;AACF;;;AD1GF,IAAM,oBAAoBC,GAAE,OAAO;AAAA,EACjC,IAAIA,GAAE,OAAO,EAAE,IAAI,CAAC;AACtB,CAAC;AAUM,SAAS,KAAK,SAAsB;AACzC,aAAW,SAAS,CAAC,YAAY,gBAAgB,gBAAgB,GAAY;AAC3E,QAAI,CAAC,QAAQ,KAAK,GAAG,KAAK,EAAG,OAAM,IAAI,MAAM,QAAQ,KAAK,eAAe;AAAA,EAC3E;AAEA,QAAM,QAAQ,aAAa;AAAA,IACzB,QAAQ;AAAA,MACN;AAAA,QACE,YAAY;AAAA,QACZ,MAAM;AAAA,QACN,UAAU,QAAQ;AAAA,QAClB,cAAc,QAAQ;AAAA,QACtB,aAAa,QAAQ;AAAA,QACrB,kBAAkB;AAAA,QAClB,UAAU;AAAA,QACV,aAAa;AAAA,QACb,wBAAwB,EAAE,KAAK,QAAQ,eAAe;AAAA,QACtD,QAAQ,QAAQ,UAAU,CAAC,2BAA2B,eAAe;AAAA,QACrE,MAAM;AAAA,QACN,sBAAsB;AAAA,QACtB,mBAAmB;AAAA,UACjB,QAAQ;AAAA,UACR,iBAAiB,EAAE,MAAM,QAAQ,GAAG;AAClC,iBAAK,IAAI,aAAa,QAAQ,QAAQ;AACtC,iBAAK,IAAI,iBAAiB,QAAQ,YAAY;AAC9C,oBAAQ,gBAAgB,UAAU,QAAQ,cAAc;AAAA,UAC1D;AAAA,QACF;AAAA,QACA,gBAAgB,CAAC,EAAE,QAAQ,MAAM,kBAAkB,MAAM,OAAO,EAAE;AAAA,QAClE,iBAAiB,SAAS;AACxB,iBAAO;AAAA,YACL,MACE,QAAQ,QACR,CAAC,QAAQ,YAAY,QAAQ,SAAS,EACnC,OAAO,CAAC,SAAS,OAAO,SAAS,QAAQ,EACzC,KAAK,GAAG,KACX;AAAA,UACJ;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF,CAAC;AAED,SAAO;AAAA,IACL,GAAG;AAAA,IACH,IAAI;AAAA,IACJ,WAAW;AAAA,MACT,aAAa,YAAY;AAAA,MACzB,gBAAgB,eAAe,OAAO;AAAA,IACxC;AAAA,IACA,cAAc;AAAA,EAChB;AACF;","names":["z","z"]}
|
package/dist/routes.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { LinkOptions } from './index';
|
|
3
|
+
export declare const connectLink: () => import("better-auth").StrictEndpoint<"/link/connect", {
|
|
4
|
+
method: "POST";
|
|
5
|
+
requireHeaders: true;
|
|
6
|
+
body: z.ZodObject<{
|
|
7
|
+
callbackURL: z.ZodOptional<z.ZodString>;
|
|
8
|
+
requestSignUp: z.ZodOptional<z.ZodBoolean>;
|
|
9
|
+
scopes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
10
|
+
errorCallbackURL: z.ZodOptional<z.ZodString>;
|
|
11
|
+
disableRedirect: z.ZodOptional<z.ZodBoolean>;
|
|
12
|
+
loginHint: z.ZodOptional<z.ZodString>;
|
|
13
|
+
additionalParams: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
14
|
+
additionalData: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
|
|
15
|
+
}, z.core.$strip>;
|
|
16
|
+
}, {
|
|
17
|
+
url: string;
|
|
18
|
+
redirect: boolean;
|
|
19
|
+
}>;
|
|
20
|
+
export declare const disconnectLink: (options: LinkOptions) => import("better-auth").StrictEndpoint<"/link/disconnect", {
|
|
21
|
+
method: "POST";
|
|
22
|
+
requireHeaders: true;
|
|
23
|
+
body: z.ZodObject<{
|
|
24
|
+
accountId: z.ZodString;
|
|
25
|
+
}, z.core.$strict>;
|
|
26
|
+
use: import("better-auth").Middleware<import("better-auth").MiddlewareOptions, (inputContext: import("better-auth").MiddlewareInputContext<import("better-auth").MiddlewareOptions>) => Promise<{
|
|
27
|
+
session: {
|
|
28
|
+
session: Record<string, any> & {
|
|
29
|
+
id: string;
|
|
30
|
+
createdAt: Date;
|
|
31
|
+
updatedAt: Date;
|
|
32
|
+
userId: string;
|
|
33
|
+
expiresAt: Date;
|
|
34
|
+
token: string;
|
|
35
|
+
ipAddress?: string | null | undefined;
|
|
36
|
+
userAgent?: string | null | undefined;
|
|
37
|
+
};
|
|
38
|
+
user: Record<string, any> & {
|
|
39
|
+
id: string;
|
|
40
|
+
createdAt: Date;
|
|
41
|
+
updatedAt: Date;
|
|
42
|
+
email: string;
|
|
43
|
+
emailVerified: boolean;
|
|
44
|
+
name: string;
|
|
45
|
+
image?: string | null | undefined;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
}>>[];
|
|
49
|
+
}, {
|
|
50
|
+
status: boolean;
|
|
51
|
+
}>;
|
|
52
|
+
//# sourceMappingURL=routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;EAgCrB,CAAC;AAEJ,eAAO,MAAM,cAAc,YAAa,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA0EhD,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stripe/link-integrations-better-auth",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Sign in with Link and connect Link wallets using Better Auth",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./client": {
|
|
14
|
+
"types": "./dist/client.d.ts",
|
|
15
|
+
"import": "./dist/client.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"sideEffects": false,
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/stripe/link-cli.git",
|
|
28
|
+
"directory": "packages/integrations/better-auth"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/stripe/link-cli/tree/main/packages/integrations/better-auth#readme",
|
|
31
|
+
"bugs": "https://github.com/stripe/link-cli/issues",
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=22"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsup && tsc --noEmit false --emitDeclarationOnly",
|
|
40
|
+
"prepack": "pnpm run build",
|
|
41
|
+
"typecheck": "tsc && tsc -p tsconfig.test.json",
|
|
42
|
+
"test": "vitest run"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@better-auth/core": "^1.7.5",
|
|
46
|
+
"better-auth": "^1.7.5"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"zod": "^4.5.4"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@better-auth/core": "1.7.5",
|
|
53
|
+
"@stripe/link-sdk": "workspace:*",
|
|
54
|
+
"@stripe/link-typescript-config": "workspace:*",
|
|
55
|
+
"@types/node": "^26.4.1",
|
|
56
|
+
"better-auth": "1.7.5",
|
|
57
|
+
"tsup": "^8.5.1",
|
|
58
|
+
"typescript": "^7.0.2",
|
|
59
|
+
"vitest": "^5.0.0"
|
|
60
|
+
}
|
|
61
|
+
}
|