add mcp
This commit is contained in:
8
.mcp.json
Normal file
8
.mcp.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"zendegi-mcp": {
|
||||
"type": "http",
|
||||
"url": "http://localhost:3001/api/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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()],
|
||||
});
|
||||
|
||||
7
apps/web/src/lib/server-client.ts
Normal file
7
apps/web/src/lib/server-client.ts
Normal 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)],
|
||||
});
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -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),
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
13
apps/web/src/routes/[.well-known]/openid-configuration.ts
Normal file
13
apps/web/src/routes/[.well-known]/openid-configuration.ts
Normal 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),
|
||||
},
|
||||
},
|
||||
});
|
||||
316
apps/web/src/routes/api/mcp/$.ts
Normal file
316
apps/web/src/routes/api/mcp/$.ts
Normal 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),
|
||||
},
|
||||
},
|
||||
});
|
||||
54
apps/web/src/routes/consent.tsx
Normal file
54
apps/web/src/routes/consent.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@better-auth/oauth-provider": "^1.4.19",
|
||||
"@zendegi/db": "workspace:*",
|
||||
"@zendegi/env": "workspace:*",
|
||||
"better-auth": "catalog:",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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],
|
||||
}),
|
||||
}));
|
||||
|
||||
96
pnpm-lock.yaml
generated
96
pnpm-lock.yaml
generated
@@ -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: {}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user