diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..014fd0d --- /dev/null +++ b/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "zendegi-mcp": { + "type": "http", + "url": "http://localhost:3001/api/mcp" + } + } +} \ No newline at end of file diff --git a/apps/web/package.json b/apps/web/package.json index 9cb99f6..7c3dd53 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -11,8 +11,10 @@ }, "dependencies": { "@base-ui/react": "^1.2.0", + "@better-auth/oauth-provider": "^1.4.19", "@fontsource-variable/inter": "^5.2.8", "@fontsource/instrument-serif": "^5.2.8", + "@modelcontextprotocol/sdk": "^1.27.0", "@tailwindcss/vite": "^4.1.8", "@tanstack/react-form": "^1.23.5", "@tanstack/react-query": "^5.80.6", diff --git a/apps/web/src/lib/auth-client.ts b/apps/web/src/lib/auth-client.ts index 3b12ab6..8c646d1 100644 --- a/apps/web/src/lib/auth-client.ts +++ b/apps/web/src/lib/auth-client.ts @@ -1,6 +1,7 @@ import { createAuthClient } from "better-auth/react"; import { anonymousClient } from "better-auth/client/plugins"; +import { oauthProviderClient } from "@better-auth/oauth-provider/client"; export const authClient = createAuthClient({ - plugins: [anonymousClient()], + plugins: [anonymousClient(), oauthProviderClient()], }); diff --git a/apps/web/src/lib/server-client.ts b/apps/web/src/lib/server-client.ts new file mode 100644 index 0000000..b01e99d --- /dev/null +++ b/apps/web/src/lib/server-client.ts @@ -0,0 +1,7 @@ +import { auth } from "@zendegi/auth"; +import { createAuthClient } from "better-auth/client"; +import { oauthProviderResourceClient } from "@better-auth/oauth-provider/resource-client"; + +export const serverClient = createAuthClient({ + plugins: [oauthProviderResourceClient(auth)], +}); diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts index ec5a963..748efa2 100644 --- a/apps/web/src/routeTree.gen.ts +++ b/apps/web/src/routeTree.gen.ts @@ -14,9 +14,15 @@ import { Route as TimelineRouteImport } from './routes/timeline' import { Route as LoginRouteImport } from './routes/login' import { Route as DemoRouteImport } from './routes/demo' import { Route as DashboardRouteImport } from './routes/dashboard' +import { Route as ConsentRouteImport } from './routes/consent' import { Route as IndexRouteImport } from './routes/index' import { Route as TimelineTimelineIdRouteImport } from './routes/timeline.$timelineId' +import { Route as DotwellKnownOpenidConfigurationRouteImport } from './routes/[.well-known]/openid-configuration' +import { Route as DotwellKnownOauthProtectedResourceRouteImport } from './routes/[.well-known]/oauth-protected-resource' +import { Route as DotwellKnownOauthAuthorizationServerRouteImport } from './routes/[.well-known]/oauth-authorization-server' +import { Route as ApiMcpSplatRouteImport } from './routes/api/mcp/$' import { Route as ApiAuthSplatRouteImport } from './routes/api/auth/$' +import { Route as DotwellKnownOauthAuthorizationServerApiAuthRouteImport } from './routes/[.well-known]/oauth-authorization-server.api.auth' const TimelinesRoute = TimelinesRouteImport.update({ id: '/timelines', @@ -43,6 +49,11 @@ const DashboardRoute = DashboardRouteImport.update({ path: '/dashboard', getParentRoute: () => rootRouteImport, } as any) +const ConsentRoute = ConsentRouteImport.update({ + id: '/consent', + path: '/consent', + getParentRoute: () => rootRouteImport, +} as any) const IndexRoute = IndexRouteImport.update({ id: '/', path: '/', @@ -53,84 +64,154 @@ const TimelineTimelineIdRoute = TimelineTimelineIdRouteImport.update({ path: '/$timelineId', getParentRoute: () => TimelineRoute, } as any) +const DotwellKnownOpenidConfigurationRoute = + DotwellKnownOpenidConfigurationRouteImport.update({ + id: '/.well-known/openid-configuration', + path: '/.well-known/openid-configuration', + getParentRoute: () => rootRouteImport, + } as any) +const DotwellKnownOauthProtectedResourceRoute = + DotwellKnownOauthProtectedResourceRouteImport.update({ + id: '/.well-known/oauth-protected-resource', + path: '/.well-known/oauth-protected-resource', + getParentRoute: () => rootRouteImport, + } as any) +const DotwellKnownOauthAuthorizationServerRoute = + DotwellKnownOauthAuthorizationServerRouteImport.update({ + id: '/.well-known/oauth-authorization-server', + path: '/.well-known/oauth-authorization-server', + getParentRoute: () => rootRouteImport, + } as any) +const ApiMcpSplatRoute = ApiMcpSplatRouteImport.update({ + id: '/api/mcp/$', + path: '/api/mcp/$', + getParentRoute: () => rootRouteImport, +} as any) const ApiAuthSplatRoute = ApiAuthSplatRouteImport.update({ id: '/api/auth/$', path: '/api/auth/$', getParentRoute: () => rootRouteImport, } as any) +const DotwellKnownOauthAuthorizationServerApiAuthRoute = + DotwellKnownOauthAuthorizationServerApiAuthRouteImport.update({ + id: '/api/auth', + path: '/api/auth', + getParentRoute: () => DotwellKnownOauthAuthorizationServerRoute, + } as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute + '/consent': typeof ConsentRoute '/dashboard': typeof DashboardRoute '/demo': typeof DemoRoute '/login': typeof LoginRoute '/timeline': typeof TimelineRouteWithChildren '/timelines': typeof TimelinesRoute + '/.well-known/oauth-authorization-server': typeof DotwellKnownOauthAuthorizationServerRouteWithChildren + '/.well-known/oauth-protected-resource': typeof DotwellKnownOauthProtectedResourceRoute + '/.well-known/openid-configuration': typeof DotwellKnownOpenidConfigurationRoute '/timeline/$timelineId': typeof TimelineTimelineIdRoute '/api/auth/$': typeof ApiAuthSplatRoute + '/api/mcp/$': typeof ApiMcpSplatRoute + '/.well-known/oauth-authorization-server/api/auth': typeof DotwellKnownOauthAuthorizationServerApiAuthRoute } export interface FileRoutesByTo { '/': typeof IndexRoute + '/consent': typeof ConsentRoute '/dashboard': typeof DashboardRoute '/demo': typeof DemoRoute '/login': typeof LoginRoute '/timeline': typeof TimelineRouteWithChildren '/timelines': typeof TimelinesRoute + '/.well-known/oauth-authorization-server': typeof DotwellKnownOauthAuthorizationServerRouteWithChildren + '/.well-known/oauth-protected-resource': typeof DotwellKnownOauthProtectedResourceRoute + '/.well-known/openid-configuration': typeof DotwellKnownOpenidConfigurationRoute '/timeline/$timelineId': typeof TimelineTimelineIdRoute '/api/auth/$': typeof ApiAuthSplatRoute + '/api/mcp/$': typeof ApiMcpSplatRoute + '/.well-known/oauth-authorization-server/api/auth': typeof DotwellKnownOauthAuthorizationServerApiAuthRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute + '/consent': typeof ConsentRoute '/dashboard': typeof DashboardRoute '/demo': typeof DemoRoute '/login': typeof LoginRoute '/timeline': typeof TimelineRouteWithChildren '/timelines': typeof TimelinesRoute + '/.well-known/oauth-authorization-server': typeof DotwellKnownOauthAuthorizationServerRouteWithChildren + '/.well-known/oauth-protected-resource': typeof DotwellKnownOauthProtectedResourceRoute + '/.well-known/openid-configuration': typeof DotwellKnownOpenidConfigurationRoute '/timeline/$timelineId': typeof TimelineTimelineIdRoute '/api/auth/$': typeof ApiAuthSplatRoute + '/api/mcp/$': typeof ApiMcpSplatRoute + '/.well-known/oauth-authorization-server/api/auth': typeof DotwellKnownOauthAuthorizationServerApiAuthRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath fullPaths: | '/' + | '/consent' | '/dashboard' | '/demo' | '/login' | '/timeline' | '/timelines' + | '/.well-known/oauth-authorization-server' + | '/.well-known/oauth-protected-resource' + | '/.well-known/openid-configuration' | '/timeline/$timelineId' | '/api/auth/$' + | '/api/mcp/$' + | '/.well-known/oauth-authorization-server/api/auth' fileRoutesByTo: FileRoutesByTo to: | '/' + | '/consent' | '/dashboard' | '/demo' | '/login' | '/timeline' | '/timelines' + | '/.well-known/oauth-authorization-server' + | '/.well-known/oauth-protected-resource' + | '/.well-known/openid-configuration' | '/timeline/$timelineId' | '/api/auth/$' + | '/api/mcp/$' + | '/.well-known/oauth-authorization-server/api/auth' id: | '__root__' | '/' + | '/consent' | '/dashboard' | '/demo' | '/login' | '/timeline' | '/timelines' + | '/.well-known/oauth-authorization-server' + | '/.well-known/oauth-protected-resource' + | '/.well-known/openid-configuration' | '/timeline/$timelineId' | '/api/auth/$' + | '/api/mcp/$' + | '/.well-known/oauth-authorization-server/api/auth' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute + ConsentRoute: typeof ConsentRoute DashboardRoute: typeof DashboardRoute DemoRoute: typeof DemoRoute LoginRoute: typeof LoginRoute TimelineRoute: typeof TimelineRouteWithChildren TimelinesRoute: typeof TimelinesRoute + DotwellKnownOauthAuthorizationServerRoute: typeof DotwellKnownOauthAuthorizationServerRouteWithChildren + DotwellKnownOauthProtectedResourceRoute: typeof DotwellKnownOauthProtectedResourceRoute + DotwellKnownOpenidConfigurationRoute: typeof DotwellKnownOpenidConfigurationRoute ApiAuthSplatRoute: typeof ApiAuthSplatRoute + ApiMcpSplatRoute: typeof ApiMcpSplatRoute } declare module '@tanstack/react-router' { @@ -170,6 +251,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DashboardRouteImport parentRoute: typeof rootRouteImport } + '/consent': { + id: '/consent' + path: '/consent' + fullPath: '/consent' + preLoaderRoute: typeof ConsentRouteImport + parentRoute: typeof rootRouteImport + } '/': { id: '/' path: '/' @@ -184,6 +272,34 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof TimelineTimelineIdRouteImport parentRoute: typeof TimelineRoute } + '/.well-known/openid-configuration': { + id: '/.well-known/openid-configuration' + path: '/.well-known/openid-configuration' + fullPath: '/.well-known/openid-configuration' + preLoaderRoute: typeof DotwellKnownOpenidConfigurationRouteImport + parentRoute: typeof rootRouteImport + } + '/.well-known/oauth-protected-resource': { + id: '/.well-known/oauth-protected-resource' + path: '/.well-known/oauth-protected-resource' + fullPath: '/.well-known/oauth-protected-resource' + preLoaderRoute: typeof DotwellKnownOauthProtectedResourceRouteImport + parentRoute: typeof rootRouteImport + } + '/.well-known/oauth-authorization-server': { + id: '/.well-known/oauth-authorization-server' + path: '/.well-known/oauth-authorization-server' + fullPath: '/.well-known/oauth-authorization-server' + preLoaderRoute: typeof DotwellKnownOauthAuthorizationServerRouteImport + parentRoute: typeof rootRouteImport + } + '/api/mcp/$': { + id: '/api/mcp/$' + path: '/api/mcp/$' + fullPath: '/api/mcp/$' + preLoaderRoute: typeof ApiMcpSplatRouteImport + parentRoute: typeof rootRouteImport + } '/api/auth/$': { id: '/api/auth/$' path: '/api/auth/$' @@ -191,6 +307,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ApiAuthSplatRouteImport parentRoute: typeof rootRouteImport } + '/.well-known/oauth-authorization-server/api/auth': { + id: '/.well-known/oauth-authorization-server/api/auth' + path: '/api/auth' + fullPath: '/.well-known/oauth-authorization-server/api/auth' + preLoaderRoute: typeof DotwellKnownOauthAuthorizationServerApiAuthRouteImport + parentRoute: typeof DotwellKnownOauthAuthorizationServerRoute + } } } @@ -206,14 +329,36 @@ const TimelineRouteWithChildren = TimelineRoute._addFileChildren( TimelineRouteChildren, ) +interface DotwellKnownOauthAuthorizationServerRouteChildren { + DotwellKnownOauthAuthorizationServerApiAuthRoute: typeof DotwellKnownOauthAuthorizationServerApiAuthRoute +} + +const DotwellKnownOauthAuthorizationServerRouteChildren: DotwellKnownOauthAuthorizationServerRouteChildren = + { + DotwellKnownOauthAuthorizationServerApiAuthRoute: + DotwellKnownOauthAuthorizationServerApiAuthRoute, + } + +const DotwellKnownOauthAuthorizationServerRouteWithChildren = + DotwellKnownOauthAuthorizationServerRoute._addFileChildren( + DotwellKnownOauthAuthorizationServerRouteChildren, + ) + const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, + ConsentRoute: ConsentRoute, DashboardRoute: DashboardRoute, DemoRoute: DemoRoute, LoginRoute: LoginRoute, TimelineRoute: TimelineRouteWithChildren, TimelinesRoute: TimelinesRoute, + DotwellKnownOauthAuthorizationServerRoute: + DotwellKnownOauthAuthorizationServerRouteWithChildren, + DotwellKnownOauthProtectedResourceRoute: + DotwellKnownOauthProtectedResourceRoute, + DotwellKnownOpenidConfigurationRoute: DotwellKnownOpenidConfigurationRoute, ApiAuthSplatRoute: ApiAuthSplatRoute, + ApiMcpSplatRoute: ApiMcpSplatRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/apps/web/src/routes/[.well-known]/oauth-authorization-server.api.auth.ts b/apps/web/src/routes/[.well-known]/oauth-authorization-server.api.auth.ts new file mode 100644 index 0000000..537597f --- /dev/null +++ b/apps/web/src/routes/[.well-known]/oauth-authorization-server.api.auth.ts @@ -0,0 +1,15 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { auth } from "@zendegi/auth"; +import { oauthProviderAuthServerMetadata } from "@better-auth/oauth-provider"; + +const handler = oauthProviderAuthServerMetadata(auth); + +export const Route = createFileRoute( + "/.well-known/oauth-authorization-server/api/auth" +)({ + server: { + handlers: { + GET: ({ request }) => handler(request), + }, + }, +}); diff --git a/apps/web/src/routes/[.well-known]/oauth-authorization-server.ts b/apps/web/src/routes/[.well-known]/oauth-authorization-server.ts new file mode 100644 index 0000000..6d94460 --- /dev/null +++ b/apps/web/src/routes/[.well-known]/oauth-authorization-server.ts @@ -0,0 +1,15 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { auth } from "@zendegi/auth"; +import { oauthProviderAuthServerMetadata } from "@better-auth/oauth-provider"; + +const handler = oauthProviderAuthServerMetadata(auth); + +export const Route = createFileRoute("/.well-known/oauth-authorization-server")( + { + server: { + handlers: { + GET: ({ request }) => handler(request), + }, + }, + }, +); diff --git a/apps/web/src/routes/[.well-known]/oauth-protected-resource.ts b/apps/web/src/routes/[.well-known]/oauth-protected-resource.ts new file mode 100644 index 0000000..3c8f191 --- /dev/null +++ b/apps/web/src/routes/[.well-known]/oauth-protected-resource.ts @@ -0,0 +1,26 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { env } from "@zendegi/env/server"; + +export const Route = createFileRoute("/.well-known/oauth-protected-resource")({ + server: { + handlers: { + GET: () => { + const baseUrl = env.BETTER_AUTH_URL.replace(/\/api\/auth$/, ""); + return new Response( + JSON.stringify({ + resource: baseUrl, + authorization_servers: [`${baseUrl}/api/auth`], + bearer_methods_supported: ["header"], + }), + { + headers: { + "Content-Type": "application/json", + "Cache-Control": + "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400", + }, + }, + ); + }, + }, + }, +}); diff --git a/apps/web/src/routes/[.well-known]/openid-configuration.ts b/apps/web/src/routes/[.well-known]/openid-configuration.ts new file mode 100644 index 0000000..701dc33 --- /dev/null +++ b/apps/web/src/routes/[.well-known]/openid-configuration.ts @@ -0,0 +1,13 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { auth } from "@zendegi/auth"; +import { oauthProviderOpenIdConfigMetadata } from "@better-auth/oauth-provider"; + +const handler = oauthProviderOpenIdConfigMetadata(auth); + +export const Route = createFileRoute("/.well-known/openid-configuration")({ + server: { + handlers: { + GET: ({ request }) => handler(request), + }, + }, +}); diff --git a/apps/web/src/routes/api/mcp/$.ts b/apps/web/src/routes/api/mcp/$.ts new file mode 100644 index 0000000..52d32f0 --- /dev/null +++ b/apps/web/src/routes/api/mcp/$.ts @@ -0,0 +1,316 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"; +import { mcpHandler } from "@better-auth/oauth-provider"; +import { env } from "@zendegi/env/server"; +import { db } from "@zendegi/db"; +import { + timeline, + timelineGroup, + timelineItem, +} from "@zendegi/db/schema/timeline"; +import { and, eq } from "drizzle-orm"; +import { z } from "zod"; + +function createMcpServer(userId: string) { + const server = new McpServer({ + name: "zendegi", + version: "1.0.0", + }); + + // --- Read tools --- + + server.registerTool( + "list_timelines", + { + description: "List the authenticated user's timelines", + }, + async () => { + const timelines = await db.query.timeline.findMany({ + where: eq(timeline.ownerId, userId), + orderBy: (t, { desc }) => [desc(t.createdAt)], + }); + return { + content: [{ type: "text" as const, text: JSON.stringify(timelines) }], + }; + } + ); + + server.registerTool( + "get_timeline", + { + description: "Get a timeline by ID, including its groups and items", + inputSchema: { id: z.string().uuid().describe("Timeline ID") }, + }, + async ({ id }) => { + const result = await db.query.timeline.findFirst({ + where: and(eq(timeline.id, id), eq(timeline.ownerId, userId)), + with: { + groups: { + orderBy: (g, { asc }) => [asc(g.sortOrder)], + with: { + items: { + orderBy: (i, { asc }) => [asc(i.start)], + }, + }, + }, + }, + }); + if (!result) { + return { + content: [{ type: "text" as const, text: "Timeline not found" }], + isError: true, + }; + } + return { + content: [{ type: "text" as const, text: JSON.stringify(result) }], + }; + } + ); + + // --- Create tools --- + + server.registerTool( + "create_timeline", + { + description: "Create a new timeline", + inputSchema: { title: z.string().min(1).describe("Timeline title") }, + }, + async ({ title }) => { + const [created] = await db + .insert(timeline) + .values({ title, ownerId: userId }) + .returning(); + return { + content: [{ type: "text" as const, text: JSON.stringify(created) }], + }; + } + ); + + server.registerTool( + "create_group", + { + description: "Create a group in a timeline", + inputSchema: { + title: z.string().min(1).describe("Group title"), + timelineId: z.string().uuid().describe("Parent timeline ID"), + }, + }, + async ({ title, timelineId }) => { + // Verify ownership + const t = await db.query.timeline.findFirst({ + where: and(eq(timeline.id, timelineId), eq(timeline.ownerId, userId)), + }); + if (!t) { + return { + content: [{ type: "text" as const, text: "Timeline not found" }], + isError: true, + }; + } + const [created] = await db + .insert(timelineGroup) + .values({ title, timelineId }) + .returning(); + return { + content: [{ type: "text" as const, text: JSON.stringify(created) }], + }; + } + ); + + server.registerTool( + "create_item", + { + description: "Create an item in a group", + inputSchema: { + title: z.string().min(1).describe("Item title"), + description: z.string().default("").describe("Item description"), + start: z.string().describe("Start date (ISO 8601)"), + end: z.string().optional().describe("End date (ISO 8601, optional)"), + timelineGroupId: z.string().uuid().describe("Parent group ID"), + }, + }, + async ({ title, description, start, end, timelineGroupId }) => { + // Verify ownership through the group's timeline + const group = await db.query.timelineGroup.findFirst({ + where: eq(timelineGroup.id, timelineGroupId), + with: { timeline: true }, + }); + if (!group || group.timeline.ownerId !== userId) { + return { + content: [{ type: "text" as const, text: "Group not found" }], + isError: true, + }; + } + const [created] = await db + .insert(timelineItem) + .values({ + title, + description, + start: new Date(start), + end: end ? new Date(end) : undefined, + timelineGroupId, + }) + .returning(); + return { + content: [{ type: "text" as const, text: JSON.stringify(created) }], + }; + } + ); + + // --- Update tools --- + + server.registerTool( + "update_item", + { + description: "Update an existing item's fields", + inputSchema: { + id: z.string().uuid().describe("Item ID"), + title: z.string().min(1).optional().describe("New title"), + description: z.string().optional().describe("New description"), + start: z.string().optional().describe("New start date (ISO 8601)"), + end: z + .string() + .nullable() + .optional() + .describe("New end date (ISO 8601, or null to remove)"), + }, + }, + async ({ id, title, description, start, end }) => { + // Verify ownership through item → group → timeline + const item = await db.query.timelineItem.findFirst({ + where: eq(timelineItem.id, id), + with: { timelineGroup: { with: { timeline: true } } }, + }); + if (!item || item.timelineGroup.timeline.ownerId !== userId) { + return { + content: [{ type: "text" as const, text: "Item not found" }], + isError: true, + }; + } + + const updates: Record = {}; + if (title !== undefined) updates.title = title; + if (description !== undefined) updates.description = description; + if (start !== undefined) updates.start = new Date(start); + if (end !== undefined) updates.end = end ? new Date(end) : null; + + const [updated] = await db + .update(timelineItem) + .set(updates) + .where(eq(timelineItem.id, id)) + .returning(); + return { + content: [{ type: "text" as const, text: JSON.stringify(updated) }], + }; + } + ); + + // --- Delete tools --- + + server.registerTool( + "delete_timeline", + { + description: "Delete a timeline and all its groups and items (cascades)", + inputSchema: { id: z.string().uuid().describe("Timeline ID") }, + }, + async ({ id }) => { + const deleted = await db + .delete(timeline) + .where(and(eq(timeline.id, id), eq(timeline.ownerId, userId))) + .returning(); + if (deleted.length === 0) { + return { + content: [{ type: "text" as const, text: "Timeline not found" }], + isError: true, + }; + } + return { + content: [{ type: "text" as const, text: "Timeline deleted" }], + }; + } + ); + + server.registerTool( + "delete_group", + { + description: "Delete a group and all its items (cascades)", + inputSchema: { id: z.string().uuid().describe("Group ID") }, + }, + async ({ id }) => { + // Verify ownership + const group = await db.query.timelineGroup.findFirst({ + where: eq(timelineGroup.id, id), + with: { timeline: true }, + }); + if (!group || group.timeline.ownerId !== userId) { + return { + content: [{ type: "text" as const, text: "Group not found" }], + isError: true, + }; + } + await db.delete(timelineGroup).where(eq(timelineGroup.id, id)); + return { + content: [{ type: "text" as const, text: "Group deleted" }], + }; + } + ); + + server.registerTool( + "delete_item", + { + description: "Delete an item", + inputSchema: { id: z.string().uuid().describe("Item ID") }, + }, + async ({ id }) => { + // Verify ownership + const item = await db.query.timelineItem.findFirst({ + where: eq(timelineItem.id, id), + with: { timelineGroup: { with: { timeline: true } } }, + }); + if (!item || item.timelineGroup.timeline.ownerId !== userId) { + return { + content: [{ type: "text" as const, text: "Item not found" }], + isError: true, + }; + } + await db.delete(timelineItem).where(eq(timelineItem.id, id)); + return { + content: [{ type: "text" as const, text: "Item deleted" }], + }; + } + ); + + return server; +} + +const handler = mcpHandler( + { + jwksUrl: `${env.BETTER_AUTH_URL}/api/auth/jwks`, + verifyOptions: { + issuer: `${env.BETTER_AUTH_URL}/api/auth`, + audience: [env.BETTER_AUTH_URL, new URL(env.BETTER_AUTH_URL).href], + }, + }, + async (req, jwt) => { + const userId = jwt.sub; + if (!userId) { + return new Response("Missing subject in token", { status: 401 }); + } + const server = createMcpServer(userId); + const transport = new WebStandardStreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + }); + await server.connect(transport); + return transport.handleRequest(req); + } +); + +export const Route = createFileRoute("/api/mcp/$")({ + server: { + handlers: { + GET: ({ request }) => handler(request), + POST: ({ request }) => handler(request), + DELETE: ({ request }) => handler(request), + }, + }, +}); diff --git a/apps/web/src/routes/consent.tsx b/apps/web/src/routes/consent.tsx new file mode 100644 index 0000000..02d6daa --- /dev/null +++ b/apps/web/src/routes/consent.tsx @@ -0,0 +1,54 @@ +import { createFileRoute, useSearch } from "@tanstack/react-router"; + +import { authClient } from "@/lib/auth-client"; +import { Button } from "@/components/ui/button"; + +export const Route = createFileRoute("/consent")({ + validateSearch: (search: Record) => ({ + client_id: (search.client_id as string) ?? "", + scope: (search.scope as string) ?? "", + }), + component: ConsentComponent, +}); + +function ConsentComponent() { + const { client_id, scope } = useSearch({ from: "/consent" }); + const scopes = scope.split(" ").filter(Boolean); + + const handleAccept = async () => { + await authClient.oauth2.consent({ accept: true }); + }; + + const handleDeny = async () => { + await authClient.oauth2.consent({ accept: false }); + }; + + return ( +
+
+

Authorize Application

+

+ {client_id} is requesting access to your account. +

+ + {scopes.length > 0 && ( +
+

Requested permissions:

+
    + {scopes.map((s) => ( +
  • {s}
  • + ))} +
+
+ )} + +
+ + +
+
+
+ ); +} diff --git a/packages/auth/package.json b/packages/auth/package.json index 1145737..0ab903a 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -15,6 +15,7 @@ "lint": "eslint ." }, "dependencies": { + "@better-auth/oauth-provider": "^1.4.19", "@zendegi/db": "workspace:*", "@zendegi/env": "workspace:*", "better-auth": "catalog:", diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts index 2736925..6e7e4c5 100644 --- a/packages/auth/src/index.ts +++ b/packages/auth/src/index.ts @@ -2,7 +2,8 @@ import { db } from "@zendegi/db"; import * as schema from "@zendegi/db/schema/auth"; import { env } from "@zendegi/env/server"; import { betterAuth } from "better-auth"; -import { anonymous } from "better-auth/plugins"; +import { anonymous, jwt } from "better-auth/plugins"; +import { oauthProvider } from "@better-auth/oauth-provider"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { tanstackStartCookies } from "better-auth/tanstack-start"; @@ -16,7 +17,19 @@ export const auth = betterAuth({ emailAndPassword: { enabled: true, }, - plugins: [tanstackStartCookies(), anonymous()], + disabledPaths: ["/token"], + plugins: [ + tanstackStartCookies(), + anonymous(), + jwt(), + oauthProvider({ + loginPage: "/login", + consentPage: "/consent", + allowDynamicClientRegistration: true, + allowUnauthenticatedClientRegistration: true, + validAudiences: [env.BETTER_AUTH_URL, new URL(env.BETTER_AUTH_URL).href], + }), + ], advanced: { database: { generateId: "uuid", diff --git a/packages/db/src/schema/auth.ts b/packages/db/src/schema/auth.ts index bcedee8..57a4701 100644 --- a/packages/db/src/schema/auth.ts +++ b/packages/db/src/schema/auth.ts @@ -1,11 +1,12 @@ import { relations, sql } from "drizzle-orm"; import { - boolean, - index, pgTable, text, timestamp, + boolean, uuid, + jsonb, + index, } from "drizzle-orm/pg-core"; export const user = pgTable("user", { @@ -16,10 +17,8 @@ export const user = pgTable("user", { email: text("email").notNull().unique(), emailVerified: boolean("email_verified").default(false).notNull(), image: text("image"), - createdAt: timestamp("created_at", { withTimezone: true }) - .defaultNow() - .notNull(), - updatedAt: timestamp("updated_at", { withTimezone: true }) + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") .defaultNow() .$onUpdate(() => /* @__PURE__ */ new Date()) .notNull(), @@ -32,12 +31,10 @@ export const session = pgTable( id: uuid("id") .default(sql`pg_catalog.gen_random_uuid()`) .primaryKey(), - expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(), + expiresAt: timestamp("expires_at").notNull(), token: text("token").notNull().unique(), - createdAt: timestamp("created_at", { withTimezone: true }) - .defaultNow() - .notNull(), - updatedAt: timestamp("updated_at", { withTimezone: true }) + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") .$onUpdate(() => /* @__PURE__ */ new Date()) .notNull(), ipAddress: text("ip_address"), @@ -46,7 +43,7 @@ export const session = pgTable( .notNull() .references(() => user.id, { onDelete: "cascade" }), }, - (table) => [index("session_userId_idx").on(table.userId)] + (table) => [index("session_userId_idx").on(table.userId)], ); export const account = pgTable( @@ -63,22 +60,16 @@ export const account = pgTable( accessToken: text("access_token"), refreshToken: text("refresh_token"), idToken: text("id_token"), - accessTokenExpiresAt: timestamp("access_token_expires_at", { - withTimezone: true, - }), - refreshTokenExpiresAt: timestamp("refresh_token_expires_at", { - withTimezone: true, - }), + accessTokenExpiresAt: timestamp("access_token_expires_at"), + refreshTokenExpiresAt: timestamp("refresh_token_expires_at"), scope: text("scope"), password: text("password"), - createdAt: timestamp("created_at", { withTimezone: true }) - .defaultNow() - .notNull(), - updatedAt: timestamp("updated_at", { withTimezone: true }) + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") .$onUpdate(() => /* @__PURE__ */ new Date()) .notNull(), }, - (table) => [index("account_userId_idx").on(table.userId)] + (table) => [index("account_userId_idx").on(table.userId)], ); export const verification = pgTable( @@ -96,19 +87,124 @@ export const verification = pgTable( .$onUpdate(() => /* @__PURE__ */ new Date()) .notNull(), }, - (table) => [index("verification_identifier_idx").on(table.identifier)] + (table) => [index("verification_identifier_idx").on(table.identifier)], ); +export const jwks = pgTable("jwks", { + id: uuid("id") + .default(sql`pg_catalog.gen_random_uuid()`) + .primaryKey(), + publicKey: text("public_key").notNull(), + privateKey: text("private_key").notNull(), + createdAt: timestamp("created_at").notNull(), + expiresAt: timestamp("expires_at"), +}); + +export const oauthClient = pgTable("oauth_client", { + id: uuid("id") + .default(sql`pg_catalog.gen_random_uuid()`) + .primaryKey(), + clientId: text("client_id").notNull().unique(), + clientSecret: text("client_secret"), + disabled: boolean("disabled").default(false), + skipConsent: boolean("skip_consent"), + enableEndSession: boolean("enable_end_session"), + scopes: text("scopes").array(), + userId: uuid("user_id").references(() => user.id, { onDelete: "cascade" }), + createdAt: timestamp("created_at"), + updatedAt: timestamp("updated_at"), + name: text("name"), + uri: text("uri"), + icon: text("icon"), + contacts: text("contacts").array(), + tos: text("tos"), + policy: text("policy"), + softwareId: text("software_id"), + softwareVersion: text("software_version"), + softwareStatement: text("software_statement"), + redirectUris: text("redirect_uris").array().notNull(), + postLogoutRedirectUris: text("post_logout_redirect_uris").array(), + tokenEndpointAuthMethod: text("token_endpoint_auth_method"), + grantTypes: text("grant_types").array(), + responseTypes: text("response_types").array(), + public: boolean("public"), + type: text("type"), + referenceId: text("reference_id"), + metadata: jsonb("metadata"), +}); + +export const oauthRefreshToken = pgTable("oauth_refresh_token", { + id: uuid("id") + .default(sql`pg_catalog.gen_random_uuid()`) + .primaryKey(), + token: text("token").notNull(), + clientId: text("client_id") + .notNull() + .references(() => oauthClient.clientId, { onDelete: "cascade" }), + sessionId: uuid("session_id").references(() => session.id, { + onDelete: "set null", + }), + userId: uuid("user_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + referenceId: text("reference_id"), + expiresAt: timestamp("expires_at"), + createdAt: timestamp("created_at"), + revoked: timestamp("revoked"), + scopes: text("scopes").array().notNull(), +}); + +export const oauthAccessToken = pgTable("oauth_access_token", { + id: uuid("id") + .default(sql`pg_catalog.gen_random_uuid()`) + .primaryKey(), + token: text("token").unique(), + clientId: text("client_id") + .notNull() + .references(() => oauthClient.clientId, { onDelete: "cascade" }), + sessionId: uuid("session_id").references(() => session.id, { + onDelete: "set null", + }), + userId: uuid("user_id").references(() => user.id, { onDelete: "cascade" }), + referenceId: text("reference_id"), + refreshId: uuid("refresh_id").references(() => oauthRefreshToken.id, { + onDelete: "cascade", + }), + expiresAt: timestamp("expires_at"), + createdAt: timestamp("created_at"), + scopes: text("scopes").array().notNull(), +}); + +export const oauthConsent = pgTable("oauth_consent", { + id: uuid("id") + .default(sql`pg_catalog.gen_random_uuid()`) + .primaryKey(), + clientId: text("client_id") + .notNull() + .references(() => oauthClient.clientId, { onDelete: "cascade" }), + userId: uuid("user_id").references(() => user.id, { onDelete: "cascade" }), + referenceId: text("reference_id"), + scopes: text("scopes").array().notNull(), + createdAt: timestamp("created_at"), + updatedAt: timestamp("updated_at"), +}); + export const userRelations = relations(user, ({ many }) => ({ sessions: many(session), accounts: many(account), + oauthClients: many(oauthClient), + oauthRefreshTokens: many(oauthRefreshToken), + oauthAccessTokens: many(oauthAccessToken), + oauthConsents: many(oauthConsent), })); -export const sessionRelations = relations(session, ({ one }) => ({ +export const sessionRelations = relations(session, ({ one, many }) => ({ user: one(user, { fields: [session.userId], references: [user.id], }), + oauthRefreshTokens: many(oauthRefreshToken), + oauthAccessTokens: many(oauthAccessToken), })); export const accountRelations = relations(account, ({ one }) => ({ @@ -117,3 +213,65 @@ export const accountRelations = relations(account, ({ one }) => ({ references: [user.id], }), })); + +export const oauthClientRelations = relations(oauthClient, ({ one, many }) => ({ + user: one(user, { + fields: [oauthClient.userId], + references: [user.id], + }), + oauthRefreshTokens: many(oauthRefreshToken), + oauthAccessTokens: many(oauthAccessToken), + oauthConsents: many(oauthConsent), +})); + +export const oauthRefreshTokenRelations = relations( + oauthRefreshToken, + ({ one, many }) => ({ + oauthClient: one(oauthClient, { + fields: [oauthRefreshToken.clientId], + references: [oauthClient.clientId], + }), + session: one(session, { + fields: [oauthRefreshToken.sessionId], + references: [session.id], + }), + user: one(user, { + fields: [oauthRefreshToken.userId], + references: [user.id], + }), + oauthAccessTokens: many(oauthAccessToken), + }), +); + +export const oauthAccessTokenRelations = relations( + oauthAccessToken, + ({ one }) => ({ + oauthClient: one(oauthClient, { + fields: [oauthAccessToken.clientId], + references: [oauthClient.clientId], + }), + session: one(session, { + fields: [oauthAccessToken.sessionId], + references: [session.id], + }), + user: one(user, { + fields: [oauthAccessToken.userId], + references: [user.id], + }), + oauthRefreshToken: one(oauthRefreshToken, { + fields: [oauthAccessToken.refreshId], + references: [oauthRefreshToken.id], + }), + }), +); + +export const oauthConsentRelations = relations(oauthConsent, ({ one }) => ({ + oauthClient: one(oauthClient, { + fields: [oauthConsent.clientId], + references: [oauthClient.clientId], + }), + user: one(user, { + fields: [oauthConsent.userId], + references: [user.id], + }), +})); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6aa837c..5cb5c60 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,8 +10,8 @@ catalogs: specifier: ^22.13.14 version: 22.19.11 better-auth: - specifier: ^1.4.18 - version: 1.4.18 + specifier: ^1.4.19 + version: 1.4.19 dotenv: specifier: ^17.2.2 version: 17.2.4 @@ -60,12 +60,18 @@ importers: '@base-ui/react': specifier: ^1.2.0 version: 1.2.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@better-auth/oauth-provider': + specifier: ^1.4.19 + version: 1.4.19(@better-auth/core@1.4.19(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-auth@1.4.19(@tanstack/react-start@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)))(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.18.0))(pg@8.18.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(better-call@1.1.8(zod@4.3.6)) '@fontsource-variable/inter': specifier: ^5.2.8 version: 5.2.8 '@fontsource/instrument-serif': specifier: ^5.2.8 version: 5.2.8 + '@modelcontextprotocol/sdk': + specifier: ^1.27.0 + version: 1.27.0(zod@4.3.6) '@tailwindcss/vite': specifier: ^4.1.8 version: 4.1.18(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) @@ -104,7 +110,7 @@ importers: version: link:../../packages/z-timeline better-auth: specifier: 'catalog:' - version: 1.4.18(@tanstack/react-start@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)))(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.18.0))(pg@8.18.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 1.4.19(@tanstack/react-start@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)))(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.18.0))(pg@8.18.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: specifier: ^2.1.1 version: 2.1.1 @@ -193,6 +199,9 @@ importers: packages/auth: dependencies: + '@better-auth/oauth-provider': + specifier: ^1.4.19 + version: 1.4.19(@better-auth/core@1.4.19(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-auth@1.4.19(@tanstack/react-start@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)))(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.18.0))(pg@8.18.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(better-call@1.1.8(zod@4.3.6)) '@zendegi/db': specifier: workspace:* version: link:../db @@ -201,7 +210,7 @@ importers: version: link:../env better-auth: specifier: 'catalog:' - version: 1.4.18(@tanstack/react-start@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)))(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.18.0))(pg@8.18.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 1.4.19(@tanstack/react-start@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)))(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.18.0))(pg@8.18.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) dotenv: specifier: 'catalog:' version: 17.2.4 @@ -494,8 +503,8 @@ packages: '@types/react': optional: true - '@better-auth/core@1.4.18': - resolution: {integrity: sha512-q+awYgC7nkLEBdx2sW0iJjkzgSHlIxGnOpsN1r/O1+a4m7osJNHtfK2mKJSL1I+GfNyIlxJF8WvD/NLuYMpmcg==} + '@better-auth/core@1.4.19': + resolution: {integrity: sha512-uADLHG1jc5BnEJi7f6ijUN5DmPPRSj++7m/G19z3UqA3MVCo4Y4t1MMa4IIxLCqGDFv22drdfxescgW+HnIowA==} peerDependencies: '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.21 @@ -504,10 +513,19 @@ packages: kysely: ^0.28.5 nanostores: ^1.0.1 - '@better-auth/telemetry@1.4.18': - resolution: {integrity: sha512-e5rDF8S4j3Um/0LIVATL2in9dL4lfO2fr2v1Wio4qTMRbfxqnUDTa+6SZtwdeJrbc4O+a3c+IyIpjG9Q/6GpfQ==} + '@better-auth/oauth-provider@1.4.19': + resolution: {integrity: sha512-KRUTgGS3ZXJ5RSWBrDDnlqvqUrNRjT6e3+yS82VWBpKnCuU8z7Dtv1o9hLsoeguGwEHUuXUxn9qP5IlwVnR8yQ==} peerDependencies: - '@better-auth/core': 1.4.18 + '@better-auth/core': 1.4.19 + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.21 + better-auth: 1.4.19 + better-call: 1.1.8 + + '@better-auth/telemetry@1.4.19': + resolution: {integrity: sha512-ApGNS7olCTtDpKF8Ow3Z+jvFAirOj7c4RyFUpu8axklh3mH57ndpfUAUjhgA8UVoaaH/mnm/Tl884BlqiewLyw==} + peerDependencies: + '@better-auth/core': 1.4.19 '@better-auth/utils@0.3.0': resolution: {integrity: sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw==} @@ -1157,8 +1175,8 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@modelcontextprotocol/sdk@1.26.0': - resolution: {integrity: sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==} + '@modelcontextprotocol/sdk@1.27.0': + resolution: {integrity: sha512-qOdO524oPMkUsOJTrsH9vz/HN3B5pKyW+9zIW51A9kDMVe7ON70drz1ouoyoyOcfzc+oxhkQ6jWmbyKnlWmYqA==} engines: {node: '>=18'} peerDependencies: '@cfworker/json-schema': ^4.1.1 @@ -2053,8 +2071,8 @@ packages: resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} hasBin: true - better-auth@1.4.18: - resolution: {integrity: sha512-bnyifLWBPcYVltH3RhS7CM62MoelEqC6Q+GnZwfiDWNfepXoQZBjEvn4urcERC7NTKgKq5zNBM8rvPvRBa6xcg==} + better-auth@1.4.19: + resolution: {integrity: sha512-3RlZJcA0+NH25wYD85vpIGwW9oSTuEmLIaGbT8zg41w/Pa2hVWHKedjoUHHJtnzkBXzDb+CShkLnSw7IThDdqQ==} peerDependencies: '@lynx-js/react': '*' '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 @@ -4850,7 +4868,7 @@ snapshots: optionalDependencies: '@types/react': 19.2.7 - '@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)': + '@better-auth/core@1.4.19(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)': dependencies: '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.21 @@ -4861,9 +4879,19 @@ snapshots: nanostores: 1.1.0 zod: 4.3.6 - '@better-auth/telemetry@1.4.18(@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))': + '@better-auth/oauth-provider@1.4.19(@better-auth/core@1.4.19(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-auth@1.4.19(@tanstack/react-start@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)))(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.18.0))(pg@8.18.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(better-call@1.1.8(zod@4.3.6))': dependencies: - '@better-auth/core': 1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) + '@better-auth/core': 1.4.19(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.21 + better-auth: 1.4.19(@tanstack/react-start@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)))(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.18.0))(pg@8.18.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + better-call: 1.1.8(zod@4.3.6) + jose: 6.1.3 + zod: 4.3.6 + + '@better-auth/telemetry@1.4.19(@better-auth/core@1.4.19(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))': + dependencies: + '@better-auth/core': 1.4.19(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.21 @@ -5292,7 +5320,7 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@modelcontextprotocol/sdk@1.26.0(zod@3.25.76)': + '@modelcontextprotocol/sdk@1.27.0(zod@3.25.76)': dependencies: '@hono/node-server': 1.19.9(hono@4.11.9) ajv: 8.17.1 @@ -5314,6 +5342,28 @@ snapshots: transitivePeerDependencies: - supports-color + '@modelcontextprotocol/sdk@1.27.0(zod@4.3.6)': + dependencies: + '@hono/node-server': 1.19.9(hono@4.11.9) + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 8.2.1(express@5.2.1) + hono: 4.11.9 + jose: 6.1.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.3.6 + zod-to-json-schema: 3.25.1(zod@4.3.6) + transitivePeerDependencies: + - supports-color + '@mswjs/interceptors@0.41.2': dependencies: '@open-draft/deferred-promise': 2.2.0 @@ -6207,10 +6257,10 @@ snapshots: baseline-browser-mapping@2.9.19: {} - better-auth@1.4.18(@tanstack/react-start@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)))(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.18.0))(pg@8.18.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + better-auth@1.4.19(@tanstack/react-start@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)))(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@types/pg@8.16.0)(kysely@0.28.11)(pg@8.18.0))(pg@8.18.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - '@better-auth/core': 1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) - '@better-auth/telemetry': 1.4.18(@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)) + '@better-auth/core': 1.4.19(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) + '@better-auth/telemetry': 1.4.19(@better-auth/core@1.4.19(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)) '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.21 '@noble/ciphers': 2.1.1 @@ -8234,7 +8284,7 @@ snapshots: '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) '@dotenvx/dotenvx': 1.52.0 - '@modelcontextprotocol/sdk': 1.26.0(zod@3.25.76) + '@modelcontextprotocol/sdk': 1.27.0(zod@3.25.76) '@types/validate-npm-package-name': 4.0.2 browserslist: 4.28.1 commander: 14.0.3 @@ -8864,6 +8914,10 @@ snapshots: dependencies: zod: 3.25.76 + zod-to-json-schema@3.25.1(zod@4.3.6): + dependencies: + zod: 4.3.6 + zod@3.25.76: {} zod@4.3.6: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index bda46bb..381a5f1 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -6,4 +6,4 @@ catalog: zod: ^4.1.13 typescript: ^5 "@types/node": ^22.13.14 - better-auth: ^1.4.18 + better-auth: ^1.4.19