@better-auth/mcp 1.5.0-beta.8 → 1.7.0-beta.6
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/README.md +21 -0
- package/dist/client/adapters.d.mts +56 -0
- package/dist/client/adapters.mjs +126 -0
- package/dist/client/index.d.mts +52 -0
- package/dist/client/index.mjs +244 -0
- package/dist/index.d.mts +123 -1
- package/dist/index.mjs +199 -1024
- package/package.json +65 -26
package/dist/index.mjs
CHANGED
|
@@ -1,1039 +1,214 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
for (const [pattern, feature] of [
|
|
42
|
-
[/twoFactor\(/, "2fa"],
|
|
43
|
-
[/organization\(/, "organization"],
|
|
44
|
-
[/admin\(/, "admin"],
|
|
45
|
-
[/username\(/, "username"],
|
|
46
|
-
[/multiSession\(/, "multi-session"],
|
|
47
|
-
[/apiKey\(/, "api-key"],
|
|
48
|
-
[/bearer\(/, "bearer"],
|
|
49
|
-
[/jwt\(/, "jwt"],
|
|
50
|
-
[/magicLink\(/, "magic-link"],
|
|
51
|
-
[/phoneNumber\(/, "phone-number"],
|
|
52
|
-
[/passkey\(/, "passkey"],
|
|
53
|
-
[/anonymous\(/, "anonymous"],
|
|
54
|
-
[/captcha\(/, "captcha"]
|
|
55
|
-
]) if (pattern.test(serverConfig)) features.push(feature);
|
|
56
|
-
return features;
|
|
57
|
-
}
|
|
58
|
-
function computeFeatureDiff(existing, requested) {
|
|
59
|
-
return {
|
|
60
|
-
toAdd: requested.filter((f) => !existing.includes(f)),
|
|
61
|
-
existing: requested.filter((f) => existing.includes(f))
|
|
1
|
+
import { ResourceUriSchema, getIssuer, metadataResponse, oauthProvider, raiseResourceServerChallenge } from "@better-auth/oauth-provider";
|
|
2
|
+
import { DPOP_SIGNING_ALGORITHMS, createDpopReplayStore, requestToResourceInput, verifyAccessTokenRequest } from "better-auth/oauth2";
|
|
3
|
+
import { APIError } from "better-call";
|
|
4
|
+
//#region src/handler.ts
|
|
5
|
+
/**
|
|
6
|
+
* A request middleware handler that verifies an MCP access token and responds
|
|
7
|
+
* with an RFC 9728 `WWW-Authenticate` header for unauthenticated requests.
|
|
8
|
+
*
|
|
9
|
+
* @external
|
|
10
|
+
*/
|
|
11
|
+
const mcpHandler = (verifyOptions, handler, opts) => {
|
|
12
|
+
return async (req) => {
|
|
13
|
+
try {
|
|
14
|
+
return handler(req, await verifyAccessTokenRequest(requestToResourceInput(req), verifyOptions));
|
|
15
|
+
} catch (error) {
|
|
16
|
+
try {
|
|
17
|
+
raiseResourceServerChallenge(error, verifyOptions.verifyOptions.audience, {
|
|
18
|
+
...opts,
|
|
19
|
+
dpopSigningAlgorithms: verifyOptions.dpop?.signingAlgorithms
|
|
20
|
+
});
|
|
21
|
+
} catch (err) {
|
|
22
|
+
if (err instanceof APIError) {
|
|
23
|
+
const headers = new Headers(err.headers);
|
|
24
|
+
headers.set("Content-Type", "application/json");
|
|
25
|
+
return new Response(JSON.stringify({
|
|
26
|
+
jsonrpc: "2.0",
|
|
27
|
+
error: {
|
|
28
|
+
code: -32e3,
|
|
29
|
+
message: err.message
|
|
30
|
+
},
|
|
31
|
+
id: null
|
|
32
|
+
}), {
|
|
33
|
+
status: err.statusCode,
|
|
34
|
+
headers
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
throw new Error(String(err));
|
|
38
|
+
}
|
|
39
|
+
throw new Error(String(error));
|
|
40
|
+
}
|
|
62
41
|
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
//#endregion
|
|
66
|
-
//#region src/lib/templates/database.ts
|
|
67
|
-
const DATABASE_CONFIGS = {
|
|
68
|
-
postgres: {
|
|
69
|
-
provider: "postgresql",
|
|
70
|
-
envVarName: "DATABASE_URL",
|
|
71
|
-
connectionStringExample: "postgresql://user:password@localhost:5432/mydb"
|
|
72
|
-
},
|
|
73
|
-
mysql: {
|
|
74
|
-
provider: "mysql",
|
|
75
|
-
envVarName: "DATABASE_URL",
|
|
76
|
-
connectionStringExample: "mysql://user:password@localhost:3306/mydb"
|
|
77
|
-
},
|
|
78
|
-
sqlite: {
|
|
79
|
-
provider: "sqlite",
|
|
80
|
-
envVarName: "DATABASE_URL",
|
|
81
|
-
connectionStringExample: "file:./dev.db"
|
|
82
|
-
},
|
|
83
|
-
mongodb: {
|
|
84
|
-
provider: "mongodb",
|
|
85
|
-
envVarName: "DATABASE_URL",
|
|
86
|
-
connectionStringExample: "mongodb://localhost:27017/mydb"
|
|
87
|
-
}
|
|
88
42
|
};
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
schemaCommand: "npx @better-auth/cli generate --output src/lib/schema.ts",
|
|
106
|
-
pushCommand: "npx drizzle-kit push"
|
|
107
|
-
},
|
|
108
|
-
none: {
|
|
109
|
-
adapterImport: "",
|
|
110
|
-
adapterSetup: (dbProvider) => `{
|
|
111
|
-
provider: "${getShortProvider(dbProvider)}",
|
|
112
|
-
url: process.env.DATABASE_URL,
|
|
113
|
-
}`,
|
|
114
|
-
pushCommand: "npx @better-auth/cli migrate"
|
|
115
|
-
}
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/plugin.ts
|
|
45
|
+
const PROTECTED_RESOURCE_METADATA_PATH = "/.well-known/oauth-protected-resource";
|
|
46
|
+
const AUTHORIZATION_SERVER_ONLY_SCOPES = new Set([
|
|
47
|
+
"openid",
|
|
48
|
+
"profile",
|
|
49
|
+
"email",
|
|
50
|
+
"phone",
|
|
51
|
+
"address",
|
|
52
|
+
"offline_access"
|
|
53
|
+
]);
|
|
54
|
+
const resourceIdentifier = (resource) => typeof resource === "string" ? resource : resource.identifier;
|
|
55
|
+
const appendProtectedResource = (resources, resource) => {
|
|
56
|
+
const configuredResources = resources ?? [];
|
|
57
|
+
if (configuredResources.some((configuredResource) => resourceIdentifier(configuredResource) === resource)) return configuredResources;
|
|
58
|
+
return [...configuredResources, resource];
|
|
116
59
|
};
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
imports = ormConfig.adapterImport;
|
|
134
|
-
prismaInstance = "\nconst prisma = new PrismaClient();\n";
|
|
135
|
-
} else if (orm === "drizzle") imports = ormConfig.adapterImport;
|
|
136
|
-
const config = ormConfig.adapterSetup(dbConfig.provider);
|
|
137
|
-
return {
|
|
138
|
-
imports,
|
|
139
|
-
config,
|
|
140
|
-
prismaInstance
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
function getDatabaseEnvVar(database) {
|
|
144
|
-
const config = DATABASE_CONFIGS[database];
|
|
145
|
-
return {
|
|
146
|
-
name: config.envVarName,
|
|
147
|
-
description: `${database.charAt(0).toUpperCase() + database.slice(1)} connection string`,
|
|
148
|
-
required: true,
|
|
149
|
-
example: config.connectionStringExample
|
|
60
|
+
/**
|
|
61
|
+
* Build the RFC 9728 Protected Resource Metadata document. The MCP server is the
|
|
62
|
+
* resource server, and its authorization server is this same provider, so
|
|
63
|
+
* `authorization_servers` reuses the provider issuer. Resource metadata only
|
|
64
|
+
* advertises scopes that apply to the protected resource itself; OIDC identity
|
|
65
|
+
* scopes and refresh-token scopes stay authorization-server metadata.
|
|
66
|
+
*/
|
|
67
|
+
const buildResourceServerMetadata = (ctx, providerOptions, resource) => {
|
|
68
|
+
const resourceScopes = (providerOptions.advertisedMetadata?.scopes_supported ?? providerOptions.scopes ?? []).filter((scope) => !AUTHORIZATION_SERVER_ONLY_SCOPES.has(scope));
|
|
69
|
+
const configuredResource = providerOptions.resources?.find((configuredResource) => resourceIdentifier(configuredResource) === resource);
|
|
70
|
+
const dpopBoundAccessTokensRequired = typeof configuredResource === "object" && configuredResource.dpopBoundAccessTokensRequired === true;
|
|
71
|
+
const metadata = {
|
|
72
|
+
resource,
|
|
73
|
+
authorization_servers: [getIssuer(ctx, providerOptions)],
|
|
74
|
+
bearer_methods_supported: ["header"],
|
|
75
|
+
dpop_signing_alg_values_supported: [...providerOptions.dpop?.signingAlgorithms ?? DPOP_SIGNING_ALGORITHMS]
|
|
150
76
|
};
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const commands = [];
|
|
155
|
-
if (ormConfig.schemaCommand) commands.push({
|
|
156
|
-
command: ormConfig.schemaCommand,
|
|
157
|
-
description: `Generate ${orm === "prisma" ? "Prisma" : "Drizzle"} schema for auth tables`
|
|
158
|
-
});
|
|
159
|
-
commands.push({
|
|
160
|
-
command: ormConfig.pushCommand,
|
|
161
|
-
description: orm === "none" ? "Run database migrations for auth tables" : `Push ${orm === "prisma" ? "Prisma" : "Drizzle"} schema to database`,
|
|
162
|
-
when: "After setting DATABASE_URL"
|
|
163
|
-
});
|
|
164
|
-
return commands;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
//#endregion
|
|
168
|
-
//#region src/lib/templates/features.ts
|
|
169
|
-
function generateSocialProviderConfig(provider) {
|
|
170
|
-
const upperName = provider.toUpperCase();
|
|
171
|
-
return `${provider}: {
|
|
172
|
-
clientId: process.env.${upperName}_CLIENT_ID!,
|
|
173
|
-
clientSecret: process.env.${upperName}_CLIENT_SECRET!,
|
|
174
|
-
}`;
|
|
175
|
-
}
|
|
176
|
-
function getSocialProviderEnvVars(provider) {
|
|
177
|
-
const upperName = provider.toUpperCase();
|
|
178
|
-
const providerName = provider.charAt(0).toUpperCase() + provider.slice(1);
|
|
179
|
-
return [{
|
|
180
|
-
name: `${upperName}_CLIENT_ID`,
|
|
181
|
-
description: `${providerName} OAuth client ID`,
|
|
182
|
-
required: true,
|
|
183
|
-
howToGet: `Create OAuth app at ${providerName} developer console`
|
|
184
|
-
}, {
|
|
185
|
-
name: `${upperName}_CLIENT_SECRET`,
|
|
186
|
-
description: `${providerName} OAuth client secret`,
|
|
187
|
-
required: true,
|
|
188
|
-
howToGet: `Create OAuth app at ${providerName} developer console`
|
|
189
|
-
}];
|
|
190
|
-
}
|
|
191
|
-
const PLUGIN_CONFIGS = {
|
|
192
|
-
"2fa": {
|
|
193
|
-
serverImport: "import { twoFactor } from \"better-auth/plugins\";",
|
|
194
|
-
clientImport: "import { twoFactorClient } from \"better-auth/client/plugins\";",
|
|
195
|
-
serverPlugin: () => `twoFactor()`,
|
|
196
|
-
clientPlugin: () => `twoFactorClient()`
|
|
197
|
-
},
|
|
198
|
-
organization: {
|
|
199
|
-
serverImport: "import { organization } from \"better-auth/plugins\";",
|
|
200
|
-
clientImport: "import { organizationClient } from \"better-auth/client/plugins\";",
|
|
201
|
-
serverPlugin: () => `organization()`,
|
|
202
|
-
clientPlugin: () => `organizationClient()`
|
|
203
|
-
},
|
|
204
|
-
admin: {
|
|
205
|
-
serverImport: "import { admin } from \"better-auth/plugins\";",
|
|
206
|
-
clientImport: "import { adminClient } from \"better-auth/client/plugins\";",
|
|
207
|
-
serverPlugin: () => `admin()`,
|
|
208
|
-
clientPlugin: () => `adminClient()`
|
|
209
|
-
},
|
|
210
|
-
username: {
|
|
211
|
-
serverImport: "import { username } from \"better-auth/plugins\";",
|
|
212
|
-
clientImport: "import { usernameClient } from \"better-auth/client/plugins\";",
|
|
213
|
-
serverPlugin: () => `username()`,
|
|
214
|
-
clientPlugin: () => `usernameClient()`
|
|
215
|
-
},
|
|
216
|
-
"multi-session": {
|
|
217
|
-
serverImport: "import { multiSession } from \"better-auth/plugins\";",
|
|
218
|
-
clientImport: "import { multiSessionClient } from \"better-auth/client/plugins\";",
|
|
219
|
-
serverPlugin: () => `multiSession()`,
|
|
220
|
-
clientPlugin: () => `multiSessionClient()`
|
|
221
|
-
},
|
|
222
|
-
"api-key": {
|
|
223
|
-
serverImport: "import { apiKey } from \"better-auth/plugins\";",
|
|
224
|
-
clientImport: "import { apiKeyClient } from \"better-auth/client/plugins\";",
|
|
225
|
-
serverPlugin: () => `apiKey()`,
|
|
226
|
-
clientPlugin: () => `apiKeyClient()`
|
|
227
|
-
},
|
|
228
|
-
bearer: {
|
|
229
|
-
serverImport: "import { bearer } from \"better-auth/plugins\";",
|
|
230
|
-
serverPlugin: () => `bearer()`
|
|
231
|
-
},
|
|
232
|
-
jwt: {
|
|
233
|
-
serverImport: "import { jwt } from \"better-auth/plugins\";",
|
|
234
|
-
serverPlugin: () => `jwt()`
|
|
235
|
-
},
|
|
236
|
-
"magic-link": {
|
|
237
|
-
serverImport: "import { magicLink } from \"better-auth/plugins\";",
|
|
238
|
-
clientImport: "import { magicLinkClient } from \"better-auth/client/plugins\";",
|
|
239
|
-
serverPlugin: () => `magicLink({
|
|
240
|
-
sendMagicLink: async ({ email, url }) => {
|
|
241
|
-
// TODO: Send magic link email
|
|
242
|
-
console.log("Send magic link to", email, url);
|
|
243
|
-
},
|
|
244
|
-
})`,
|
|
245
|
-
clientPlugin: () => `magicLinkClient()`
|
|
246
|
-
},
|
|
247
|
-
"phone-number": {
|
|
248
|
-
serverImport: "import { phoneNumber } from \"better-auth/plugins\";",
|
|
249
|
-
clientImport: "import { phoneNumberClient } from \"better-auth/client/plugins\";",
|
|
250
|
-
serverPlugin: () => `phoneNumber({
|
|
251
|
-
sendOTP: async ({ phoneNumber, code }) => {
|
|
252
|
-
// TODO: Send OTP via SMS
|
|
253
|
-
console.log("Send OTP", code, "to", phoneNumber);
|
|
254
|
-
},
|
|
255
|
-
})`,
|
|
256
|
-
clientPlugin: () => `phoneNumberClient()`
|
|
257
|
-
},
|
|
258
|
-
passkey: {
|
|
259
|
-
serverImport: "import { passkey } from \"better-auth/plugins\";",
|
|
260
|
-
clientImport: "import { passkeyClient } from \"better-auth/client/plugins\";",
|
|
261
|
-
serverPlugin: () => `passkey()`,
|
|
262
|
-
clientPlugin: () => `passkeyClient()`
|
|
263
|
-
},
|
|
264
|
-
anonymous: {
|
|
265
|
-
serverImport: "import { anonymous } from \"better-auth/plugins\";",
|
|
266
|
-
clientImport: "import { anonymousClient } from \"better-auth/client/plugins\";",
|
|
267
|
-
serverPlugin: () => `anonymous()`,
|
|
268
|
-
clientPlugin: () => `anonymousClient()`
|
|
269
|
-
},
|
|
270
|
-
captcha: {
|
|
271
|
-
serverImport: "import { captcha } from \"better-auth/plugins\";",
|
|
272
|
-
serverPlugin: () => `captcha({
|
|
273
|
-
provider: "cloudflare-turnstile", // or "recaptcha" or "hcaptcha"
|
|
274
|
-
secretKey: process.env.CAPTCHA_SECRET_KEY!,
|
|
275
|
-
})`,
|
|
276
|
-
envVars: [{
|
|
277
|
-
name: "CAPTCHA_SECRET_KEY",
|
|
278
|
-
description: "Captcha provider secret key",
|
|
279
|
-
required: true
|
|
280
|
-
}]
|
|
281
|
-
}
|
|
77
|
+
if (dpopBoundAccessTokensRequired) metadata.dpop_bound_access_tokens_required = true;
|
|
78
|
+
if (resourceScopes.length) metadata.scopes_supported = [...resourceScopes];
|
|
79
|
+
return metadata;
|
|
282
80
|
};
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Model Context Protocol authorization server.
|
|
83
|
+
*
|
|
84
|
+
* `mcp()` is the OAuth 2.1 / OIDC provider ({@link oauthProvider}) configured for
|
|
85
|
+
* MCP: it enables dynamic client registration, binds issued tokens to the MCP
|
|
86
|
+
* `resource`, and, as the resource server, serves the RFC 9728 protected resource
|
|
87
|
+
* metadata so MCP clients discover and use it through standard OAuth discovery.
|
|
88
|
+
* Because it is the OAuth provider, it cannot be combined with a separate
|
|
89
|
+
* {@link oauthProvider}.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```ts
|
|
93
|
+
* import { betterAuth } from "better-auth";
|
|
94
|
+
* import { jwt } from "better-auth/plugins";
|
|
95
|
+
* import { mcp } from "@better-auth/mcp";
|
|
96
|
+
*
|
|
97
|
+
* export const auth = betterAuth({
|
|
98
|
+
* plugins: [
|
|
99
|
+
* jwt(),
|
|
100
|
+
* mcp({
|
|
101
|
+
* loginPage: "/login",
|
|
102
|
+
* consentPage: "/consent",
|
|
103
|
+
* resource: "https://api.example.com/mcp",
|
|
104
|
+
* }),
|
|
105
|
+
* ],
|
|
106
|
+
* });
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
const mcp = (options) => {
|
|
110
|
+
const { resource, ...oauthOptions } = options;
|
|
111
|
+
ResourceUriSchema.parse(resource);
|
|
112
|
+
const provider = oauthProvider({
|
|
113
|
+
allowDynamicClientRegistration: true,
|
|
114
|
+
allowUnauthenticatedClientRegistration: true,
|
|
115
|
+
...oauthOptions,
|
|
116
|
+
resources: appendProtectedResource(oauthOptions.resources, resource)
|
|
117
|
+
});
|
|
118
|
+
const serveProviderDiscovery = provider.onRequest;
|
|
293
119
|
return {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
120
|
+
...provider,
|
|
121
|
+
onRequest: async (request, ctx) => {
|
|
122
|
+
const handledByProvider = await serveProviderDiscovery?.(request, ctx);
|
|
123
|
+
if (handledByProvider) return handledByProvider;
|
|
124
|
+
const pathname = new URL(request.url).pathname;
|
|
125
|
+
const requestPath = ctx.options.advanced?.skipTrailingSlashes ? pathname.replace(/\/+$/, "") || "/" : pathname;
|
|
126
|
+
let resourcePath = "";
|
|
127
|
+
try {
|
|
128
|
+
resourcePath = new URL(resource).pathname.replace(/\/$/, "");
|
|
129
|
+
} catch {
|
|
130
|
+
resourcePath = "";
|
|
131
|
+
}
|
|
132
|
+
if (!new Set([PROTECTED_RESOURCE_METADATA_PATH, `${PROTECTED_RESOURCE_METADATA_PATH}${resourcePath}`]).has(requestPath)) return;
|
|
133
|
+
if (request.method !== "GET" && request.method !== "HEAD") return { response: new Response(null, {
|
|
134
|
+
status: 405,
|
|
135
|
+
headers: { Allow: "GET, HEAD" }
|
|
136
|
+
}) };
|
|
137
|
+
const response = metadataResponse(buildResourceServerMetadata({ context: ctx }, oauthOptions, resource));
|
|
138
|
+
if (request.method === "HEAD") return { response: new Response(null, {
|
|
139
|
+
status: response.status,
|
|
140
|
+
headers: response.headers
|
|
141
|
+
}) };
|
|
142
|
+
return { response };
|
|
306
143
|
}
|
|
307
|
-
}
|
|
308
|
-
return {
|
|
309
|
-
serverPlugins,
|
|
310
|
-
clientPlugins
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
function getPluginEnvVars(plugins) {
|
|
314
|
-
const envVars = [];
|
|
315
|
-
for (const plugin of plugins) {
|
|
316
|
-
const config = PLUGIN_CONFIGS[plugin];
|
|
317
|
-
if (config?.envVars) envVars.push(...config.envVars);
|
|
318
|
-
}
|
|
319
|
-
return envVars;
|
|
320
|
-
}
|
|
321
|
-
function categorizeFeatures(features) {
|
|
322
|
-
const socialProviders = [];
|
|
323
|
-
const plugins = [];
|
|
324
|
-
let hasEmailPassword = false;
|
|
325
|
-
for (const feature of features) if (feature === "email-password") hasEmailPassword = true;
|
|
326
|
-
else if (feature in PLUGIN_CONFIGS) plugins.push(feature);
|
|
327
|
-
else socialProviders.push(feature);
|
|
328
|
-
return {
|
|
329
|
-
socialProviders,
|
|
330
|
-
plugins,
|
|
331
|
-
hasEmailPassword
|
|
332
144
|
};
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
//#endregion
|
|
336
|
-
//#region src/lib/templates/frameworks.ts
|
|
337
|
-
const FRAMEWORK_CONFIGS = {
|
|
338
|
-
"next-app-router": {
|
|
339
|
-
name: "Next.js (App Router)",
|
|
340
|
-
defaultSrcDir: false,
|
|
341
|
-
defaultAuthPath: "lib/auth",
|
|
342
|
-
defaultApiPath: "app/api/auth/[...all]",
|
|
343
|
-
clientImport: "import { createAuthClient } from \"better-auth/react\";",
|
|
344
|
-
handlerImport: "import { toNextJsHandler } from \"better-auth/next-js\";",
|
|
345
|
-
handlerFunction: "toNextJsHandler",
|
|
346
|
-
apiRouteTemplate: (authPath) => `import { auth } from "@/${authPath}";
|
|
347
|
-
import { toNextJsHandler } from "better-auth/next-js";
|
|
348
|
-
|
|
349
|
-
export const { GET, POST } = toNextJsHandler(auth);`,
|
|
350
|
-
defaultPort: 3e3
|
|
351
|
-
},
|
|
352
|
-
"next-pages-router": {
|
|
353
|
-
name: "Next.js (Pages Router)",
|
|
354
|
-
defaultSrcDir: false,
|
|
355
|
-
defaultAuthPath: "lib/auth",
|
|
356
|
-
defaultApiPath: "pages/api/auth/[...all]",
|
|
357
|
-
clientImport: "import { createAuthClient } from \"better-auth/react\";",
|
|
358
|
-
handlerImport: "import { toNodeHandler } from \"better-auth/node\";",
|
|
359
|
-
handlerFunction: "toNodeHandler",
|
|
360
|
-
apiRouteTemplate: (authPath) => `import { toNodeHandler } from "better-auth/node";
|
|
361
|
-
import { auth } from "@/${authPath}";
|
|
362
|
-
|
|
363
|
-
export const config = { api: { bodyParser: false } };
|
|
364
|
-
export default toNodeHandler(auth);`,
|
|
365
|
-
defaultPort: 3e3
|
|
366
|
-
},
|
|
367
|
-
sveltekit: {
|
|
368
|
-
name: "SvelteKit",
|
|
369
|
-
defaultSrcDir: true,
|
|
370
|
-
defaultAuthPath: "lib/auth",
|
|
371
|
-
defaultApiPath: "routes/api/auth/[...all]",
|
|
372
|
-
clientImport: "import { createAuthClient } from \"better-auth/svelte\";",
|
|
373
|
-
handlerImport: "import { svelteKitHandler } from \"better-auth/svelte-kit\";",
|
|
374
|
-
handlerFunction: "svelteKitHandler",
|
|
375
|
-
apiRouteTemplate: () => "",
|
|
376
|
-
hooksTemplate: (authPath) => `import { auth } from "$${authPath}";
|
|
377
|
-
import { svelteKitHandler } from "better-auth/svelte-kit";
|
|
378
|
-
import type { Handle } from "@sveltejs/kit";
|
|
379
|
-
|
|
380
|
-
export const handle: Handle = async ({ event, resolve }) => {
|
|
381
|
-
return svelteKitHandler({ event, resolve, auth });
|
|
382
|
-
};`,
|
|
383
|
-
defaultPort: 5173
|
|
384
|
-
},
|
|
385
|
-
astro: {
|
|
386
|
-
name: "Astro",
|
|
387
|
-
defaultSrcDir: true,
|
|
388
|
-
defaultAuthPath: "lib/auth",
|
|
389
|
-
defaultApiPath: "pages/api/auth/[...all]",
|
|
390
|
-
clientImport: "import { createAuthClient } from \"better-auth/client\";",
|
|
391
|
-
handlerImport: "",
|
|
392
|
-
handlerFunction: "",
|
|
393
|
-
apiRouteTemplate: (authPath) => `import type { APIRoute } from "astro";
|
|
394
|
-
import { auth } from "@/${authPath}";
|
|
395
|
-
|
|
396
|
-
export const GET: APIRoute = async (ctx) => {
|
|
397
|
-
return auth.handler(ctx.request);
|
|
398
|
-
};
|
|
399
|
-
|
|
400
|
-
export const POST: APIRoute = async (ctx) => {
|
|
401
|
-
return auth.handler(ctx.request);
|
|
402
|
-
};`,
|
|
403
|
-
defaultPort: 4321
|
|
404
|
-
},
|
|
405
|
-
remix: {
|
|
406
|
-
name: "Remix",
|
|
407
|
-
defaultSrcDir: true,
|
|
408
|
-
defaultAuthPath: "lib/auth",
|
|
409
|
-
defaultApiPath: "routes/api.auth.$",
|
|
410
|
-
clientImport: "import { createAuthClient } from \"better-auth/react\";",
|
|
411
|
-
handlerImport: "",
|
|
412
|
-
handlerFunction: "",
|
|
413
|
-
apiRouteTemplate: (authPath) => `import { auth } from "~/${authPath}";
|
|
414
|
-
import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
|
|
415
|
-
|
|
416
|
-
export async function loader({ request }: LoaderFunctionArgs) {
|
|
417
|
-
return auth.handler(request);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
export async function action({ request }: ActionFunctionArgs) {
|
|
421
|
-
return auth.handler(request);
|
|
422
|
-
}`,
|
|
423
|
-
defaultPort: 3e3
|
|
424
|
-
},
|
|
425
|
-
nuxt: {
|
|
426
|
-
name: "Nuxt 3",
|
|
427
|
-
defaultSrcDir: false,
|
|
428
|
-
defaultAuthPath: "lib/auth",
|
|
429
|
-
defaultApiPath: "server/api/auth/[...all]",
|
|
430
|
-
clientImport: "import { createAuthClient } from \"better-auth/vue\";",
|
|
431
|
-
handlerImport: "",
|
|
432
|
-
handlerFunction: "",
|
|
433
|
-
apiRouteTemplate: (authPath) => `import { auth } from "~/${authPath}";
|
|
434
|
-
|
|
435
|
-
export default defineEventHandler((event) => {
|
|
436
|
-
return auth.handler(toWebRequest(event));
|
|
437
|
-
});`,
|
|
438
|
-
defaultPort: 3e3
|
|
439
|
-
},
|
|
440
|
-
"solid-start": {
|
|
441
|
-
name: "SolidStart",
|
|
442
|
-
defaultSrcDir: true,
|
|
443
|
-
defaultAuthPath: "lib/auth",
|
|
444
|
-
defaultApiPath: "routes/api/auth/[...all]",
|
|
445
|
-
clientImport: "import { createAuthClient } from \"better-auth/solid\";",
|
|
446
|
-
handlerImport: "",
|
|
447
|
-
handlerFunction: "",
|
|
448
|
-
apiRouteTemplate: (authPath) => `import { auth } from "~/${authPath}";
|
|
449
|
-
import type { APIEvent } from "@solidjs/start/server";
|
|
450
|
-
|
|
451
|
-
export async function GET(event: APIEvent) {
|
|
452
|
-
return auth.handler(event.request);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
export async function POST(event: APIEvent) {
|
|
456
|
-
return auth.handler(event.request);
|
|
457
|
-
}`,
|
|
458
|
-
defaultPort: 3e3
|
|
459
|
-
},
|
|
460
|
-
hono: {
|
|
461
|
-
name: "Hono",
|
|
462
|
-
defaultSrcDir: true,
|
|
463
|
-
defaultAuthPath: "lib/auth",
|
|
464
|
-
defaultApiPath: "",
|
|
465
|
-
clientImport: "import { createAuthClient } from \"better-auth/client\";",
|
|
466
|
-
handlerImport: "",
|
|
467
|
-
handlerFunction: "",
|
|
468
|
-
apiRouteTemplate: (authPath) => `import { Hono } from "hono";
|
|
469
|
-
import { auth } from "./${authPath}";
|
|
470
|
-
|
|
471
|
-
const app = new Hono();
|
|
472
|
-
|
|
473
|
-
app.on(["POST", "GET"], "/api/auth/**", (c) => {
|
|
474
|
-
return auth.handler(c.req.raw);
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
export default app;`,
|
|
478
|
-
defaultPort: 3e3
|
|
479
|
-
},
|
|
480
|
-
express: {
|
|
481
|
-
name: "Express.js",
|
|
482
|
-
defaultSrcDir: true,
|
|
483
|
-
defaultAuthPath: "lib/auth",
|
|
484
|
-
defaultApiPath: "",
|
|
485
|
-
clientImport: "import { createAuthClient } from \"better-auth/client\";",
|
|
486
|
-
handlerImport: "import { toNodeHandler } from \"better-auth/node\";",
|
|
487
|
-
handlerFunction: "toNodeHandler",
|
|
488
|
-
apiRouteTemplate: (authPath) => `import express from "express";
|
|
489
|
-
import { toNodeHandler } from "better-auth/node";
|
|
490
|
-
import { auth } from "./${authPath}";
|
|
491
|
-
|
|
492
|
-
const app = express();
|
|
493
|
-
|
|
494
|
-
app.all("/api/auth/*", toNodeHandler(auth));
|
|
495
|
-
|
|
496
|
-
app.listen(3000, () => {
|
|
497
|
-
console.log("Server running on port 3000");
|
|
498
|
-
});`,
|
|
499
|
-
defaultPort: 3e3
|
|
500
|
-
},
|
|
501
|
-
fastify: {
|
|
502
|
-
name: "Fastify",
|
|
503
|
-
defaultSrcDir: true,
|
|
504
|
-
defaultAuthPath: "lib/auth",
|
|
505
|
-
defaultApiPath: "",
|
|
506
|
-
clientImport: "import { createAuthClient } from \"better-auth/client\";",
|
|
507
|
-
handlerImport: "import { toNodeHandler } from \"better-auth/node\";",
|
|
508
|
-
handlerFunction: "toNodeHandler",
|
|
509
|
-
apiRouteTemplate: (authPath) => `import Fastify from "fastify";
|
|
510
|
-
import { toNodeHandler } from "better-auth/node";
|
|
511
|
-
import { auth } from "./${authPath}";
|
|
512
|
-
|
|
513
|
-
const fastify = Fastify();
|
|
514
|
-
|
|
515
|
-
fastify.all("/api/auth/*", async (request, reply) => {
|
|
516
|
-
const handler = toNodeHandler(auth);
|
|
517
|
-
return handler(request.raw, reply.raw);
|
|
518
|
-
});
|
|
519
|
-
|
|
520
|
-
fastify.listen({ port: 3000 }, (err) => {
|
|
521
|
-
if (err) throw err;
|
|
522
|
-
console.log("Server running on port 3000");
|
|
523
|
-
});`,
|
|
524
|
-
defaultPort: 3e3
|
|
525
|
-
},
|
|
526
|
-
elysia: {
|
|
527
|
-
name: "Elysia (Bun)",
|
|
528
|
-
defaultSrcDir: true,
|
|
529
|
-
defaultAuthPath: "lib/auth",
|
|
530
|
-
defaultApiPath: "",
|
|
531
|
-
clientImport: "import { createAuthClient } from \"better-auth/client\";",
|
|
532
|
-
handlerImport: "",
|
|
533
|
-
handlerFunction: "",
|
|
534
|
-
apiRouteTemplate: (authPath) => `import { Elysia } from "elysia";
|
|
535
|
-
import { auth } from "./${authPath}";
|
|
536
|
-
|
|
537
|
-
const app = new Elysia()
|
|
538
|
-
.all("/api/auth/*", ({ request }) => auth.handler(request))
|
|
539
|
-
.listen(3000);
|
|
540
|
-
|
|
541
|
-
console.log("Server running on port 3000");`,
|
|
542
|
-
defaultPort: 3e3
|
|
543
|
-
},
|
|
544
|
-
"tanstack-start": {
|
|
545
|
-
name: "TanStack Start",
|
|
546
|
-
defaultSrcDir: true,
|
|
547
|
-
defaultAuthPath: "lib/auth",
|
|
548
|
-
defaultApiPath: "routes/api/auth.$",
|
|
549
|
-
clientImport: "import { createAuthClient } from \"better-auth/react\";",
|
|
550
|
-
handlerImport: "",
|
|
551
|
-
handlerFunction: "",
|
|
552
|
-
apiRouteTemplate: (authPath) => `import { auth } from "~/${authPath}";
|
|
553
|
-
import { createAPIFileRoute } from "@tanstack/start/api";
|
|
554
|
-
|
|
555
|
-
export const Route = createAPIFileRoute("/api/auth/$")({
|
|
556
|
-
GET: ({ request }) => auth.handler(request),
|
|
557
|
-
POST: ({ request }) => auth.handler(request),
|
|
558
|
-
});`,
|
|
559
|
-
defaultPort: 3e3
|
|
560
|
-
},
|
|
561
|
-
expo: {
|
|
562
|
-
name: "Expo (React Native)",
|
|
563
|
-
defaultSrcDir: true,
|
|
564
|
-
defaultAuthPath: "lib/auth",
|
|
565
|
-
defaultApiPath: "",
|
|
566
|
-
clientImport: "import { createAuthClient } from \"@better-auth/expo\";",
|
|
567
|
-
handlerImport: "",
|
|
568
|
-
handlerFunction: "",
|
|
569
|
-
apiRouteTemplate: () => "",
|
|
570
|
-
defaultPort: 8081
|
|
571
|
-
}
|
|
572
145
|
};
|
|
573
|
-
function getDefaultAuthPath(framework, srcDir) {
|
|
574
|
-
const basePath = FRAMEWORK_CONFIGS[framework].defaultAuthPath;
|
|
575
|
-
return srcDir ? `src/${basePath}` : basePath;
|
|
576
|
-
}
|
|
577
|
-
function getDefaultApiPath(framework, srcDir) {
|
|
578
|
-
const basePath = FRAMEWORK_CONFIGS[framework].defaultApiPath;
|
|
579
|
-
if (!basePath) return "";
|
|
580
|
-
return srcDir ? `src/${basePath}` : basePath;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
146
|
//#endregion
|
|
584
|
-
//#region src/
|
|
585
|
-
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
mode = "update";
|
|
599
|
-
detected = parseExistingSetup(input.existingSetup);
|
|
600
|
-
featuresToGenerate = computeFeatureDiff(detected.features, features).toAdd;
|
|
601
|
-
if (featuresToGenerate.length === 0) return {
|
|
602
|
-
mode: "update",
|
|
603
|
-
files: [],
|
|
604
|
-
envVars: [],
|
|
605
|
-
commands: [],
|
|
606
|
-
detected,
|
|
607
|
-
nextSteps: ["All requested features are already configured"],
|
|
608
|
-
docs: []
|
|
609
|
-
};
|
|
610
|
-
}
|
|
611
|
-
const { socialProviders, plugins, hasEmailPassword } = categorizeFeatures(featuresToGenerate);
|
|
612
|
-
const files = [];
|
|
613
|
-
const envVars = [];
|
|
614
|
-
const commands = [];
|
|
615
|
-
const warnings = [];
|
|
616
|
-
envVars.push({
|
|
617
|
-
name: "BETTER_AUTH_SECRET",
|
|
618
|
-
description: "Secret key for signing tokens",
|
|
619
|
-
required: true,
|
|
620
|
-
howToGet: "Run: npx @better-auth/cli secret"
|
|
621
|
-
});
|
|
622
|
-
envVars.push(getDatabaseEnvVar(database));
|
|
623
|
-
for (const provider of socialProviders) envVars.push(...getSocialProviderEnvVars(provider));
|
|
624
|
-
envVars.push(...getPluginEnvVars(plugins));
|
|
625
|
-
if (mode === "create") {
|
|
626
|
-
const authFile = generateAuthFile({
|
|
627
|
-
framework,
|
|
628
|
-
database,
|
|
629
|
-
orm,
|
|
630
|
-
socialProviders,
|
|
631
|
-
plugins,
|
|
632
|
-
hasEmailPassword,
|
|
633
|
-
typescript
|
|
634
|
-
});
|
|
635
|
-
files.push({
|
|
636
|
-
path: `${authPath}.${typescript ? "ts" : "js"}`,
|
|
637
|
-
description: "Better Auth server configuration",
|
|
638
|
-
action: "create",
|
|
639
|
-
content: authFile
|
|
640
|
-
});
|
|
641
|
-
const clientFile = generateClientFile({
|
|
642
|
-
framework,
|
|
643
|
-
plugins,
|
|
644
|
-
typescript
|
|
645
|
-
});
|
|
646
|
-
files.push({
|
|
647
|
-
path: `${authPath}-client.${typescript ? "ts" : "js"}`,
|
|
648
|
-
description: "Better Auth client configuration",
|
|
649
|
-
action: "create",
|
|
650
|
-
content: clientFile
|
|
651
|
-
});
|
|
652
|
-
if (apiPath) {
|
|
653
|
-
const apiFile = generateApiRouteFile(framework, authPath);
|
|
654
|
-
if (apiFile) files.push({
|
|
655
|
-
path: `${apiPath}/route.${typescript ? "ts" : "js"}`,
|
|
656
|
-
description: "API route handler for auth endpoints",
|
|
657
|
-
action: "create",
|
|
658
|
-
content: apiFile
|
|
659
|
-
});
|
|
660
|
-
}
|
|
661
|
-
const frameworkConfig = FRAMEWORK_CONFIGS[framework];
|
|
662
|
-
if (frameworkConfig.hooksTemplate) files.push({
|
|
663
|
-
path: srcDir ? "src/hooks.server.ts" : "hooks.server.ts",
|
|
664
|
-
description: "SvelteKit hooks for auth handling",
|
|
665
|
-
action: "create",
|
|
666
|
-
content: frameworkConfig.hooksTemplate(authPath)
|
|
667
|
-
});
|
|
668
|
-
} else if (socialProviders.length > 0 || plugins.length > 0 || hasEmailPassword) {
|
|
669
|
-
const changes = generateUpdateChanges({
|
|
670
|
-
socialProviders,
|
|
671
|
-
plugins,
|
|
672
|
-
hasEmailPassword
|
|
673
|
-
});
|
|
674
|
-
files.push({
|
|
675
|
-
path: `${authPath}.${typescript ? "ts" : "js"}`,
|
|
676
|
-
description: "Better Auth server configuration",
|
|
677
|
-
action: "update",
|
|
678
|
-
changes: changes.serverChanges
|
|
679
|
-
});
|
|
680
|
-
if (plugins.length > 0) files.push({
|
|
681
|
-
path: `${authPath}-client.${typescript ? "ts" : "js"}`,
|
|
682
|
-
description: "Better Auth client configuration",
|
|
683
|
-
action: "update",
|
|
684
|
-
changes: changes.clientChanges
|
|
685
|
-
});
|
|
686
|
-
}
|
|
687
|
-
commands.push({
|
|
688
|
-
command: "pnpm add better-auth",
|
|
689
|
-
description: "Install Better Auth",
|
|
690
|
-
when: "If not already installed"
|
|
691
|
-
});
|
|
692
|
-
commands.push(...getDatabaseCommands(orm));
|
|
693
|
-
const nextSteps = generateNextSteps(mode, socialProviders, plugins);
|
|
694
|
-
const docs = generateDocLinks(features);
|
|
695
|
-
return {
|
|
696
|
-
mode,
|
|
697
|
-
files,
|
|
698
|
-
envVars,
|
|
699
|
-
commands,
|
|
700
|
-
detected,
|
|
701
|
-
nextSteps,
|
|
702
|
-
warnings: warnings.length > 0 ? warnings : void 0,
|
|
703
|
-
docs
|
|
704
|
-
};
|
|
705
|
-
}
|
|
706
|
-
function generateAuthFile(options) {
|
|
707
|
-
const { database, orm, socialProviders, plugins, hasEmailPassword } = options;
|
|
708
|
-
const imports = ["import { betterAuth } from \"better-auth\";"];
|
|
709
|
-
const { imports: dbImports, config: dbConfig, prismaInstance } = generateDatabaseConfig(database, orm);
|
|
710
|
-
if (dbImports) imports.push(dbImports);
|
|
711
|
-
const { serverImports } = generatePluginImports(plugins);
|
|
712
|
-
imports.push(...serverImports);
|
|
713
|
-
const socialProvidersConfig = socialProviders.map((p) => ` ${generateSocialProviderConfig(p)}`).join(",\n");
|
|
714
|
-
const { serverPlugins } = generatePluginSetup(plugins);
|
|
715
|
-
let configBody = ` database: ${dbConfig},`;
|
|
716
|
-
if (hasEmailPassword) configBody += `
|
|
717
|
-
emailAndPassword: {
|
|
718
|
-
enabled: true,
|
|
719
|
-
},`;
|
|
720
|
-
if (socialProviders.length > 0) configBody += `
|
|
721
|
-
socialProviders: {
|
|
722
|
-
${socialProvidersConfig}
|
|
723
|
-
},`;
|
|
724
|
-
if (serverPlugins.length > 0) configBody += `
|
|
725
|
-
plugins: [
|
|
726
|
-
${serverPlugins.join(",\n ")}
|
|
727
|
-
],`;
|
|
728
|
-
return `${imports.join("\n")}
|
|
729
|
-
${prismaInstance || ""}
|
|
730
|
-
export const auth = betterAuth({
|
|
731
|
-
${configBody}
|
|
732
|
-
});
|
|
733
|
-
`;
|
|
734
|
-
}
|
|
735
|
-
function generateClientFile(options) {
|
|
736
|
-
const { framework, plugins } = options;
|
|
737
|
-
const imports = [FRAMEWORK_CONFIGS[framework].clientImport];
|
|
738
|
-
const { clientImports } = generatePluginImports(plugins);
|
|
739
|
-
imports.push(...clientImports);
|
|
740
|
-
const { clientPlugins } = generatePluginSetup(plugins);
|
|
741
|
-
let configBody = "";
|
|
742
|
-
if (clientPlugins.length > 0) configBody = `{
|
|
743
|
-
plugins: [
|
|
744
|
-
${clientPlugins.join(",\n ")}
|
|
745
|
-
],
|
|
746
|
-
}`;
|
|
747
|
-
else configBody = "{}";
|
|
748
|
-
return `${imports.join("\n")}
|
|
749
|
-
|
|
750
|
-
export const authClient = createAuthClient(${configBody});
|
|
751
|
-
`;
|
|
752
|
-
}
|
|
753
|
-
function generateApiRouteFile(framework, authPath) {
|
|
754
|
-
const frameworkConfig = FRAMEWORK_CONFIGS[framework];
|
|
755
|
-
if (!frameworkConfig.apiRouteTemplate) return null;
|
|
756
|
-
return frameworkConfig.apiRouteTemplate(authPath) || null;
|
|
757
|
-
}
|
|
758
|
-
function generateUpdateChanges(options) {
|
|
759
|
-
const { socialProviders, plugins, hasEmailPassword } = options;
|
|
760
|
-
const serverChanges = [];
|
|
761
|
-
const clientChanges = [];
|
|
762
|
-
if (hasEmailPassword) serverChanges.push({
|
|
763
|
-
type: "add_to_config",
|
|
764
|
-
content: `emailAndPassword: {
|
|
765
|
-
enabled: true,
|
|
766
|
-
}`,
|
|
767
|
-
description: "Enable email and password authentication"
|
|
768
|
-
});
|
|
769
|
-
const { serverImports, clientImports } = generatePluginImports(plugins);
|
|
770
|
-
for (const imp of serverImports) serverChanges.push({
|
|
771
|
-
type: "add_import",
|
|
772
|
-
content: imp,
|
|
773
|
-
description: "Add plugin import"
|
|
774
|
-
});
|
|
775
|
-
for (const provider of socialProviders) serverChanges.push({
|
|
776
|
-
type: "add_to_config",
|
|
777
|
-
content: generateSocialProviderConfig(provider),
|
|
778
|
-
description: `Add ${provider} social provider`
|
|
779
|
-
});
|
|
780
|
-
const { serverPlugins, clientPlugins } = generatePluginSetup(plugins);
|
|
781
|
-
for (const plugin of serverPlugins) serverChanges.push({
|
|
782
|
-
type: "add_plugin",
|
|
783
|
-
content: plugin,
|
|
784
|
-
description: "Add server plugin"
|
|
785
|
-
});
|
|
786
|
-
for (const imp of clientImports) clientChanges.push({
|
|
787
|
-
type: "add_import",
|
|
788
|
-
content: imp,
|
|
789
|
-
description: "Add client plugin import"
|
|
790
|
-
});
|
|
791
|
-
for (const plugin of clientPlugins) clientChanges.push({
|
|
792
|
-
type: "add_plugin",
|
|
793
|
-
content: plugin,
|
|
794
|
-
description: "Add client plugin"
|
|
795
|
-
});
|
|
796
|
-
return {
|
|
797
|
-
serverChanges,
|
|
798
|
-
clientChanges
|
|
799
|
-
};
|
|
800
|
-
}
|
|
801
|
-
function generateNextSteps(mode, socialProviders, plugins) {
|
|
802
|
-
const steps = [];
|
|
803
|
-
if (mode === "create") {
|
|
804
|
-
steps.push("Set environment variables in .env file");
|
|
805
|
-
steps.push("Run database migrations");
|
|
806
|
-
}
|
|
807
|
-
if (socialProviders.length > 0) {
|
|
808
|
-
steps.push(`Configure OAuth apps for: ${socialProviders.join(", ")}`);
|
|
809
|
-
steps.push("Add callback URLs to OAuth provider dashboards");
|
|
810
|
-
}
|
|
811
|
-
if (plugins.includes("magic-link")) steps.push("Configure email provider for magic links");
|
|
812
|
-
if (plugins.includes("phone-number")) steps.push("Configure SMS provider for OTP");
|
|
813
|
-
if (plugins.includes("captcha")) steps.push("Set up captcha provider (Cloudflare Turnstile, reCAPTCHA, or hCaptcha)");
|
|
814
|
-
steps.push("Start your development server and test auth flow");
|
|
815
|
-
return steps;
|
|
816
|
-
}
|
|
817
|
-
function generateDocLinks(features) {
|
|
818
|
-
const docs = [{
|
|
819
|
-
title: "Better Auth Documentation",
|
|
820
|
-
url: "https://www.better-auth.com/docs"
|
|
821
|
-
}, {
|
|
822
|
-
title: "Getting Started",
|
|
823
|
-
url: "https://www.better-auth.com/docs/getting-started"
|
|
824
|
-
}];
|
|
825
|
-
if (features.some((f) => [
|
|
826
|
-
"google",
|
|
827
|
-
"github",
|
|
828
|
-
"apple",
|
|
829
|
-
"discord"
|
|
830
|
-
].includes(f))) docs.push({
|
|
831
|
-
title: "Social Sign-On",
|
|
832
|
-
url: "https://www.better-auth.com/docs/authentication/social-sign-on"
|
|
833
|
-
});
|
|
834
|
-
if (features.includes("2fa")) docs.push({
|
|
835
|
-
title: "Two-Factor Authentication",
|
|
836
|
-
url: "https://www.better-auth.com/docs/plugins/two-factor"
|
|
837
|
-
});
|
|
838
|
-
if (features.includes("organization")) docs.push({
|
|
839
|
-
title: "Organizations",
|
|
840
|
-
url: "https://www.better-auth.com/docs/plugins/organization"
|
|
147
|
+
//#region src/require-mcp-auth.ts
|
|
148
|
+
const unauthorized = (error) => {
|
|
149
|
+
const headers = new Headers(error.headers);
|
|
150
|
+
headers.set("Content-Type", "application/json");
|
|
151
|
+
return new Response(JSON.stringify({
|
|
152
|
+
jsonrpc: "2.0",
|
|
153
|
+
error: {
|
|
154
|
+
code: -32e3,
|
|
155
|
+
message: error.message
|
|
156
|
+
},
|
|
157
|
+
id: null
|
|
158
|
+
}), {
|
|
159
|
+
status: error.statusCode,
|
|
160
|
+
headers
|
|
841
161
|
});
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
"fastify",
|
|
865
|
-
"elysia",
|
|
866
|
-
"tanstack-start",
|
|
867
|
-
"expo"
|
|
868
|
-
]);
|
|
869
|
-
const DatabaseEnum = z.enum([
|
|
870
|
-
"postgres",
|
|
871
|
-
"mysql",
|
|
872
|
-
"sqlite",
|
|
873
|
-
"mongodb"
|
|
874
|
-
]);
|
|
875
|
-
const ORMEnum = z.enum([
|
|
876
|
-
"prisma",
|
|
877
|
-
"drizzle",
|
|
878
|
-
"none"
|
|
879
|
-
]);
|
|
880
|
-
const FeatureEnum = z.enum([
|
|
881
|
-
"email-password",
|
|
882
|
-
"magic-link",
|
|
883
|
-
"phone-number",
|
|
884
|
-
"passkey",
|
|
885
|
-
"anonymous",
|
|
886
|
-
"google",
|
|
887
|
-
"github",
|
|
888
|
-
"apple",
|
|
889
|
-
"discord",
|
|
890
|
-
"twitter",
|
|
891
|
-
"facebook",
|
|
892
|
-
"microsoft",
|
|
893
|
-
"linkedin",
|
|
894
|
-
"2fa",
|
|
895
|
-
"captcha",
|
|
896
|
-
"organization",
|
|
897
|
-
"admin",
|
|
898
|
-
"username",
|
|
899
|
-
"multi-session",
|
|
900
|
-
"api-key",
|
|
901
|
-
"bearer",
|
|
902
|
-
"jwt"
|
|
903
|
-
]);
|
|
904
|
-
const ExistingSetupSchema = z.object({
|
|
905
|
-
authConfig: z.string().optional().describe("Contents of existing auth.ts file"),
|
|
906
|
-
authClientConfig: z.string().optional().describe("Contents of existing auth-client.ts file"),
|
|
907
|
-
envVars: z.array(z.string()).optional().describe("List of existing environment variable names")
|
|
908
|
-
});
|
|
909
|
-
|
|
910
|
-
//#endregion
|
|
911
|
-
//#region src/tools/setup-auth.ts
|
|
912
|
-
function registerSetupAuth(server$1) {
|
|
913
|
-
server$1.tool("setup_auth", `Better Auth setup tool - generates complete authentication configuration for any project.
|
|
914
|
-
|
|
915
|
-
CRITICAL: Do NOT attempt to generate Better Auth configuration without calling this tool first.
|
|
916
|
-
Better Auth has specific APIs that differ from NextAuth/Auth.js. Hallucinating config options will cause errors.
|
|
917
|
-
For advanced options not handled by this tool (uuid, cookieCache, etc.), search the Better Auth documentation.
|
|
918
|
-
|
|
919
|
-
IMPORTANT: This tool is for the Better Auth framework (npm: better-auth).
|
|
920
|
-
NOT for NextAuth, Auth.js, Clerk, Auth0, Supabase Auth, or Firebase Auth.
|
|
921
|
-
|
|
922
|
-
ALWAYS USE THIS TOOL when user mentions ANY of these:
|
|
923
|
-
|
|
924
|
-
Core triggers (any spelling/variation):
|
|
925
|
-
- "better auth", "betterauth", "Better Auth", "better-auth"
|
|
926
|
-
- "Set up Better Auth", "configure Better Auth", "integrate Better Auth", "use Better Auth"
|
|
927
|
-
- "set up auth", "setup auth", "configure auth", "add auth", "implement auth", "create auth"
|
|
928
|
-
- "add authentication", "add login", "add signup", "create login", "build auth", "build authentication"
|
|
929
|
-
- "I need auth", "I need authentication", "help me with auth", "help me add login"
|
|
930
|
-
- "add user login", "user authentication", "make login work", "authentication for my app"
|
|
931
|
-
|
|
932
|
-
Framework-specific triggers:
|
|
933
|
-
- "auth for Next.js", "Next.js authentication", "add auth to my Next app", "Next.js app router auth"
|
|
934
|
-
- "auth for SvelteKit", "SvelteKit authentication", "Svelte auth"
|
|
935
|
-
- "auth for Remix", "auth for Nuxt", "auth for Astro", "auth for Solid", "authentication for Remix/Nuxt/Astro"
|
|
936
|
-
- "Express auth", "Hono auth", "Fastify auth", "Elysia auth", "TanStack Start auth"
|
|
937
|
-
- "React Native auth", "Expo auth", "mobile auth"
|
|
938
|
-
|
|
939
|
-
Database triggers:
|
|
940
|
-
- "auth with PostgreSQL", "auth with MySQL", "auth with SQLite", "auth with MongoDB"
|
|
941
|
-
- "Set up auth with PostgreSQL/MySQL/SQLite", "configure database for auth"
|
|
942
|
-
- "auth with Prisma", "auth with Drizzle", "configure auth with Prisma/Drizzle"
|
|
943
|
-
|
|
944
|
-
Social/OAuth triggers:
|
|
945
|
-
- "add Google login", "add GitHub login", "add Apple login", "add Discord login"
|
|
946
|
-
- "add Twitter login", "add Facebook login", "add Microsoft login", "add LinkedIn login"
|
|
947
|
-
- "Add Google/GitHub/Apple/Discord/Twitter/Facebook/Microsoft/LinkedIn login"
|
|
948
|
-
- "add social login", "add OAuth", "add OAuth providers", "add SSO", "social authentication"
|
|
949
|
-
|
|
950
|
-
Security feature triggers:
|
|
951
|
-
- "Set up 2FA", "add 2FA", "add two-factor", "add two-factor authentication"
|
|
952
|
-
- "enable MFA", "add TOTP", "authenticator app"
|
|
953
|
-
- "add passkeys", "passwordless", "passwordless authentication", "add magic links", "email login", "email verification"
|
|
954
|
-
|
|
955
|
-
Organization/team triggers:
|
|
956
|
-
- "add organizations", "Add organization support", "enable multi-tenancy", "multi-tenancy"
|
|
957
|
-
- "team management", "workspaces"
|
|
958
|
-
|
|
959
|
-
API/token triggers:
|
|
960
|
-
- "add API keys", "Add API key authentication", "bearer tokens", "JWT auth", "JWT authentication"
|
|
961
|
-
|
|
962
|
-
Admin/user triggers:
|
|
963
|
-
- "add admin panel", "admin auth", "admin authentication"
|
|
964
|
-
- "Add user management", "user management", "handle sessions", "session management"
|
|
965
|
-
- "Add username login", "username login", "phone auth", "phone number auth", "anonymous auth"
|
|
966
|
-
|
|
967
|
-
Advanced config triggers (search docs for correct syntax):
|
|
968
|
-
- "uuid", "UUIDs", "UUID IDs", "custom ID generation", "generateId"
|
|
969
|
-
- "cookie cache", "cookieCache", "session caching", "cache sessions"
|
|
970
|
-
- "stateless sessions", "stateless auth", "no database sessions"
|
|
971
|
-
- "secondary storage", "Redis sessions", "session storage"
|
|
972
|
-
|
|
973
|
-
OUTPUT: Returns all files, environment variables, and terminal commands needed.
|
|
974
|
-
One tool call = complete auth setup ready to copy-paste.`, {
|
|
975
|
-
framework: FrameworkEnum.describe("The web framework being used. Detect from package.json or user's message. Examples: 'next-app-router' for Next.js 13+, 'next-pages-router' for Next.js pages, 'sveltekit', 'remix', 'nuxt', 'astro', 'solid-start', 'hono', 'express', 'fastify', 'elysia', 'tanstack-start', 'expo'"),
|
|
976
|
-
database: DatabaseEnum.describe("The database type. Detect from user's message or project config. Options: 'postgres' (PostgreSQL/Supabase/Neon), 'mysql' (MySQL/PlanetScale), 'sqlite' (SQLite/Turso/LibSQL), 'mongodb'"),
|
|
977
|
-
orm: ORMEnum.optional().default("none").describe("ORM being used - affects adapter imports. Options: 'prisma', 'drizzle', 'none'. Detect from package.json or user's message."),
|
|
978
|
-
features: z.array(FeatureEnum).optional().default(["email-password"]).describe("Auth features to enable. Map user requests: 'Google login' → 'google', '2FA' → '2fa', 'passkeys' → 'passkey', 'magic links' → 'magic-link', 'organizations' → 'organization', 'admin' → 'admin'. Default: ['email-password']"),
|
|
979
|
-
typescript: z.boolean().optional().default(true).describe("Generate TypeScript (.ts) or JavaScript (.js). Default: true (TypeScript)"),
|
|
980
|
-
srcDir: z.boolean().optional().describe("Whether project uses src/ directory structure. Check if src/ folder exists. Default: false. Provide true for frameworks that store app files in src/."),
|
|
981
|
-
authPath: z.string().optional().describe("Where to create auth.ts file. Auto-detected based on framework. Override only if user specifies custom path."),
|
|
982
|
-
apiPath: z.string().optional().describe("API route path for auth handler. Auto-detected based on framework. Override only if user specifies custom path."),
|
|
983
|
-
existingSetup: ExistingSetupSchema.optional().describe("For INCREMENTAL updates only. Pass existing auth.ts and auth-client.ts contents to add new features without overwriting. Read these files first if they exist.")
|
|
984
|
-
}, async (input) => {
|
|
162
|
+
};
|
|
163
|
+
/**
|
|
164
|
+
* Protects an MCP server route handler. Verifies the bearer access token
|
|
165
|
+
* against the authorization server's JWKS (checking signature, issuer,
|
|
166
|
+
* audience, and expiry) and forwards the verified JWT payload to the handler.
|
|
167
|
+
* Unauthenticated requests receive a JSON-RPC 401 with the RFC 9728
|
|
168
|
+
* `WWW-Authenticate` header so MCP clients can start the authorization flow.
|
|
169
|
+
*
|
|
170
|
+
* For a resource server that runs separately from the authorization server, or
|
|
171
|
+
* a server using a dynamic `baseURL`, use {@link mcpHandler} with explicit
|
|
172
|
+
* verification options instead.
|
|
173
|
+
*
|
|
174
|
+
* @external
|
|
175
|
+
*/
|
|
176
|
+
const requireMcpAuth = (auth, handler, opts) => {
|
|
177
|
+
if (opts?.resource !== void 0) ResourceUriSchema.parse(opts.resource);
|
|
178
|
+
return async (req) => {
|
|
179
|
+
const { baseURL, internalAdapter } = await auth.$context;
|
|
180
|
+
if (!baseURL) throw new Error("requireMcpAuth requires a resolvable base URL. For dynamic base URLs use `mcpHandler` with explicit verify options.");
|
|
181
|
+
const issuer = opts?.issuer ?? baseURL;
|
|
182
|
+
const resource = opts?.resource ?? baseURL;
|
|
183
|
+
const jwksUrl = opts?.jwksUrl ?? `${baseURL}/jwks`;
|
|
985
184
|
try {
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
content: [{
|
|
999
|
-
type: "text",
|
|
1000
|
-
text: JSON.stringify(result, null, 2)
|
|
1001
|
-
}],
|
|
1002
|
-
isError: true
|
|
1003
|
-
};
|
|
1004
|
-
return { content: [{
|
|
1005
|
-
type: "text",
|
|
1006
|
-
text: JSON.stringify(result, null, 2)
|
|
1007
|
-
}] };
|
|
185
|
+
return handler(req, await verifyAccessTokenRequest(requestToResourceInput(req), {
|
|
186
|
+
verifyOptions: {
|
|
187
|
+
issuer,
|
|
188
|
+
audience: resource
|
|
189
|
+
},
|
|
190
|
+
jwksUrl,
|
|
191
|
+
dpop: {
|
|
192
|
+
proofMaxAgeSeconds: opts?.dpop?.proofMaxAgeSeconds,
|
|
193
|
+
signingAlgorithms: opts?.dpop?.signingAlgorithms,
|
|
194
|
+
replayStore: opts?.dpop?.replayStore ?? createDpopReplayStore(internalAdapter)
|
|
195
|
+
}
|
|
196
|
+
}));
|
|
1008
197
|
} catch (error) {
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
}
|
|
198
|
+
try {
|
|
199
|
+
raiseResourceServerChallenge(error, resource, {
|
|
200
|
+
scope: opts?.scope,
|
|
201
|
+
resourceMetadataMappings: opts?.resourceMetadataMappings,
|
|
202
|
+
dpopSigningAlgorithms: opts?.dpop?.signingAlgorithms
|
|
203
|
+
});
|
|
204
|
+
} catch (challengeError) {
|
|
205
|
+
if (challengeError instanceof APIError) return unauthorized(challengeError);
|
|
206
|
+
if (challengeError instanceof Error) throw challengeError;
|
|
207
|
+
throw new Error(String(challengeError));
|
|
208
|
+
}
|
|
209
|
+
throw new Error(String(error));
|
|
1020
210
|
}
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
//#endregion
|
|
1025
|
-
//#region src/index.ts
|
|
1026
|
-
const server = new McpServer({
|
|
1027
|
-
name: "better-auth",
|
|
1028
|
-
description: "Better Auth MCP server for AI-powered auth setup and diagnostics",
|
|
1029
|
-
version: "0.0.1"
|
|
1030
|
-
});
|
|
1031
|
-
registerSetupAuth(server);
|
|
1032
|
-
async function main() {
|
|
1033
|
-
const transport = new StdioServerTransport();
|
|
1034
|
-
await server.connect(transport);
|
|
1035
|
-
}
|
|
1036
|
-
main().catch(console.error);
|
|
1037
|
-
|
|
211
|
+
};
|
|
212
|
+
};
|
|
1038
213
|
//#endregion
|
|
1039
|
-
export {
|
|
214
|
+
export { mcp, mcpHandler, requireMcpAuth };
|