This commit is contained in:
2026-02-24 19:31:30 +01:00
parent 28898fb081
commit 1bb9241116
16 changed files with 878 additions and 50 deletions

8
.mcp.json Normal file
View File

@@ -0,0 +1,8 @@
{
"mcpServers": {
"zendegi-mcp": {
"type": "http",
"url": "http://localhost:3001/api/mcp"
}
}
}

View File

@@ -11,8 +11,10 @@
}, },
"dependencies": { "dependencies": {
"@base-ui/react": "^1.2.0", "@base-ui/react": "^1.2.0",
"@better-auth/oauth-provider": "^1.4.19",
"@fontsource-variable/inter": "^5.2.8", "@fontsource-variable/inter": "^5.2.8",
"@fontsource/instrument-serif": "^5.2.8", "@fontsource/instrument-serif": "^5.2.8",
"@modelcontextprotocol/sdk": "^1.27.0",
"@tailwindcss/vite": "^4.1.8", "@tailwindcss/vite": "^4.1.8",
"@tanstack/react-form": "^1.23.5", "@tanstack/react-form": "^1.23.5",
"@tanstack/react-query": "^5.80.6", "@tanstack/react-query": "^5.80.6",

View File

@@ -1,6 +1,7 @@
import { createAuthClient } from "better-auth/react"; import { createAuthClient } from "better-auth/react";
import { anonymousClient } from "better-auth/client/plugins"; import { anonymousClient } from "better-auth/client/plugins";
import { oauthProviderClient } from "@better-auth/oauth-provider/client";
export const authClient = createAuthClient({ export const authClient = createAuthClient({
plugins: [anonymousClient()], plugins: [anonymousClient(), oauthProviderClient()],
}); });

View File

@@ -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)],
});

View File

@@ -14,9 +14,15 @@ import { Route as TimelineRouteImport } from './routes/timeline'
import { Route as LoginRouteImport } from './routes/login' import { Route as LoginRouteImport } from './routes/login'
import { Route as DemoRouteImport } from './routes/demo' import { Route as DemoRouteImport } from './routes/demo'
import { Route as DashboardRouteImport } from './routes/dashboard' import { Route as DashboardRouteImport } from './routes/dashboard'
import { Route as ConsentRouteImport } from './routes/consent'
import { Route as IndexRouteImport } from './routes/index' import { Route as IndexRouteImport } from './routes/index'
import { Route as TimelineTimelineIdRouteImport } from './routes/timeline.$timelineId' 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 ApiAuthSplatRouteImport } from './routes/api/auth/$'
import { Route as DotwellKnownOauthAuthorizationServerApiAuthRouteImport } from './routes/[.well-known]/oauth-authorization-server.api.auth'
const TimelinesRoute = TimelinesRouteImport.update({ const TimelinesRoute = TimelinesRouteImport.update({
id: '/timelines', id: '/timelines',
@@ -43,6 +49,11 @@ const DashboardRoute = DashboardRouteImport.update({
path: '/dashboard', path: '/dashboard',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const ConsentRoute = ConsentRouteImport.update({
id: '/consent',
path: '/consent',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({ const IndexRoute = IndexRouteImport.update({
id: '/', id: '/',
path: '/', path: '/',
@@ -53,84 +64,154 @@ const TimelineTimelineIdRoute = TimelineTimelineIdRouteImport.update({
path: '/$timelineId', path: '/$timelineId',
getParentRoute: () => TimelineRoute, getParentRoute: () => TimelineRoute,
} as any) } 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({ const ApiAuthSplatRoute = ApiAuthSplatRouteImport.update({
id: '/api/auth/$', id: '/api/auth/$',
path: '/api/auth/$', path: '/api/auth/$',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const DotwellKnownOauthAuthorizationServerApiAuthRoute =
DotwellKnownOauthAuthorizationServerApiAuthRouteImport.update({
id: '/api/auth',
path: '/api/auth',
getParentRoute: () => DotwellKnownOauthAuthorizationServerRoute,
} as any)
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof IndexRoute
'/consent': typeof ConsentRoute
'/dashboard': typeof DashboardRoute '/dashboard': typeof DashboardRoute
'/demo': typeof DemoRoute '/demo': typeof DemoRoute
'/login': typeof LoginRoute '/login': typeof LoginRoute
'/timeline': typeof TimelineRouteWithChildren '/timeline': typeof TimelineRouteWithChildren
'/timelines': typeof TimelinesRoute '/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 '/timeline/$timelineId': typeof TimelineTimelineIdRoute
'/api/auth/$': typeof ApiAuthSplatRoute '/api/auth/$': typeof ApiAuthSplatRoute
'/api/mcp/$': typeof ApiMcpSplatRoute
'/.well-known/oauth-authorization-server/api/auth': typeof DotwellKnownOauthAuthorizationServerApiAuthRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
'/consent': typeof ConsentRoute
'/dashboard': typeof DashboardRoute '/dashboard': typeof DashboardRoute
'/demo': typeof DemoRoute '/demo': typeof DemoRoute
'/login': typeof LoginRoute '/login': typeof LoginRoute
'/timeline': typeof TimelineRouteWithChildren '/timeline': typeof TimelineRouteWithChildren
'/timelines': typeof TimelinesRoute '/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 '/timeline/$timelineId': typeof TimelineTimelineIdRoute
'/api/auth/$': typeof ApiAuthSplatRoute '/api/auth/$': typeof ApiAuthSplatRoute
'/api/mcp/$': typeof ApiMcpSplatRoute
'/.well-known/oauth-authorization-server/api/auth': typeof DotwellKnownOauthAuthorizationServerApiAuthRoute
} }
export interface FileRoutesById { export interface FileRoutesById {
__root__: typeof rootRouteImport __root__: typeof rootRouteImport
'/': typeof IndexRoute '/': typeof IndexRoute
'/consent': typeof ConsentRoute
'/dashboard': typeof DashboardRoute '/dashboard': typeof DashboardRoute
'/demo': typeof DemoRoute '/demo': typeof DemoRoute
'/login': typeof LoginRoute '/login': typeof LoginRoute
'/timeline': typeof TimelineRouteWithChildren '/timeline': typeof TimelineRouteWithChildren
'/timelines': typeof TimelinesRoute '/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 '/timeline/$timelineId': typeof TimelineTimelineIdRoute
'/api/auth/$': typeof ApiAuthSplatRoute '/api/auth/$': typeof ApiAuthSplatRoute
'/api/mcp/$': typeof ApiMcpSplatRoute
'/.well-known/oauth-authorization-server/api/auth': typeof DotwellKnownOauthAuthorizationServerApiAuthRoute
} }
export interface FileRouteTypes { export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: fullPaths:
| '/' | '/'
| '/consent'
| '/dashboard' | '/dashboard'
| '/demo' | '/demo'
| '/login' | '/login'
| '/timeline' | '/timeline'
| '/timelines' | '/timelines'
| '/.well-known/oauth-authorization-server'
| '/.well-known/oauth-protected-resource'
| '/.well-known/openid-configuration'
| '/timeline/$timelineId' | '/timeline/$timelineId'
| '/api/auth/$' | '/api/auth/$'
| '/api/mcp/$'
| '/.well-known/oauth-authorization-server/api/auth'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: to:
| '/' | '/'
| '/consent'
| '/dashboard' | '/dashboard'
| '/demo' | '/demo'
| '/login' | '/login'
| '/timeline' | '/timeline'
| '/timelines' | '/timelines'
| '/.well-known/oauth-authorization-server'
| '/.well-known/oauth-protected-resource'
| '/.well-known/openid-configuration'
| '/timeline/$timelineId' | '/timeline/$timelineId'
| '/api/auth/$' | '/api/auth/$'
| '/api/mcp/$'
| '/.well-known/oauth-authorization-server/api/auth'
id: id:
| '__root__' | '__root__'
| '/' | '/'
| '/consent'
| '/dashboard' | '/dashboard'
| '/demo' | '/demo'
| '/login' | '/login'
| '/timeline' | '/timeline'
| '/timelines' | '/timelines'
| '/.well-known/oauth-authorization-server'
| '/.well-known/oauth-protected-resource'
| '/.well-known/openid-configuration'
| '/timeline/$timelineId' | '/timeline/$timelineId'
| '/api/auth/$' | '/api/auth/$'
| '/api/mcp/$'
| '/.well-known/oauth-authorization-server/api/auth'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
export interface RootRouteChildren { export interface RootRouteChildren {
IndexRoute: typeof IndexRoute IndexRoute: typeof IndexRoute
ConsentRoute: typeof ConsentRoute
DashboardRoute: typeof DashboardRoute DashboardRoute: typeof DashboardRoute
DemoRoute: typeof DemoRoute DemoRoute: typeof DemoRoute
LoginRoute: typeof LoginRoute LoginRoute: typeof LoginRoute
TimelineRoute: typeof TimelineRouteWithChildren TimelineRoute: typeof TimelineRouteWithChildren
TimelinesRoute: typeof TimelinesRoute TimelinesRoute: typeof TimelinesRoute
DotwellKnownOauthAuthorizationServerRoute: typeof DotwellKnownOauthAuthorizationServerRouteWithChildren
DotwellKnownOauthProtectedResourceRoute: typeof DotwellKnownOauthProtectedResourceRoute
DotwellKnownOpenidConfigurationRoute: typeof DotwellKnownOpenidConfigurationRoute
ApiAuthSplatRoute: typeof ApiAuthSplatRoute ApiAuthSplatRoute: typeof ApiAuthSplatRoute
ApiMcpSplatRoute: typeof ApiMcpSplatRoute
} }
declare module '@tanstack/react-router' { declare module '@tanstack/react-router' {
@@ -170,6 +251,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DashboardRouteImport preLoaderRoute: typeof DashboardRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/consent': {
id: '/consent'
path: '/consent'
fullPath: '/consent'
preLoaderRoute: typeof ConsentRouteImport
parentRoute: typeof rootRouteImport
}
'/': { '/': {
id: '/' id: '/'
path: '/' path: '/'
@@ -184,6 +272,34 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof TimelineTimelineIdRouteImport preLoaderRoute: typeof TimelineTimelineIdRouteImport
parentRoute: typeof TimelineRoute 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/$': { '/api/auth/$': {
id: '/api/auth/$' id: '/api/auth/$'
path: '/api/auth/$' path: '/api/auth/$'
@@ -191,6 +307,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ApiAuthSplatRouteImport preLoaderRoute: typeof ApiAuthSplatRouteImport
parentRoute: typeof rootRouteImport 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, TimelineRouteChildren,
) )
interface DotwellKnownOauthAuthorizationServerRouteChildren {
DotwellKnownOauthAuthorizationServerApiAuthRoute: typeof DotwellKnownOauthAuthorizationServerApiAuthRoute
}
const DotwellKnownOauthAuthorizationServerRouteChildren: DotwellKnownOauthAuthorizationServerRouteChildren =
{
DotwellKnownOauthAuthorizationServerApiAuthRoute:
DotwellKnownOauthAuthorizationServerApiAuthRoute,
}
const DotwellKnownOauthAuthorizationServerRouteWithChildren =
DotwellKnownOauthAuthorizationServerRoute._addFileChildren(
DotwellKnownOauthAuthorizationServerRouteChildren,
)
const rootRouteChildren: RootRouteChildren = { const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, IndexRoute: IndexRoute,
ConsentRoute: ConsentRoute,
DashboardRoute: DashboardRoute, DashboardRoute: DashboardRoute,
DemoRoute: DemoRoute, DemoRoute: DemoRoute,
LoginRoute: LoginRoute, LoginRoute: LoginRoute,
TimelineRoute: TimelineRouteWithChildren, TimelineRoute: TimelineRouteWithChildren,
TimelinesRoute: TimelinesRoute, TimelinesRoute: TimelinesRoute,
DotwellKnownOauthAuthorizationServerRoute:
DotwellKnownOauthAuthorizationServerRouteWithChildren,
DotwellKnownOauthProtectedResourceRoute:
DotwellKnownOauthProtectedResourceRoute,
DotwellKnownOpenidConfigurationRoute: DotwellKnownOpenidConfigurationRoute,
ApiAuthSplatRoute: ApiAuthSplatRoute, ApiAuthSplatRoute: ApiAuthSplatRoute,
ApiMcpSplatRoute: ApiMcpSplatRoute,
} }
export const routeTree = rootRouteImport export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren) ._addFileChildren(rootRouteChildren)

View File

@@ -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),
},
},
});

View File

@@ -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),
},
},
},
);

View File

@@ -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",
},
},
);
},
},
},
});

View File

@@ -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),
},
},
});

View File

@@ -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<string, unknown> = {};
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),
},
},
});

View File

@@ -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<string, unknown>) => ({
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 (
<section className="container mx-auto max-w-md px-4 py-12">
<div className="space-y-6">
<h1 className="text-2xl font-serif font-bold">Authorize Application</h1>
<p className="text-muted-foreground">
<strong>{client_id}</strong> is requesting access to your account.
</p>
{scopes.length > 0 && (
<div className="space-y-2">
<p className="font-medium">Requested permissions:</p>
<ul className="list-inside list-disc space-y-1">
{scopes.map((s) => (
<li key={s}>{s}</li>
))}
</ul>
</div>
)}
<div className="flex gap-3">
<Button onClick={handleAccept}>Allow</Button>
<Button variant="outline" onClick={handleDeny}>
Deny
</Button>
</div>
</div>
</section>
);
}

View File

@@ -15,6 +15,7 @@
"lint": "eslint ." "lint": "eslint ."
}, },
"dependencies": { "dependencies": {
"@better-auth/oauth-provider": "^1.4.19",
"@zendegi/db": "workspace:*", "@zendegi/db": "workspace:*",
"@zendegi/env": "workspace:*", "@zendegi/env": "workspace:*",
"better-auth": "catalog:", "better-auth": "catalog:",

View File

@@ -2,7 +2,8 @@ import { db } from "@zendegi/db";
import * as schema from "@zendegi/db/schema/auth"; import * as schema from "@zendegi/db/schema/auth";
import { env } from "@zendegi/env/server"; import { env } from "@zendegi/env/server";
import { betterAuth } from "better-auth"; 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 { drizzleAdapter } from "better-auth/adapters/drizzle";
import { tanstackStartCookies } from "better-auth/tanstack-start"; import { tanstackStartCookies } from "better-auth/tanstack-start";
@@ -16,7 +17,19 @@ export const auth = betterAuth({
emailAndPassword: { emailAndPassword: {
enabled: true, 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: { advanced: {
database: { database: {
generateId: "uuid", generateId: "uuid",

View File

@@ -1,11 +1,12 @@
import { relations, sql } from "drizzle-orm"; import { relations, sql } from "drizzle-orm";
import { import {
boolean,
index,
pgTable, pgTable,
text, text,
timestamp, timestamp,
boolean,
uuid, uuid,
jsonb,
index,
} from "drizzle-orm/pg-core"; } from "drizzle-orm/pg-core";
export const user = pgTable("user", { export const user = pgTable("user", {
@@ -16,10 +17,8 @@ export const user = pgTable("user", {
email: text("email").notNull().unique(), email: text("email").notNull().unique(),
emailVerified: boolean("email_verified").default(false).notNull(), emailVerified: boolean("email_verified").default(false).notNull(),
image: text("image"), image: text("image"),
createdAt: timestamp("created_at", { withTimezone: true }) createdAt: timestamp("created_at").defaultNow().notNull(),
.defaultNow() updatedAt: timestamp("updated_at")
.notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true })
.defaultNow() .defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date()) .$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(), .notNull(),
@@ -32,12 +31,10 @@ export const session = pgTable(
id: uuid("id") id: uuid("id")
.default(sql`pg_catalog.gen_random_uuid()`) .default(sql`pg_catalog.gen_random_uuid()`)
.primaryKey(), .primaryKey(),
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(), expiresAt: timestamp("expires_at").notNull(),
token: text("token").notNull().unique(), token: text("token").notNull().unique(),
createdAt: timestamp("created_at", { withTimezone: true }) createdAt: timestamp("created_at").defaultNow().notNull(),
.defaultNow() updatedAt: timestamp("updated_at")
.notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true })
.$onUpdate(() => /* @__PURE__ */ new Date()) .$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(), .notNull(),
ipAddress: text("ip_address"), ipAddress: text("ip_address"),
@@ -46,7 +43,7 @@ export const session = pgTable(
.notNull() .notNull()
.references(() => user.id, { onDelete: "cascade" }), .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( export const account = pgTable(
@@ -63,22 +60,16 @@ export const account = pgTable(
accessToken: text("access_token"), accessToken: text("access_token"),
refreshToken: text("refresh_token"), refreshToken: text("refresh_token"),
idToken: text("id_token"), idToken: text("id_token"),
accessTokenExpiresAt: timestamp("access_token_expires_at", { accessTokenExpiresAt: timestamp("access_token_expires_at"),
withTimezone: true, refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
}),
refreshTokenExpiresAt: timestamp("refresh_token_expires_at", {
withTimezone: true,
}),
scope: text("scope"), scope: text("scope"),
password: text("password"), password: text("password"),
createdAt: timestamp("created_at", { withTimezone: true }) createdAt: timestamp("created_at").defaultNow().notNull(),
.defaultNow() updatedAt: timestamp("updated_at")
.notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true })
.$onUpdate(() => /* @__PURE__ */ new Date()) .$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(), .notNull(),
}, },
(table) => [index("account_userId_idx").on(table.userId)] (table) => [index("account_userId_idx").on(table.userId)],
); );
export const verification = pgTable( export const verification = pgTable(
@@ -96,19 +87,124 @@ export const verification = pgTable(
.$onUpdate(() => /* @__PURE__ */ new Date()) .$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(), .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 }) => ({ export const userRelations = relations(user, ({ many }) => ({
sessions: many(session), sessions: many(session),
accounts: many(account), 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, { user: one(user, {
fields: [session.userId], fields: [session.userId],
references: [user.id], references: [user.id],
}), }),
oauthRefreshTokens: many(oauthRefreshToken),
oauthAccessTokens: many(oauthAccessToken),
})); }));
export const accountRelations = relations(account, ({ one }) => ({ export const accountRelations = relations(account, ({ one }) => ({
@@ -117,3 +213,65 @@ export const accountRelations = relations(account, ({ one }) => ({
references: [user.id], 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],
}),
}));

96
pnpm-lock.yaml generated
View File

@@ -10,8 +10,8 @@ catalogs:
specifier: ^22.13.14 specifier: ^22.13.14
version: 22.19.11 version: 22.19.11
better-auth: better-auth:
specifier: ^1.4.18 specifier: ^1.4.19
version: 1.4.18 version: 1.4.19
dotenv: dotenv:
specifier: ^17.2.2 specifier: ^17.2.2
version: 17.2.4 version: 17.2.4
@@ -60,12 +60,18 @@ importers:
'@base-ui/react': '@base-ui/react':
specifier: ^1.2.0 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) 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': '@fontsource-variable/inter':
specifier: ^5.2.8 specifier: ^5.2.8
version: 5.2.8 version: 5.2.8
'@fontsource/instrument-serif': '@fontsource/instrument-serif':
specifier: ^5.2.8 specifier: ^5.2.8
version: 5.2.8 version: 5.2.8
'@modelcontextprotocol/sdk':
specifier: ^1.27.0
version: 1.27.0(zod@4.3.6)
'@tailwindcss/vite': '@tailwindcss/vite':
specifier: ^4.1.8 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)) 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 version: link:../../packages/z-timeline
better-auth: better-auth:
specifier: 'catalog:' 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: clsx:
specifier: ^2.1.1 specifier: ^2.1.1
version: 2.1.1 version: 2.1.1
@@ -193,6 +199,9 @@ importers:
packages/auth: packages/auth:
dependencies: 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': '@zendegi/db':
specifier: workspace:* specifier: workspace:*
version: link:../db version: link:../db
@@ -201,7 +210,7 @@ importers:
version: link:../env version: link:../env
better-auth: better-auth:
specifier: 'catalog:' 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: dotenv:
specifier: 'catalog:' specifier: 'catalog:'
version: 17.2.4 version: 17.2.4
@@ -494,8 +503,8 @@ packages:
'@types/react': '@types/react':
optional: true optional: true
'@better-auth/core@1.4.18': '@better-auth/core@1.4.19':
resolution: {integrity: sha512-q+awYgC7nkLEBdx2sW0iJjkzgSHlIxGnOpsN1r/O1+a4m7osJNHtfK2mKJSL1I+GfNyIlxJF8WvD/NLuYMpmcg==} resolution: {integrity: sha512-uADLHG1jc5BnEJi7f6ijUN5DmPPRSj++7m/G19z3UqA3MVCo4Y4t1MMa4IIxLCqGDFv22drdfxescgW+HnIowA==}
peerDependencies: peerDependencies:
'@better-auth/utils': 0.3.0 '@better-auth/utils': 0.3.0
'@better-fetch/fetch': 1.1.21 '@better-fetch/fetch': 1.1.21
@@ -504,10 +513,19 @@ packages:
kysely: ^0.28.5 kysely: ^0.28.5
nanostores: ^1.0.1 nanostores: ^1.0.1
'@better-auth/telemetry@1.4.18': '@better-auth/oauth-provider@1.4.19':
resolution: {integrity: sha512-e5rDF8S4j3Um/0LIVATL2in9dL4lfO2fr2v1Wio4qTMRbfxqnUDTa+6SZtwdeJrbc4O+a3c+IyIpjG9Q/6GpfQ==} resolution: {integrity: sha512-KRUTgGS3ZXJ5RSWBrDDnlqvqUrNRjT6e3+yS82VWBpKnCuU8z7Dtv1o9hLsoeguGwEHUuXUxn9qP5IlwVnR8yQ==}
peerDependencies: 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': '@better-auth/utils@0.3.0':
resolution: {integrity: sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw==} resolution: {integrity: sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw==}
@@ -1157,8 +1175,8 @@ packages:
'@jridgewell/trace-mapping@0.3.31': '@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@modelcontextprotocol/sdk@1.26.0': '@modelcontextprotocol/sdk@1.27.0':
resolution: {integrity: sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==} resolution: {integrity: sha512-qOdO524oPMkUsOJTrsH9vz/HN3B5pKyW+9zIW51A9kDMVe7ON70drz1ouoyoyOcfzc+oxhkQ6jWmbyKnlWmYqA==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
'@cfworker/json-schema': ^4.1.1 '@cfworker/json-schema': ^4.1.1
@@ -2053,8 +2071,8 @@ packages:
resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==}
hasBin: true hasBin: true
better-auth@1.4.18: better-auth@1.4.19:
resolution: {integrity: sha512-bnyifLWBPcYVltH3RhS7CM62MoelEqC6Q+GnZwfiDWNfepXoQZBjEvn4urcERC7NTKgKq5zNBM8rvPvRBa6xcg==} resolution: {integrity: sha512-3RlZJcA0+NH25wYD85vpIGwW9oSTuEmLIaGbT8zg41w/Pa2hVWHKedjoUHHJtnzkBXzDb+CShkLnSw7IThDdqQ==}
peerDependencies: peerDependencies:
'@lynx-js/react': '*' '@lynx-js/react': '*'
'@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0
@@ -4850,7 +4868,7 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/react': 19.2.7 '@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: dependencies:
'@better-auth/utils': 0.3.0 '@better-auth/utils': 0.3.0
'@better-fetch/fetch': 1.1.21 '@better-fetch/fetch': 1.1.21
@@ -4861,9 +4879,19 @@ snapshots:
nanostores: 1.1.0 nanostores: 1.1.0
zod: 4.3.6 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: 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-auth/utils': 0.3.0
'@better-fetch/fetch': 1.1.21 '@better-fetch/fetch': 1.1.21
@@ -5292,7 +5320,7 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2 '@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5 '@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: dependencies:
'@hono/node-server': 1.19.9(hono@4.11.9) '@hono/node-server': 1.19.9(hono@4.11.9)
ajv: 8.17.1 ajv: 8.17.1
@@ -5314,6 +5342,28 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - 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': '@mswjs/interceptors@0.41.2':
dependencies: dependencies:
'@open-draft/deferred-promise': 2.2.0 '@open-draft/deferred-promise': 2.2.0
@@ -6207,10 +6257,10 @@ snapshots:
baseline-browser-mapping@2.9.19: {} 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: 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/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/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-auth/utils': 0.3.0
'@better-fetch/fetch': 1.1.21 '@better-fetch/fetch': 1.1.21
'@noble/ciphers': 2.1.1 '@noble/ciphers': 2.1.1
@@ -8234,7 +8284,7 @@ snapshots:
'@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0)
'@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0)
'@dotenvx/dotenvx': 1.52.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 '@types/validate-npm-package-name': 4.0.2
browserslist: 4.28.1 browserslist: 4.28.1
commander: 14.0.3 commander: 14.0.3
@@ -8864,6 +8914,10 @@ snapshots:
dependencies: dependencies:
zod: 3.25.76 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@3.25.76: {}
zod@4.3.6: {} zod@4.3.6: {}

View File

@@ -6,4 +6,4 @@ catalog:
zod: ^4.1.13 zod: ^4.1.13
typescript: ^5 typescript: ^5
"@types/node": ^22.13.14 "@types/node": ^22.13.14
better-auth: ^1.4.18 better-auth: ^1.4.19