Compare commits

...

7 Commits

Author SHA1 Message Date
Mauricio Siu
d54aa02ad2 feat: enhance API parameter documentation with semantic hints
- Introduced semantic hints for API parameters in the endpoint catalog, providing clearer guidance on expected values and formats.
- Updated the endpoint text generation to include these hints, improving the usability of the API documentation for developers.
- Enhanced the overall clarity of parameter descriptions, facilitating better understanding and implementation of API calls.

These changes significantly improve the developer experience by making API interactions more intuitive and informative.
2026-04-12 11:55:41 -06:00
Mauricio Siu
734641b516 feat: implement embeddings for AI chat and enhance tool retrieval
- Introduced a new embeddings system for AI chat, allowing for improved context understanding and response accuracy.
- Added functionality to retrieve relevant endpoints based on user queries, enhancing the AI's ability to provide precise information.
- Updated the chat panel to restore messages from local storage and persist chat history, improving user experience.
- Enhanced error handling and added semantic hints for API parameters, ensuring clearer guidance for users.

These changes significantly improve the AI chat capabilities and overall interaction quality within the Dokploy platform.
2026-04-12 11:54:01 -06:00
Mauricio Siu
cee2e9f002 feat: enhance AI chat functionality and API integration
- Updated the AI chat panel to support multiple service types, including applications, databases, and more, improving context handling.
- Implemented local storage for chat messages, allowing users to retain their chat history.
- Enhanced API integration by adding new endpoints for reading deployment build logs and creating tools from OpenAPI specifications.
- Improved error handling and user experience in the chat interface, ensuring smoother interactions.

These changes significantly enhance the AI chat capabilities and overall user experience within the Dokploy platform.
2026-04-11 22:15:58 -06:00
Mauricio Siu
e0b4a13340 chore: remove unused import from settings router
- Removed the unused import of `tryCatch` from the settings router file, streamlining the code and improving readability.

This change contributes to cleaner code management by eliminating unnecessary dependencies.
2026-04-11 14:50:58 -06:00
Mauricio Siu
ec202c8c6e chore: update @dokploy/trpc-openapi to version 0.0.19 and enhance API documentation
- Updated the version of @dokploy/trpc-openapi from 0.0.18 to 0.0.19 in pnpm-lock.yaml and package.json.
- Added OpenAPI metadata to various procedures across multiple routers, improving API documentation and clarity for developers.
- Enhanced descriptions and summaries for several API endpoints, ensuring better understanding of their functionality.

These changes improve the overall API usability and documentation quality.
2026-04-11 14:50:28 -06:00
Mauricio Siu
7e89eaed4a Merge branch 'canary' into feat/add-chat 2026-04-11 10:14:13 -06:00
Mauricio Siu
e508f3143f feat: implement AI chat panel and logging features
- Added a new ChatPanel component for AI interactions, allowing users to chat with an AI assistant for infrastructure management.
- Integrated the ChatPanel into the dashboard layout for easy access.
- Created a new API endpoint for AI chat functionality, enabling dynamic interactions based on user context.
- Implemented log reading capabilities across various services (application, compose, libsql, mariadb, mongo, mysql, postgres, redis) to enhance troubleshooting.
- Introduced utility functions for fetching container logs, improving the overall user experience in managing deployments.

This feature enriches the user interface by providing real-time AI assistance and log analysis, streamlining operational workflows.
2026-04-09 11:19:42 -06:00
55 changed files with 37570 additions and 22883 deletions

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,629 @@
"use client";
import { useChat } from "@ai-sdk/react";
import type { ChatContext } from "@dokploy/server/utils/ai/chat-tools";
import { DefaultChatTransport } from "ai";
import {
Bot,
Check,
ChevronDown,
Loader2,
Send,
Trash2,
Wrench,
X,
} from "lucide-react";
import { useRouter } from "next/router";
import { useEffect, useMemo, useRef, useState } from "react";
import ReactMarkdown from "react-markdown";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api";
function useChatContext(): ChatContext {
const router = useRouter();
const { query, pathname } = router;
return useMemo(() => {
const projectId =
typeof query.projectId === "string" ? query.projectId : undefined;
const environmentId =
typeof query.environmentId === "string"
? query.environmentId
: undefined;
const serverId =
typeof query.serverId === "string" ? query.serverId : undefined;
const serviceParams = [
{ key: "applicationId", type: "application" },
{ key: "composeId", type: "compose" },
{ key: "postgresId", type: "postgres" },
{ key: "mysqlId", type: "mysql" },
{ key: "redisId", type: "redis" },
{ key: "mongoId", type: "mongo" },
{ key: "mariadbId", type: "mariadb" },
{ key: "libsqlId", type: "libsql" },
] as const;
for (const { key, type } of serviceParams) {
if (query[key] && typeof query[key] === "string") {
return {
type,
id: query[key] as string,
projectId,
environmentId,
serverId,
};
}
}
if (query.projectId && typeof query.projectId === "string") {
return {
type: "project" as const,
id: query.projectId,
projectId,
environmentId,
serverId,
};
}
return {
type: "general" as const,
id: "",
projectId,
environmentId,
serverId,
};
}, [
query.applicationId,
query.composeId,
query.postgresId,
query.mysqlId,
query.redisId,
query.mongoId,
query.mariadbId,
query.libsqlId,
query.projectId,
query.environmentId,
query.serverId,
pathname,
]);
}
export function ChatPanel() {
const [open, setOpen] = useState(false);
const [aiId, setAiId] = useState<string>("");
const [input, setInput] = useState("");
const scrollRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLTextAreaElement>(null);
const context = useChatContext();
const aiIdRef = useRef(aiId);
const contextRef = useRef(context);
aiIdRef.current = aiId;
contextRef.current = context;
const { data: isCloud } = api.settings.isCloud.useQuery(undefined, {
refetchOnWindowFocus: false,
});
const { data: providers } = api.ai.getEnabledProviders.useQuery(undefined, {
refetchOnWindowFocus: false,
enabled: !isCloud,
});
const enabledProviders = providers ?? [];
const STORAGE_KEY = "dokploy-chat-messages";
const restoredRef = useRef(false);
const { messages, sendMessage, status, setMessages, addToolApprovalResponse } = useChat({
id: "dokploy-chat",
transport: new DefaultChatTransport({
api: "/api/ai/chat",
body: () => ({
...(isCloud ? {} : { aiId: aiIdRef.current }),
context: contextRef.current,
}),
}),
});
const isLoading = status === "streaming" || status === "submitted";
// Restore messages from localStorage on mount
useEffect(() => {
if (restoredRef.current) return;
restoredRef.current = true;
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
const parsed = JSON.parse(stored);
if (Array.isArray(parsed) && parsed.length > 0) {
setMessages(parsed);
}
}
} catch {
// ignore
}
}, [setMessages]);
// Persist messages to localStorage
useEffect(() => {
if (!restoredRef.current) return;
if (messages.length > 0) {
try {
const toStore = messages.slice(-50);
localStorage.setItem(STORAGE_KEY, JSON.stringify(toStore));
} catch {
// localStorage full or unavailable — ignore
}
}
}, [messages]);
useEffect(() => {
if (!isCloud && !aiId && enabledProviders.length > 0 && enabledProviders[0]) {
setAiId(enabledProviders[0].aiId);
}
}, [enabledProviders, aiId, isCloud]);
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [messages, status]);
if (!isCloud && enabledProviders.length === 0) return null;
const handleSend = () => {
if (!input.trim() || isLoading) return;
if (!isCloud && !aiId) return;
sendMessage({ text: input });
setInput("");
setTimeout(() => inputRef.current?.focus(), 0);
};
const contextLabel =
context.type === "general" ? "General" : context.type;
const lastMessage = messages[messages.length - 1];
return (
<>
<Button
onClick={() => setOpen(true)}
variant="outline"
className="fixed bottom-6 right-6 z-50 h-11 w-11 rounded-full shadow-md border"
size="icon"
>
<Bot className="h-5 w-5" />
</Button>
<Sheet open={open} onOpenChange={setOpen}>
<SheetContent
side="right"
className="w-full sm:w-[480px] p-0 flex flex-col border-l outline-none"
>
<SheetHeader className="px-4 py-3 border-b shrink-0">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Bot className="h-4 w-4 text-muted-foreground" />
<SheetTitle className="text-sm font-medium">
{isCloud ? "Dokploy Agent" : "AI Assistant"}
</SheetTitle>
{isLoading && (
<span className="text-xs text-muted-foreground animate-pulse">
working...
</span>
)}
</div>
</div>
<SheetDescription className="sr-only">
Chat with AI to manage your infrastructure
</SheetDescription>
<div className="flex items-center gap-2 pt-1">
{!isCloud && (
<Select value={aiId} onValueChange={setAiId}>
<SelectTrigger className="h-8 text-xs flex-1">
<SelectValue placeholder="Select provider..." />
</SelectTrigger>
<SelectContent>
{enabledProviders.map((p) => (
<SelectItem key={p.aiId} value={p.aiId}>
{p.name} ({p.model})
</SelectItem>
))}
</SelectContent>
</Select>
)}
<Badge
variant="outline"
className="text-xs shrink-0 capitalize font-normal"
>
{contextLabel}
</Badge>
{messages.length > 0 && (
<Button
variant="ghost"
size="icon"
className="h-8 w-8 shrink-0"
onClick={() => { setMessages([]); localStorage.removeItem(STORAGE_KEY); }}
title="Clear chat"
>
<Trash2 className="h-3.5 w-3.5" />
</Button>
)}
</div>
</SheetHeader>
<div
ref={scrollRef}
className="flex-1 overflow-y-auto p-4 space-y-3"
>
{messages.length === 0 && (
<div className="flex flex-col items-center justify-center h-full text-muted-foreground gap-3">
<Bot className="h-8 w-8 opacity-30" />
<p className="text-sm text-center">
Ask me anything about your{" "}
{context.type === "general"
? "infrastructure"
: context.type}
</p>
<div className="flex flex-wrap gap-1.5 justify-center">
{(context.type === "application"
? [
"What's the status of this app?",
"Why did the last build fail?",
"Show me recent deployments",
"Redeploy this app",
]
: context.type === "compose"
? [
"Show compose service status",
"Why did the last deploy fail?",
"Show me the domains",
"Redeploy this service",
]
: context.type === "postgres" ||
context.type === "mysql" ||
context.type === "redis" ||
context.type === "mongo" ||
context.type === "mariadb" ||
context.type === "libsql"
? [
`Show ${context.type} status`,
"What's the connection info?",
"Show recent deployments",
"Restart this database",
]
: context.type === "project"
? [
"How many services do I have?",
"Show me all environments",
"Which services are failing?",
]
: [
"List all my projects",
"Show project overview",
"What servers do I have?",
]
).map((suggestion) => (
<Button
key={suggestion}
variant="outline"
size="sm"
className="text-xs h-7 font-normal"
onClick={() => setInput(suggestion)}
>
{suggestion}
</Button>
))}
</div>
</div>
)}
{messages.map((message) => {
if (message.role === "user") {
return (
<div key={message.id} className="flex justify-end">
<div className="max-w-[85%] rounded-lg px-3 py-2 text-sm bg-muted">
<p className="whitespace-pre-wrap">
{message.parts
.filter(
(p): p is { type: "text"; text: string } =>
p.type === "text",
)
.map((p) => p.text)
.join("")}
</p>
</div>
</div>
);
}
return (
<div key={message.id} className="flex justify-start">
<div className="max-w-[90%] space-y-2">
{message.parts.map((part, i) => {
if (
part.type === "text" &&
(part as { text?: string }).text?.trim()
) {
return (
<div
key={`text-${message.id}-${i}`}
className="rounded-lg border px-3 py-2 text-sm prose prose-sm dark:prose-invert max-w-none break-words"
>
<ReactMarkdown>
{(part as { text: string }).text}
</ReactMarkdown>
</div>
);
}
if (part.type === "dynamic-tool") {
return (
<div
key={part.toolCallId}
className="rounded-lg border px-3 py-2"
>
<ToolCallDisplay
toolCallId={part.toolCallId}
toolName={part.toolName}
state={part.state}
input={(part as any).input}
output={
part.state === "output-available"
? part.output
: undefined
}
onApprove={(id) =>
addToolApprovalResponse({
id,
approved: true,
})
}
onDeny={(id) =>
addToolApprovalResponse({
id,
approved: false,
reason: "User denied",
})
}
/>
</div>
);
}
if (part.type === "reasoning") {
return (
<Collapsible key={`reasoning-${message.id}-${i}`}>
<CollapsibleTrigger asChild>
<button
type="button"
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
>
<Bot className="h-3 w-3" />
<span>Thinking...</span>
<ChevronDown className="h-3 w-3" />
</button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="mt-1 rounded-lg border px-3 py-2 text-xs text-muted-foreground italic">
{(part as any).text ||
(part as any).reasoning}
</div>
</CollapsibleContent>
</Collapsible>
);
}
return null;
})}
</div>
</div>
);
})}
{isLoading && lastMessage?.role === "user" && (
<div className="flex justify-start">
<div className="rounded-lg border px-3 py-2 flex items-center gap-2 text-xs text-muted-foreground">
<Loader2 className="h-3 w-3 animate-spin" />
Investigating...
</div>
</div>
)}
</div>
<div className="border-t p-3 shrink-0 flex gap-2">
<Textarea
ref={inputRef}
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder={
!isCloud && !aiId
? "Select a provider first..."
: "Ask anything..."
}
disabled={(!isCloud && !aiId) || isLoading}
className="min-h-[40px] max-h-[120px] resize-none text-sm"
rows={1}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSend();
}
}}
/>
<Button
type="button"
size="icon"
variant="outline"
disabled={
(!isCloud && !aiId) || !input.trim() || isLoading
}
className="shrink-0 h-10 w-10"
onClick={handleSend}
>
<Send className="h-4 w-4" />
</Button>
</div>
</SheetContent>
</Sheet>
</>
);
}
function ToolCallDisplay({
toolCallId,
toolName,
state,
input,
output,
onApprove,
onDeny,
}: {
toolCallId: string;
toolName: string;
state: string;
input?: unknown;
output?: unknown;
onApprove?: (id: string) => void;
onDeny?: (id: string) => void;
}) {
const [isOpen, setIsOpen] = useState(false);
const isRunning =
state === "input-streaming" || state === "input-available";
const isDone = state === "output-available";
const isError = state === "output-error";
const needsApproval = state === "requires-approval";
const outputText = output
? typeof output === "string"
? output
: JSON.stringify(output, null, 2)
: null;
// Extract operationId and params from input
const inputData = input as { operationId?: string; params?: Record<string, unknown> } | undefined;
const operationId = inputData?.operationId;
const params = inputData?.params;
// Format: "compose-one" → "compose → one"
const displayLabel = operationId
? operationId.replace("-", " → ")
: toolName;
// Determine HTTP method hint from operationId
const isReadOp = operationId?.match(/^(.*-)?(one|all|get|list|read|search|by)/i);
const StatusIcon = isRunning
? () => <Loader2 className="h-3.5 w-3.5 animate-spin text-blue-500 shrink-0" />
: isDone
? () => <Check className="h-3.5 w-3.5 text-green-500 shrink-0" />
: isError
? () => <X className="h-3.5 w-3.5 text-red-500 shrink-0" />
: () => <Wrench className="h-3.5 w-3.5 text-muted-foreground shrink-0" />;
if (needsApproval) {
return (
<div className="space-y-2">
<div className="flex items-center gap-2 text-xs">
<Wrench className="h-3.5 w-3.5 text-yellow-500 shrink-0" />
<code className="font-mono text-xs font-medium">{displayLabel}</code>
<Badge variant="outline" className="text-[10px] px-1 py-0 h-4 font-normal">
write
</Badge>
</div>
{params && Object.keys(params).length > 0 && (
<div className="ml-5.5 flex flex-wrap gap-1">
{Object.entries(params).map(([key, value]) => (
<span key={key} className="text-[10px] bg-muted px-1.5 py-0.5 rounded font-mono">
{key}={typeof value === "string" ? `"${value}"` : String(value)}
</span>
))}
</div>
)}
<div className="flex gap-1.5 ml-5.5">
<Button
variant="outline"
size="sm"
className="h-6 text-xs px-2"
onClick={() => onApprove?.(toolCallId)}
>
<Check className="h-3 w-3 mr-1" />
Approve
</Button>
<Button
variant="ghost"
size="sm"
className="h-6 text-xs px-2"
onClick={() => onDeny?.(toolCallId)}
>
<X className="h-3 w-3 mr-1" />
Deny
</Button>
</div>
</div>
);
}
return (
<div className="space-y-1">
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
<CollapsibleTrigger asChild>
<button
type="button"
className="flex items-center gap-2 text-xs w-full hover:bg-muted/50 rounded -mx-1 px-1 py-0.5 transition-colors"
>
<StatusIcon />
<code className="font-mono text-xs font-medium">{displayLabel}</code>
{isReadOp && (
<Badge variant="secondary" className="text-[10px] px-1 py-0 h-4 font-normal">
read
</Badge>
)}
{params && Object.keys(params).length > 0 && (
<span className="text-[10px] text-muted-foreground truncate">
{Object.entries(params)
.slice(0, 3)
.map(([k, v]) => `${k}=${typeof v === "string" ? `"${String(v).slice(0, 20)}"` : String(v)}`)
.join(", ")}
{Object.keys(params).length > 3 ? ` +${Object.keys(params).length - 3}` : ""}
</span>
)}
{(outputText || isRunning) && (
<ChevronDown
className={`h-3 w-3 ml-auto text-muted-foreground transition-transform shrink-0 ${isOpen ? "rotate-180" : ""}`}
/>
)}
</button>
</CollapsibleTrigger>
{outputText && (
<CollapsibleContent>
<pre className="mt-1 ml-5.5 p-2 bg-muted/50 rounded text-[10px] overflow-x-auto max-h-[200px] overflow-y-auto leading-tight whitespace-pre-wrap break-words">
{outputText}
</pre>
</CollapsibleContent>
)}
</Collapsible>
</div>
);
}

View File

@@ -1,4 +1,5 @@
import { api } from "@/utils/api";
import { ChatPanel } from "../dashboard/ai-chat/chat-panel";
import { ImpersonationBar } from "../dashboard/impersonation/impersonation-bar";
import { HubSpotWidget } from "../shared/HubSpotWidget";
import Page from "./side";
@@ -23,6 +24,7 @@ export const DashboardLayout = ({ children }: Props) => {
return (
<>
<Page>{children}</Page>
<ChatPanel />
{isChatEnabled && (
<>
<HubSpotWidget />

View File

@@ -46,6 +46,7 @@
"@ai-sdk/mistral": "^3.0.20",
"@ai-sdk/openai": "^3.0.29",
"@ai-sdk/openai-compatible": "^2.0.30",
"@ai-sdk/react": "^3.0.156",
"@better-auth/api-key": "1.5.4",
"@better-auth/sso": "1.5.4",
"@codemirror/autocomplete": "^6.18.6",
@@ -57,7 +58,7 @@
"@codemirror/search": "^6.6.0",
"@codemirror/view": "^6.39.15",
"@dokploy/server": "workspace:*",
"@dokploy/trpc-openapi": "0.0.18",
"@dokploy/trpc-openapi": "0.0.19",
"@faker-js/faker": "^8.4.1",
"@hookform/resolvers": "^5.2.2",
"@octokit/auth-app": "^6.1.3",

View File

@@ -0,0 +1,251 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { IS_CLOUD, validateRequest } from "@dokploy/server";
import { getAiSettingById } from "@dokploy/server/services/ai";
import {
type ChatContext,
getAllTools,
} from "@dokploy/server/utils/ai/chat-tools";
import {
buildEndpointCatalog,
createApiTool,
} from "@dokploy/server/utils/ai/api-tool";
import {
getOrCreateEmbeddings,
retrieveRelevantEndpoints,
} from "@dokploy/server/utils/ai/tool-retrieval";
import { selectAIProvider } from "@dokploy/server/utils/ai/select-ai-provider";
import { createAnthropic } from "@ai-sdk/anthropic";
import { convertToModelMessages, stepCountIs, streamText } from "ai";
import type { NextApiRequest, NextApiResponse } from "next";
let cachedSpec: any = null;
function getOpenApiSpec() {
if (!cachedSpec) {
try {
const specPath = join(process.cwd(), "../../openapi.json");
cachedSpec = JSON.parse(readFileSync(specPath, "utf-8"));
} catch {
cachedSpec = null;
}
}
return cachedSpec;
}
function buildContextBlock(context: ChatContext): string {
if (context.type === "general") {
return "CONTEXT: The user is on the general dashboard (no specific resource selected). Use project-all to list their projects if needed.";
}
const lines: string[] = [];
lines.push(
`CONTEXT: The user is currently viewing a specific ${context.type}. The ${context.type}Id is "${context.id}".`,
);
lines.push(
`When the user says "this app", "this service", "this database", "add env var", etc., they ALWAYS mean this ${context.type} (ID: "${context.id}"). NEVER ask which service they mean.`,
);
if (context.projectId) {
lines.push(`- projectId: "${context.projectId}"`);
}
if (context.environmentId) {
lines.push(`- environmentId: "${context.environmentId}"`);
}
if (context.serverId) {
lines.push(`- serverId: "${context.serverId}"`);
}
lines.push(
"Use these IDs directly when calling tools — do NOT ask the user for them. You already know exactly which resource the user is talking about.",
);
return lines.join("\n");
}
function buildSystemPrompt(context: ChatContext, catalog: string | null, endpointCount?: number) {
const contextBlock = buildContextBlock(context);
return `You are an autonomous DevOps agent inside Dokploy (Docker-based PaaS). You take action immediately — you don't explain, you don't ask, you DO.
${contextBlock}
THINKING PROCESS (do this before EVERY action):
1. Scan ALL section headers (## tag — description) in the ENDPOINT CATALOG to find which sections are relevant
2. Read the endpoint descriptions in those sections to pick the right operationId
3. Call the endpoint with the correct params — use the IDs from the context above
BEHAVIOR:
- When the user asks you to do something → DO IT. Call the API right away.
- When you need information → call the endpoint to get it. Never say "I can't access" or "I don't have the ability to".
- When something fails → read the error, figure out the fix, and apply it. Don't stop to explain the error — fix it.
- EVERY capability you need is in the ENDPOINT CATALOG below. If you think you can't do something, you're wrong — scan ALL sections again.
- You already have all the IDs you need from the context above. NEVER ask the user for IDs, paths, or information you can discover by calling endpoints.
- NEVER ask for confirmation or permission. The only exception is deleting a service entirely. For everything else (read, update, deploy, stop, start, restart) → just do it immediately.
KEY PATTERN: When you need to explore files, find paths, or check repository structure → use the "patch" section endpoints to browse directories and read files. NEVER ask the user for file paths.
DATA MODEL: Project → Environment → Services (application, compose, postgres, mysql, redis, mongo, mariadb, libsql). Each service has deployments with build logs.
TOOL: You have one tool "call_api". Pass operationId + params from the catalog.
- ALWAYS pass required params (*) in the "params" object. Example: { "operationId": "domain-byComposeId", "params": { "composeId": "abc123" } }
- Params: * = required, ? = optional, [a|b|c] = allowed values
- GET = read-only (auto-executed). POST/PUT/DELETE = write (user approves).
- If a call fails, read the error message and fix the params. NEVER retry the same call with the same params.
RESPONSE STYLE:
- 2-3 sentences max. No walls of text.
- Never explain limitations — find the right endpoint and act.
- Answer in the user's language.
${catalog ? `ENDPOINT CATALOG (${endpointCount} endpoints):\n${catalog}` : ""}`;
}
function getUserMessages(messages: any[]): string {
const texts: string[] = [];
for (const msg of messages) {
if (msg.role !== "user") continue;
if (typeof msg.content === "string") {
texts.push(msg.content);
} else if (Array.isArray(msg.content)) {
texts.push(
msg.content
.filter((p: any) => p.type === "text")
.map((p: any) => p.text)
.join(" "),
);
}
}
return texts.slice(-3).join(". ");
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}
try {
const { session, user } = await validateRequest(req);
if (!user || !session) {
return res.status(401).json({ error: "Unauthorized" });
}
const body = req.body;
const messages = body.messages;
const aiId = body.aiId;
const context = (body.context as ChatContext) || {
type: "general" as const,
id: "",
};
// ─── Resolve model ────────────────────────────────────────
let model: any;
if (IS_CLOUD && process.env.CLOUD_ANTHROPIC_API_KEY) {
const anthropic = createAnthropic({
apiKey: process.env.CLOUD_ANTHROPIC_API_KEY,
});
model = anthropic("claude-haiku-4-5-20251001");
} else {
if (!aiId || !messages) {
return res
.status(400)
.json({ error: "Missing aiId or messages" });
}
const aiSettings = await getAiSettingById(aiId);
if (!aiSettings || !aiSettings.isEnabled) {
return res
.status(400)
.json({ error: "AI provider not enabled" });
}
const provider = selectAIProvider(aiSettings);
model = provider(aiSettings.model);
}
if (!messages) {
return res.status(400).json({ error: "Missing messages" });
}
// ─── Resolve tools ────────────────────────────────────────
const protocol = req.headers["x-forwarded-proto"] || "http";
const host = req.headers.host || "localhost:3000";
const toolConfig = {
baseUrl: `${protocol}://${host}/api`,
cookie: req.headers.cookie || "",
};
let tools: Record<string, any>;
let catalogText: string | null = null;
let endpointCount = 0;
const spec = getOpenApiSpec();
if (spec) {
const voyageApiKey = process.env.VOYAGE_API_KEY;
if (!voyageApiKey) {
return res.status(400).json({ error: "VOYAGE_API_KEY is required" });
}
const embeddingsPath = join(process.cwd(), ".tool-embeddings.json");
const allEmbeddings = await getOrCreateEmbeddings(
spec,
voyageApiKey,
embeddingsPath,
);
const userQuery = getUserMessages(messages).trim();
const { operationIds: tagFilteredIds } = buildEndpointCatalog(spec, context.type);
let relevantIds: Set<string> | undefined;
if (userQuery && allEmbeddings.length > 0) {
const topIds = await retrieveRelevantEndpoints(
userQuery,
allEmbeddings,
voyageApiKey,
{ allowedOperationIds: tagFilteredIds, topK: 25 },
);
if (topIds.length > 0) {
relevantIds = new Set(topIds);
}
}
const { catalog, count, operationIds } = buildEndpointCatalog(
spec,
context.type,
relevantIds,
);
catalogText = catalog;
endpointCount = count;
tools = createApiTool(spec, toolConfig, operationIds, 8000);
} else {
tools = getAllTools(context, toolConfig);
}
// ─── Stream response ──────────────────────────────────────
const modelMessages = await convertToModelMessages(messages);
const result = streamText({
model,
system: buildSystemPrompt(context, catalogText, endpointCount),
messages: modelMessages,
tools,
stopWhen: stepCountIs(12),
});
// Disable buffering for streaming
res.setHeader("X-Accel-Buffering", "no");
res.setHeader("Cache-Control", "no-cache, no-transform");
result.pipeUIMessageStreamToResponse(res);
} catch (error) {
console.error("AI chat error:", error);
return res.status(500).json({
error:
error instanceof Error ? error.message : "Internal server error",
});
}
}

View File

@@ -10,6 +10,12 @@ import { adminProcedure, createTRPCRouter } from "../trpc";
export const adminRouter = createTRPCRouter({
setupMonitoring: adminProcedure
.meta({
openapi: {
summary: "Update web server monitoring settings",
description: "Update the monitoring configuration for the web server including refresh rates, thresholds, and container services. Restarts the monitoring system and returns the updated settings. Disabled on cloud.",
},
})
.input(apiUpdateWebServerMonitoring)
.mutation(async ({ input }) => {
try {

View File

@@ -41,12 +41,24 @@ import { generatePassword } from "@/templates/utils";
export const aiRouter = createTRPCRouter({
one: adminProcedure
.meta({
openapi: {
summary: "Get AI settings by ID",
description: "Returns a single AI provider configuration by its ID.",
},
})
.input(z.object({ aiId: z.string() }))
.query(async ({ input }) => {
return await getAiSettingById(input.aiId);
}),
getModels: protectedProcedure
.meta({
openapi: {
summary: "List available AI models",
description: "Fetches the list of models from the given AI provider URL. Supports OpenAI-compatible, Ollama, Gemini, Perplexity, ZAI, and MiniMax providers.",
},
})
.input(z.object({ apiUrl: z.string().min(1), apiKey: z.string() }))
.query(async ({ input }) => {
try {
@@ -174,33 +186,75 @@ export const aiRouter = createTRPCRouter({
});
}
}),
create: adminProcedure.input(apiCreateAi).mutation(async ({ ctx, input }) => {
create: adminProcedure
.meta({
openapi: {
summary: "Create AI provider",
description: "Saves a new AI provider configuration (API URL, key, model) for the current organization.",
},
})
.input(apiCreateAi)
.mutation(async ({ ctx, input }) => {
return await saveAiSettings(ctx.session.activeOrganizationId, input);
}),
update: adminProcedure.input(apiUpdateAi).mutation(async ({ ctx, input }) => {
update: adminProcedure
.meta({
openapi: {
summary: "Update AI provider",
description: "Updates an existing AI provider configuration for the current organization.",
},
})
.input(apiUpdateAi)
.mutation(async ({ ctx, input }) => {
return await saveAiSettings(ctx.session.activeOrganizationId, input);
}),
getAll: adminProcedure.query(async ({ ctx }) => {
getAll: adminProcedure
.meta({
openapi: {
summary: "List all AI providers",
description: "Returns all AI provider configurations for the current organization.",
},
})
.query(async ({ ctx }) => {
return await getAiSettingsByOrganizationId(
ctx.session.activeOrganizationId,
);
}),
get: adminProcedure
.meta({
openapi: {
summary: "Get AI provider",
description: "Returns a single AI provider configuration by its ID.",
},
})
.input(z.object({ aiId: z.string() }))
.query(async ({ input }) => {
return await getAiSettingById(input.aiId);
}),
delete: adminProcedure
.meta({
openapi: {
summary: "Delete AI provider",
description: "Removes an AI provider configuration by its ID.",
},
})
.input(z.object({ aiId: z.string() }))
.mutation(async ({ input }) => {
return await deleteAiSettings(input.aiId);
}),
getEnabledProviders: protectedProcedure.query(async ({ ctx }) => {
getEnabledProviders: protectedProcedure
.meta({
openapi: {
summary: "List enabled AI providers",
description: "Returns a lightweight list of enabled AI providers (ID, name, model) for the current organization, suitable for dropdown selectors.",
},
})
.query(async ({ ctx }) => {
const settings = await getAiSettingsByOrganizationId(
ctx.session.activeOrganizationId,
);
@@ -210,6 +264,12 @@ export const aiRouter = createTRPCRouter({
}),
analyzeLogs: protectedProcedure
.meta({
openapi: {
summary: "Analyze logs with AI",
description: "Sends build or runtime logs to the specified AI provider for analysis. Returns a summary of issues found, root causes, and suggested fixes.",
},
})
.input(
z.object({
aiId: z.string().min(1),
@@ -268,6 +328,12 @@ ${input.logs}`,
}),
testConnection: protectedProcedure
.meta({
openapi: {
summary: "Test AI provider connection",
description: "Sends a minimal prompt to the specified AI provider and model to verify the API URL, key, and model are valid and reachable.",
},
})
.input(
z.object({
apiUrl: z.string().min(1),
@@ -302,6 +368,12 @@ ${input.logs}`,
}),
suggest: protectedProcedure
.meta({
openapi: {
summary: "Suggest deployment variants",
description: "Uses AI to generate deployment configuration suggestions (docker-compose variants) based on the user's input prompt.",
},
})
.input(
z.object({
aiId: z.string(),
@@ -323,6 +395,12 @@ ${input.logs}`,
}
}),
deploy: protectedProcedure
.meta({
openapi: {
summary: "Deploy AI suggestion",
description: "Deploys an AI-generated suggestion by creating a compose service with its docker-compose file, environment variables, domains, and config file mounts.",
},
})
.input(deploySuggestionSchema)
.mutation(async ({ ctx, input }) => {
const environment = await findEnvironmentById(input.environmentId);

View File

@@ -79,6 +79,12 @@ import { cancelDeployment, deploy } from "@/server/utils/deploy";
export const applicationRouter = createTRPCRouter({
create: protectedProcedure
.meta({
openapi: {
summary: "Create an application",
description: "Creates a new application in the specified project environment. Supports GitHub, GitLab, Bitbucket, Git, Docker image, and drop sources.",
},
})
.input(apiCreateApplication)
.mutation(async ({ input, ctx }) => {
try {
@@ -134,6 +140,12 @@ export const applicationRouter = createTRPCRouter({
}
}),
one: protectedProcedure
.meta({
openapi: {
summary: "Get an application",
description: "Retrieves detailed information about an application by its ID, including git provider access status and deployment configuration.",
},
})
.input(apiFindOneApplication)
.query(async ({ input, ctx }) => {
await checkServiceAccess(ctx, input.applicationId, "read");
@@ -189,6 +201,12 @@ export const applicationRouter = createTRPCRouter({
}),
reload: protectedProcedure
.meta({
openapi: {
summary: "Reload an application",
description: "Restarts the Docker container for the application by mechanizing it. Resets the application status to idle, then to done on success or error on failure.",
},
})
.input(apiReloadApplication)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -218,6 +236,12 @@ export const applicationRouter = createTRPCRouter({
}),
delete: protectedProcedure
.meta({
openapi: {
summary: "Delete an application",
description: "Permanently deletes an application and cleans up all associated resources including Docker services, Traefik configuration, deployments, middlewares, and source code.",
},
})
.input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => {
await checkServiceAccess(ctx, input.applicationId, "delete");
@@ -279,6 +303,12 @@ export const applicationRouter = createTRPCRouter({
}),
stop: protectedProcedure
.meta({
openapi: {
summary: "Stop an application",
description: "Stops the running Docker service for the application and sets its status to idle.",
},
})
.input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -301,6 +331,12 @@ export const applicationRouter = createTRPCRouter({
}),
start: protectedProcedure
.meta({
openapi: {
summary: "Start an application",
description: "Starts the Docker service for the application and sets its status to done.",
},
})
.input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -323,6 +359,12 @@ export const applicationRouter = createTRPCRouter({
}),
redeploy: protectedProcedure
.meta({
openapi: {
summary: "Redeploy an application",
description: "Triggers a rebuild and redeployment of the application. Queues a deployment job or executes it directly for cloud servers.",
},
})
.input(apiRedeployApplication)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -367,6 +409,12 @@ export const applicationRouter = createTRPCRouter({
});
}),
saveEnvironment: protectedProcedure
.meta({
openapi: {
summary: "Save environment variables",
description: "Updates the environment variables, build arguments, and build secrets for an application.",
},
})
.input(apiSaveEnvironmentVariables)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -388,6 +436,12 @@ export const applicationRouter = createTRPCRouter({
return true;
}),
saveBuildType: protectedProcedure
.meta({
openapi: {
summary: "Save build type configuration",
description: "Updates the build type and related settings for an application, including Dockerfile path, build context, publish directory, and build stage.",
},
})
.input(apiSaveBuildType)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -413,6 +467,12 @@ export const applicationRouter = createTRPCRouter({
return true;
}),
saveGithubProvider: protectedProcedure
.meta({
openapi: {
summary: "Save GitHub provider",
description: "Configures the application to use a GitHub repository as its source, setting the repository, branch, owner, and build path.",
},
})
.input(apiSaveGithubProvider)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -440,6 +500,12 @@ export const applicationRouter = createTRPCRouter({
return true;
}),
saveGitlabProvider: protectedProcedure
.meta({
openapi: {
summary: "Save GitLab provider",
description: "Configures the application to use a GitLab repository as its source, setting the repository, branch, owner, build path, and project ID.",
},
})
.input(apiSaveGitlabProvider)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -468,6 +534,12 @@ export const applicationRouter = createTRPCRouter({
return true;
}),
saveBitbucketProvider: protectedProcedure
.meta({
openapi: {
summary: "Save Bitbucket provider",
description: "Configures the application to use a Bitbucket repository as its source, setting the repository, branch, owner, and build path.",
},
})
.input(apiSaveBitbucketProvider)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -495,6 +567,12 @@ export const applicationRouter = createTRPCRouter({
return true;
}),
saveGiteaProvider: protectedProcedure
.meta({
openapi: {
summary: "Save Gitea provider",
description: "Configures the application to use a Gitea repository as its source, setting the repository, branch, owner, and build path.",
},
})
.input(apiSaveGiteaProvider)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -521,6 +599,12 @@ export const applicationRouter = createTRPCRouter({
return true;
}),
saveDockerProvider: protectedProcedure
.meta({
openapi: {
summary: "Save Docker provider",
description: "Configures the application to use a Docker image as its source, setting the image name, registry URL, and optional credentials.",
},
})
.input(apiSaveDockerProvider)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -544,6 +628,12 @@ export const applicationRouter = createTRPCRouter({
return true;
}),
saveGitProvider: protectedProcedure
.meta({
openapi: {
summary: "Save Git provider",
description: "Configures the application to use a custom Git repository URL as its source, with optional SSH key authentication.",
},
})
.input(apiSaveGitProvider)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -569,6 +659,12 @@ export const applicationRouter = createTRPCRouter({
return true;
}),
disconnectGitProvider: protectedProcedure
.meta({
openapi: {
summary: "Disconnect git provider",
description: "Removes all git provider configuration from the application, resetting source type to default and clearing repository, branch, and owner fields for all providers.",
},
})
.input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -622,6 +718,12 @@ export const applicationRouter = createTRPCRouter({
return true;
}),
markRunning: protectedProcedure
.meta({
openapi: {
summary: "Mark application as running",
description: "Sets the application status to running. Used to indicate that a deployment is in progress.",
},
})
.input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -637,6 +739,12 @@ export const applicationRouter = createTRPCRouter({
});
}),
update: protectedProcedure
.meta({
openapi: {
summary: "Update an application",
description: "Updates the general configuration of an application such as name, description, memory limits, CPU limits, and other settings.",
},
})
.input(apiUpdateApplication)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -673,6 +781,12 @@ export const applicationRouter = createTRPCRouter({
return true;
}),
refreshToken: protectedProcedure
.meta({
openapi: {
summary: "Refresh deploy token",
description: "Regenerates the webhook refresh token for the application, invalidating the previous token used for triggering deployments.",
},
})
.input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -691,6 +805,12 @@ export const applicationRouter = createTRPCRouter({
return true;
}),
deploy: protectedProcedure
.meta({
openapi: {
summary: "Deploy an application",
description: "Triggers a new deployment for the application. Queues a deployment job or executes it directly for cloud servers.",
},
})
.input(apiDeployApplication)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -735,6 +855,12 @@ export const applicationRouter = createTRPCRouter({
}),
cleanQueues: protectedProcedure
.meta({
openapi: {
summary: "Clean deployment queues",
description: "Removes all pending deployment jobs from the queue for the specified application.",
},
})
.input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -743,6 +869,12 @@ export const applicationRouter = createTRPCRouter({
await cleanQueuesByApplication(input.applicationId);
}),
clearDeployments: protectedProcedure
.meta({
openapi: {
summary: "Clear old deployments",
description: "Removes old deployment logs and artifacts for the application to free up disk space.",
},
})
.input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -759,6 +891,12 @@ export const applicationRouter = createTRPCRouter({
return true;
}),
killBuild: protectedProcedure
.meta({
openapi: {
summary: "Kill active build",
description: "Forcefully terminates the currently running Docker build process for the application.",
},
})
.input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -774,6 +912,12 @@ export const applicationRouter = createTRPCRouter({
});
}),
readTraefikConfig: protectedProcedure
.meta({
openapi: {
summary: "Read Traefik configuration",
description: "Reads the current Traefik reverse proxy configuration file for the application. Supports both local and remote server configurations.",
},
})
.input(apiFindOneApplication)
.query(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -793,6 +937,12 @@ export const applicationRouter = createTRPCRouter({
}),
dropDeployment: protectedProcedure
.meta({
openapi: {
summary: "Deploy from zip upload",
description: "Deploys an application from an uploaded zip file. Unzips the file into the application directory and triggers a deployment.",
},
})
.input(
zfd.formData({
applicationId: z.string(),
@@ -849,6 +999,12 @@ export const applicationRouter = createTRPCRouter({
return true;
}),
updateTraefikConfig: protectedProcedure
.meta({
openapi: {
summary: "Update Traefik configuration",
description: "Writes a new Traefik reverse proxy configuration for the application. Supports both local and remote server configurations.",
},
})
.input(z.object({ applicationId: z.string(), traefikConfig: z.string() }))
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -873,6 +1029,12 @@ export const applicationRouter = createTRPCRouter({
return true;
}),
readAppMonitoring: withPermission("monitoring", "read")
.meta({
openapi: {
summary: "Read application monitoring stats",
description: "Retrieves CPU and memory monitoring statistics for the application. Only available in self-hosted mode.",
},
})
.input(apiFindMonitoringStats)
.query(async ({ input }) => {
if (IS_CLOUD) {
@@ -886,6 +1048,12 @@ export const applicationRouter = createTRPCRouter({
return stats;
}),
move: protectedProcedure
.meta({
openapi: {
summary: "Move application to another environment",
description: "Moves an application to a different environment within the same project or to another project's environment.",
},
})
.input(
z.object({
applicationId: z.string(),
@@ -922,6 +1090,12 @@ export const applicationRouter = createTRPCRouter({
}),
cancelDeployment: protectedProcedure
.meta({
openapi: {
summary: "Cancel a deployment",
description: "Cancels an in-progress deployment for the application and resets its status to idle. Only available in cloud version.",
},
})
.input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -972,6 +1146,12 @@ export const applicationRouter = createTRPCRouter({
}),
search: protectedProcedure
.meta({
openapi: {
summary: "Search applications",
description: "Searches applications by name, appName, description, repository, owner, or Docker image with pagination. Respects service-level access control.",
},
})
.input(
z.object({
q: z.string().optional(),
@@ -1104,6 +1284,12 @@ export const applicationRouter = createTRPCRouter({
}),
readLogs: protectedProcedure
.meta({
openapi: {
summary: "Read application logs",
description: "Retrieves Docker container logs for the application with configurable tail length, time range, and optional text search filtering.",
},
})
.input(
apiFindOneApplication.extend({
tail: z.number().int().min(1).max(10000).default(100),

View File

@@ -78,6 +78,12 @@ interface RcloneFile {
export const backupRouter = createTRPCRouter({
create: protectedProcedure
.meta({
openapi: {
summary: "Create a backup",
description: "Creates a new backup configuration for a database or compose service. If enabled, automatically schedules the backup according to the provided cron expression.",
},
})
.input(apiCreateBackup)
.mutation(async ({ input, ctx }) => {
try {
@@ -152,6 +158,12 @@ export const backupRouter = createTRPCRouter({
}
}),
one: protectedProcedure
.meta({
openapi: {
summary: "Get a backup",
description: "Returns the details of a specific backup configuration by its ID.",
},
})
.input(apiFindOneBackup)
.query(async ({ input, ctx }) => {
const backup = await findBackupById(input.backupId);
@@ -172,6 +184,12 @@ export const backupRouter = createTRPCRouter({
return backup;
}),
update: protectedProcedure
.meta({
openapi: {
summary: "Update a backup",
description: "Updates an existing backup configuration. Reschedules or removes the backup job depending on the enabled state.",
},
})
.input(apiUpdateBackup)
.mutation(async ({ input, ctx }) => {
try {
@@ -229,6 +247,12 @@ export const backupRouter = createTRPCRouter({
}
}),
remove: protectedProcedure
.meta({
openapi: {
summary: "Delete a backup",
description: "Permanently removes a backup configuration and unschedules any associated backup job.",
},
})
.input(apiRemoveBackup)
.mutation(async ({ input, ctx }) => {
try {
@@ -272,6 +296,12 @@ export const backupRouter = createTRPCRouter({
}
}),
manualBackupPostgres: protectedProcedure
.meta({
openapi: {
summary: "Run a PostgreSQL backup manually",
description: "Immediately executes a PostgreSQL backup using the specified backup configuration. Cleans up old backups according to retention settings.",
},
})
.input(apiFindOneBackup)
.mutation(async ({ input, ctx }) => {
try {
@@ -303,6 +333,12 @@ export const backupRouter = createTRPCRouter({
}),
manualBackupMySql: protectedProcedure
.meta({
openapi: {
summary: "Run a MySQL backup manually",
description: "Immediately executes a MySQL backup using the specified backup configuration. Cleans up old backups according to retention settings.",
},
})
.input(apiFindOneBackup)
.mutation(async ({ input, ctx }) => {
try {
@@ -330,6 +366,12 @@ export const backupRouter = createTRPCRouter({
}
}),
manualBackupMariadb: protectedProcedure
.meta({
openapi: {
summary: "Run a MariaDB backup manually",
description: "Immediately executes a MariaDB backup using the specified backup configuration. Cleans up old backups according to retention settings.",
},
})
.input(apiFindOneBackup)
.mutation(async ({ input, ctx }) => {
try {
@@ -357,6 +399,12 @@ export const backupRouter = createTRPCRouter({
}
}),
manualBackupCompose: protectedProcedure
.meta({
openapi: {
summary: "Run a Compose backup manually",
description: "Immediately executes a Compose service backup using the specified backup configuration. Cleans up old backups according to retention settings.",
},
})
.input(apiFindOneBackup)
.mutation(async ({ input, ctx }) => {
try {
@@ -384,6 +432,12 @@ export const backupRouter = createTRPCRouter({
}
}),
manualBackupMongo: protectedProcedure
.meta({
openapi: {
summary: "Run a MongoDB backup manually",
description: "Immediately executes a MongoDB backup using the specified backup configuration. Cleans up old backups according to retention settings.",
},
})
.input(apiFindOneBackup)
.mutation(async ({ input, ctx }) => {
try {
@@ -411,6 +465,12 @@ export const backupRouter = createTRPCRouter({
}
}),
manualBackupLibsql: protectedProcedure
.meta({
openapi: {
summary: "Run a LibSQL backup manually",
description: "Immediately executes a LibSQL backup using the specified backup configuration. Cleans up old backups according to retention settings.",
},
})
.input(apiFindOneBackup)
.mutation(async ({ input, ctx }) => {
try {
@@ -438,6 +498,12 @@ export const backupRouter = createTRPCRouter({
}
}),
manualBackupWebServer: withPermission("backup", "create")
.meta({
openapi: {
summary: "Run a web server backup manually",
description: "Immediately executes a web server backup using the specified backup configuration. Cleans up old backups according to retention settings.",
},
})
.input(apiFindOneBackup)
.mutation(async ({ input, ctx }) => {
const backup = await findBackupById(input.backupId);
@@ -451,6 +517,12 @@ export const backupRouter = createTRPCRouter({
return true;
}),
listBackupFiles: withPermission("backup", "read")
.meta({
openapi: {
summary: "List backup files in S3",
description: "Lists backup files stored in the S3 destination bucket. Supports searching by path prefix and returns up to 100 results.",
},
})
.input(
z.object({
destinationId: z.string(),

View File

@@ -25,6 +25,12 @@ import {
export const bitbucketRouter = createTRPCRouter({
create: withPermission("gitProviders", "create")
.meta({
openapi: {
summary: "Create Bitbucket provider",
description: "Creates a new Bitbucket provider configuration linked to the active organization. Requires gitProviders create permission.",
},
})
.input(apiCreateBitbucket)
.mutation(async ({ input, ctx }) => {
try {
@@ -50,11 +56,24 @@ export const bitbucketRouter = createTRPCRouter({
}
}),
one: protectedProcedure
.meta({
openapi: {
summary: "Get Bitbucket provider",
description: "Returns a single Bitbucket provider configuration by its ID.",
},
})
.input(apiFindOneBitbucket)
.query(async ({ input }) => {
return await findBitbucketById(input.bitbucketId);
}),
bitbucketProviders: protectedProcedure.query(async ({ ctx }) => {
bitbucketProviders: protectedProcedure
.meta({
openapi: {
summary: "List Bitbucket providers",
description: "Returns all Bitbucket providers accessible to the current user within the active organization.",
},
})
.query(async ({ ctx }) => {
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
let result = await db.query.bitbucket.findMany({
@@ -77,16 +96,34 @@ export const bitbucketRouter = createTRPCRouter({
}),
getBitbucketRepositories: protectedProcedure
.meta({
openapi: {
summary: "List Bitbucket repositories",
description: "Fetches the list of repositories accessible by the Bitbucket provider. Calls the Bitbucket API using the provider's credentials.",
},
})
.input(apiFindOneBitbucket)
.query(async ({ input }) => {
return await getBitbucketRepositories(input.bitbucketId);
}),
getBitbucketBranches: protectedProcedure
.meta({
openapi: {
summary: "List Bitbucket branches",
description: "Fetches the list of branches for a specific Bitbucket repository. Calls the Bitbucket API using the provider's credentials.",
},
})
.input(apiFindBitbucketBranches)
.query(async ({ input }) => {
return await getBitbucketBranches(input);
}),
testConnection: protectedProcedure
.meta({
openapi: {
summary: "Test Bitbucket connection",
description: "Tests the connection to a Bitbucket provider by attempting to fetch its repositories. Returns the number of repositories found or throws an error on failure.",
},
})
.input(apiBitbucketTestConnection)
.mutation(async ({ input }) => {
try {
@@ -101,6 +138,12 @@ export const bitbucketRouter = createTRPCRouter({
}
}),
update: withPermission("gitProviders", "create")
.meta({
openapi: {
summary: "Update Bitbucket provider",
description: "Updates a Bitbucket provider configuration. Requires gitProviders create permission.",
},
})
.input(apiUpdateBitbucket)
.mutation(async ({ input, ctx }) => {
const result = await updateBitbucket(input.bitbucketId, {

View File

@@ -19,6 +19,12 @@ import {
export const certificateRouter = createTRPCRouter({
create: withPermission("certificate", "create")
.meta({
openapi: {
summary: "Create a certificate",
description: "Creates a new SSL/TLS certificate. In cloud mode, a server must be specified. Logs an audit entry upon creation.",
},
})
.input(apiCreateCertificate)
.mutation(async ({ input, ctx }) => {
if (IS_CLOUD && !input.serverId) {
@@ -41,6 +47,12 @@ export const certificateRouter = createTRPCRouter({
}),
one: withPermission("certificate", "read")
.meta({
openapi: {
summary: "Get a certificate",
description: "Returns a single certificate by its ID. Verifies that the certificate belongs to the current organization.",
},
})
.input(apiFindCertificate)
.query(async ({ input, ctx }) => {
const certificates = await findCertificateById(input.certificateId);
@@ -53,6 +65,12 @@ export const certificateRouter = createTRPCRouter({
return certificates;
}),
remove: withPermission("certificate", "delete")
.meta({
openapi: {
summary: "Delete a certificate",
description: "Deletes a certificate by its ID after verifying organization ownership. Logs an audit entry before removal.",
},
})
.input(apiFindCertificate)
.mutation(async ({ input, ctx }) => {
const certificates = await findCertificateById(input.certificateId);
@@ -71,7 +89,14 @@ export const certificateRouter = createTRPCRouter({
await removeCertificateById(input.certificateId);
return true;
}),
all: withPermission("certificate", "read").query(async ({ ctx }) => {
all: withPermission("certificate", "read")
.meta({
openapi: {
summary: "List all certificates",
description: "Returns all certificates belonging to the current organization, including their associated server information.",
},
})
.query(async ({ ctx }) => {
return await db.query.certificates.findMany({
where: eq(certificates.organizationId, ctx.session.activeOrganizationId),
with: {
@@ -80,6 +105,12 @@ export const certificateRouter = createTRPCRouter({
});
}),
update: withPermission("certificate", "update")
.meta({
openapi: {
summary: "Update a certificate",
description: "Updates the name, certificate data, and private key of an existing certificate. Verifies organization ownership before applying changes.",
},
})
.input(apiUpdateCertificate)
.mutation(async ({ input, ctx }) => {
const certificate = await findCertificateById(input.certificateId);

View File

@@ -13,6 +13,12 @@ import { createTRPCRouter, withPermission } from "../trpc";
export const clusterRouter = createTRPCRouter({
getNodes: withPermission("server", "read")
.meta({
openapi: {
summary: "Get cluster nodes",
description: "Retrieves all nodes in the Docker Swarm cluster. Optionally targets a remote server.",
},
})
.input(
z.object({
serverId: z.string().optional(),
@@ -25,6 +31,12 @@ export const clusterRouter = createTRPCRouter({
}),
removeWorker: withPermission("server", "delete")
.meta({
openapi: {
summary: "Remove a worker node",
description: "Drains and forcefully removes a worker node from the Docker Swarm cluster. An audit log entry is created for the removal.",
},
})
.input(
z.object({
nodeId: z.string(),
@@ -60,6 +72,12 @@ export const clusterRouter = createTRPCRouter({
}),
addWorker: withPermission("server", "create")
.meta({
openapi: {
summary: "Get worker join command",
description: "Returns the Docker Swarm join command and token for adding a new worker node to the cluster, along with the Docker version.",
},
})
.input(
z.object({
serverId: z.string().optional(),
@@ -83,6 +101,12 @@ export const clusterRouter = createTRPCRouter({
}),
addManager: withPermission("server", "create")
.meta({
openapi: {
summary: "Get manager join command",
description: "Returns the Docker Swarm join command and token for adding a new manager node to the cluster, along with the Docker version.",
},
})
.input(
z.object({
serverId: z.string().optional(),

View File

@@ -83,6 +83,12 @@ import { audit } from "../utils/audit";
export const composeRouter = createTRPCRouter({
create: protectedProcedure
.meta({
openapi: {
summary: "Create a compose service",
description: "Creates a new Docker Compose service in the specified project environment with the given configuration.",
},
})
.input(apiCreateCompose)
.mutation(async ({ ctx, input }) => {
try {
@@ -133,6 +139,12 @@ export const composeRouter = createTRPCRouter({
}),
one: protectedProcedure
.meta({
openapi: {
summary: "Get a compose service",
description: "Retrieves detailed information about a compose service by its ID, including git provider access status and deployment configuration.",
},
})
.input(apiFindCompose)
.query(async ({ input, ctx }) => {
await checkServiceAccess(ctx, input.composeId, "read");
@@ -189,6 +201,12 @@ export const composeRouter = createTRPCRouter({
}),
update: protectedProcedure
.meta({
openapi: {
summary: "Update a compose service",
description: "Updates the configuration of a compose service such as name, description, compose file content, and other settings.",
},
})
.input(apiUpdateCompose)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, {
@@ -204,6 +222,12 @@ export const composeRouter = createTRPCRouter({
return updated;
}),
saveEnvironment: protectedProcedure
.meta({
openapi: {
summary: "Save compose environment variables",
description: "Updates the environment variables for a compose service.",
},
})
.input(apiSaveEnvironmentVariablesCompose)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, {
@@ -229,6 +253,12 @@ export const composeRouter = createTRPCRouter({
return true;
}),
delete: protectedProcedure
.meta({
openapi: {
summary: "Delete a compose service",
description: "Permanently deletes a compose service and cleans up associated Docker resources, deployments, and directories. Optionally deletes associated volumes.",
},
})
.input(apiDeleteCompose)
.mutation(async ({ input, ctx }) => {
await checkServiceAccess(ctx, input.composeId, "delete");
@@ -279,6 +309,12 @@ export const composeRouter = createTRPCRouter({
return composeResult;
}),
cleanQueues: protectedProcedure
.meta({
openapi: {
summary: "Clean deployment queues",
description: "Removes all pending deployment jobs from the queue for the specified compose service.",
},
})
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, {
@@ -288,6 +324,12 @@ export const composeRouter = createTRPCRouter({
return { success: true, message: "Queues cleaned successfully" };
}),
clearDeployments: protectedProcedure
.meta({
openapi: {
summary: "Clear old deployments",
description: "Removes old deployment logs and artifacts for the compose service to free up disk space.",
},
})
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, {
@@ -304,6 +346,12 @@ export const composeRouter = createTRPCRouter({
return true;
}),
killBuild: protectedProcedure
.meta({
openapi: {
summary: "Kill active build",
description: "Forcefully terminates the currently running Docker build process for the compose service.",
},
})
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, {
@@ -314,6 +362,12 @@ export const composeRouter = createTRPCRouter({
}),
loadServices: protectedProcedure
.meta({
openapi: {
summary: "Load compose services",
description: "Parses the compose file and returns the list of services defined in it, with their current container status.",
},
})
.input(apiFetchServices)
.query(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, {
@@ -322,6 +376,12 @@ export const composeRouter = createTRPCRouter({
return await loadServices(input.composeId, input.type);
}),
loadMountsByService: protectedProcedure
.meta({
openapi: {
summary: "Load mounts by service",
description: "Retrieves the Docker volume mounts for a specific service within a compose stack by inspecting the running container.",
},
})
.input(
z.object({
composeId: z.string().min(1),
@@ -340,6 +400,12 @@ export const composeRouter = createTRPCRouter({
return mounts;
}),
fetchSourceType: protectedProcedure
.meta({
openapi: {
summary: "Fetch and clone source",
description: "Clones the compose repository from the configured git provider and returns the source type. Executes the clone command locally or on a remote server.",
},
})
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
try {
@@ -365,6 +431,12 @@ export const composeRouter = createTRPCRouter({
}),
randomizeCompose: protectedProcedure
.meta({
openapi: {
summary: "Randomize compose file",
description: "Adds a random suffix to service names and volumes in the compose file to avoid naming conflicts between deployments.",
},
})
.input(apiRandomizeCompose)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, {
@@ -381,6 +453,12 @@ export const composeRouter = createTRPCRouter({
return result;
}),
isolatedDeployment: protectedProcedure
.meta({
openapi: {
summary: "Randomize for isolated deployment",
description: "Randomizes the compose file for isolated deployment mode, ensuring unique service and volume names to support parallel deployments.",
},
})
.input(apiRandomizeCompose)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, {
@@ -400,6 +478,12 @@ export const composeRouter = createTRPCRouter({
return result;
}),
getConvertedCompose: protectedProcedure
.meta({
openapi: {
summary: "Get converted compose file",
description: "Returns the compose file with domains injected as Traefik labels, converted to YAML format ready for deployment.",
},
})
.input(apiFindCompose)
.query(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, {
@@ -414,6 +498,12 @@ export const composeRouter = createTRPCRouter({
}),
deploy: protectedProcedure
.meta({
openapi: {
summary: "Deploy a compose service",
description: "Triggers a new deployment for the compose service. Queues a deployment job or executes it directly for cloud servers.",
},
})
.input(apiDeployCompose)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, {
@@ -464,6 +554,12 @@ export const composeRouter = createTRPCRouter({
};
}),
redeploy: protectedProcedure
.meta({
openapi: {
summary: "Redeploy a compose service",
description: "Triggers a rebuild and redeployment of the compose service. Queues a deployment job or executes it directly for cloud servers.",
},
})
.input(apiRedeployCompose)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, {
@@ -512,6 +608,12 @@ export const composeRouter = createTRPCRouter({
};
}),
stop: protectedProcedure
.meta({
openapi: {
summary: "Stop a compose service",
description: "Stops all running containers for the compose service using docker compose stop.",
},
})
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, {
@@ -528,6 +630,12 @@ export const composeRouter = createTRPCRouter({
return true;
}),
start: protectedProcedure
.meta({
openapi: {
summary: "Start a compose service",
description: "Starts all containers for the compose service using docker compose start.",
},
})
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, {
@@ -544,6 +652,12 @@ export const composeRouter = createTRPCRouter({
return true;
}),
getDefaultCommand: protectedProcedure
.meta({
openapi: {
summary: "Get default compose command",
description: "Generates and returns the default docker compose command that would be used to deploy the service.",
},
})
.input(apiFindCompose)
.query(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, {
@@ -554,6 +668,12 @@ export const composeRouter = createTRPCRouter({
return `docker ${command}`;
}),
refreshToken: protectedProcedure
.meta({
openapi: {
summary: "Refresh deploy token",
description: "Regenerates the webhook refresh token for the compose service, invalidating the previous token used for triggering deployments.",
},
})
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, {
@@ -572,6 +692,12 @@ export const composeRouter = createTRPCRouter({
return true;
}),
deployTemplate: protectedProcedure
.meta({
openapi: {
summary: "Deploy a template",
description: "Creates a new compose service from a template by fetching its files, processing variables, creating mounts and domains, and setting up the compose configuration.",
},
})
.input(
z.object({
environmentId: z.string(),
@@ -680,6 +806,12 @@ export const composeRouter = createTRPCRouter({
}),
templates: protectedProcedure
.meta({
openapi: {
summary: "List available templates",
description: "Fetches the list of available compose templates from the GitHub templates repository.",
},
})
.input(z.object({ baseUrl: z.string().optional() }))
.query(async ({ input }) => {
try {
@@ -698,6 +830,12 @@ export const composeRouter = createTRPCRouter({
}),
getTags: protectedProcedure
.meta({
openapi: {
summary: "Get template tags",
description: "Fetches all unique tags from the available compose templates for filtering purposes.",
},
})
.input(z.object({ baseUrl: z.string().optional() }))
.query(async ({ input }) => {
const githubTemplates = await fetchTemplatesList(input.baseUrl);
@@ -707,6 +845,12 @@ export const composeRouter = createTRPCRouter({
return uniqueTags;
}),
disconnectGitProvider: protectedProcedure
.meta({
openapi: {
summary: "Disconnect git provider",
description: "Removes all git provider configuration from the compose service, resetting source type to default and clearing repository, branch, and owner fields for all providers.",
},
})
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, {
@@ -759,6 +903,12 @@ export const composeRouter = createTRPCRouter({
}),
move: protectedProcedure
.meta({
openapi: {
summary: "Move compose to another environment",
description: "Moves a compose service to a different environment within the same project or to another project's environment.",
},
})
.input(
z.object({
composeId: z.string(),
@@ -796,6 +946,12 @@ export const composeRouter = createTRPCRouter({
}),
processTemplate: protectedProcedure
.meta({
openapi: {
summary: "Process a template",
description: "Processes a base64-encoded template configuration, resolving variables and generating the compose file and environment settings without applying them.",
},
})
.input(
z.object({
base64: z.string(),
@@ -860,6 +1016,12 @@ export const composeRouter = createTRPCRouter({
}),
import: protectedProcedure
.meta({
openapi: {
summary: "Import a template",
description: "Imports a base64-encoded template into an existing compose service, replacing its compose file, environment variables, mounts, and domains with the template's configuration.",
},
})
.input(
z.object({
base64: z.string(),
@@ -972,6 +1134,12 @@ export const composeRouter = createTRPCRouter({
}),
cancelDeployment: protectedProcedure
.meta({
openapi: {
summary: "Cancel a deployment",
description: "Cancels an in-progress deployment for the compose service and resets its status to idle. Only available in cloud version.",
},
})
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, {
@@ -1025,6 +1193,12 @@ export const composeRouter = createTRPCRouter({
}),
search: protectedProcedure
.meta({
openapi: {
summary: "Search compose services",
description: "Searches compose services by name, appName, or description with pagination. Respects service-level access control.",
},
})
.input(
z.object({
q: z.string().optional(),
@@ -1133,6 +1307,12 @@ export const composeRouter = createTRPCRouter({
}),
readLogs: protectedProcedure
.meta({
openapi: {
summary: "Read compose container logs",
description: "Retrieves Docker container logs for a specific container within the compose service with configurable tail length, time range, and optional text search filtering.",
},
})
.input(
apiFindCompose.extend({
containerId: z

View File

@@ -34,6 +34,12 @@ import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc";
export const deploymentRouter = createTRPCRouter({
all: protectedProcedure
.meta({
openapi: {
summary: "List deployments by application",
description: "Returns all deployments associated with the given application, ordered by creation date.",
},
})
.input(apiFindAllByApplication)
.query(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -43,6 +49,12 @@ export const deploymentRouter = createTRPCRouter({
}),
allByCompose: protectedProcedure
.meta({
openapi: {
summary: "List deployments by compose",
description: "Returns all deployments associated with the given compose service.",
},
})
.input(apiFindAllByCompose)
.query(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, {
@@ -51,11 +63,24 @@ export const deploymentRouter = createTRPCRouter({
return await findAllDeploymentsByComposeId(input.composeId);
}),
allByServer: withPermission("deployment", "read")
.meta({
openapi: {
summary: "List deployments by server",
description: "Returns all deployments associated with the given server.",
},
})
.input(apiFindAllByServer)
.query(async ({ input }) => {
return await findAllDeploymentsByServerId(input.serverId);
}),
allCentralized: withPermission("deployment", "read").query(
allCentralized: withPermission("deployment", "read")
.meta({
openapi: {
summary: "List all deployments centralized",
description: "Returns all deployments across all services in the organization. Non-admin users only see deployments for their accessible services.",
},
})
.query(
async ({ ctx }) => {
const orgId = ctx.session.activeOrganizationId;
const accessedServices =
@@ -69,7 +94,14 @@ export const deploymentRouter = createTRPCRouter({
},
),
queueList: withPermission("deployment", "read").query(async ({ ctx }) => {
queueList: withPermission("deployment", "read")
.meta({
openapi: {
summary: "List deployment queue jobs",
description: "Returns all jobs in the deployment queue with their current state, timestamps, and resolved service paths.",
},
})
.query(async ({ ctx }) => {
const orgId = ctx.session.activeOrganizationId;
let rows: QueueJobRow[];
@@ -116,6 +148,12 @@ export const deploymentRouter = createTRPCRouter({
}),
allByType: protectedProcedure
.meta({
openapi: {
summary: "List deployments by service type",
description: "Returns all deployments for a given service ID and type (application, compose, etc.), including associated rollback information.",
},
})
.input(apiFindAllByType)
.query(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.id, {
@@ -131,6 +169,12 @@ export const deploymentRouter = createTRPCRouter({
return deploymentsList;
}),
killProcess: protectedProcedure
.meta({
openapi: {
summary: "Cancel a running deployment",
description: "Kills the running process of a deployment by sending SIGKILL to its PID. Updates the deployment status to error.",
},
})
.input(
z.object({
deploymentId: z.string().min(1),
@@ -168,6 +212,12 @@ export const deploymentRouter = createTRPCRouter({
}),
removeDeployment: protectedProcedure
.meta({
openapi: {
summary: "Delete a deployment",
description: "Permanently removes a deployment record and its associated data.",
},
})
.input(
z.object({
deploymentId: z.string().min(1),
@@ -189,4 +239,43 @@ export const deploymentRouter = createTRPCRouter({
});
return result;
}),
readBuildLogs: protectedProcedure
.meta({
openapi: {
summary: "Read deployment build logs",
description:
"Reads the build/deployment log file for a specific deployment. Returns the last N lines (default 200). Works for both local and remote server deployments.",
},
})
.input(
z.object({
deploymentId: z.string().min(1),
tail: z.number().int().min(1).max(10000).default(200),
}),
)
.query(async ({ input, ctx }) => {
const deployment = await findDeploymentById(input.deploymentId);
const serviceId = deployment.applicationId || deployment.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
deployment: ["read"],
});
}
const command = `tail -n ${input.tail} ${deployment.logPath} 2>/dev/null || echo "Log file not found"`;
const { stdout } = deployment.serverId
? await execAsyncRemote(deployment.serverId, command)
: await execAsync(command);
return {
deploymentId: deployment.deploymentId,
status: deployment.status,
errorMessage: deployment.errorMessage || null,
title: deployment.title,
createdAt: deployment.createdAt,
logs: stdout,
};
}),
});

View File

@@ -22,6 +22,12 @@ import {
export const destinationRouter = createTRPCRouter({
create: withPermission("destination", "create")
.meta({
openapi: {
summary: "Create backup destination",
description: "Creates a new S3-compatible backup destination for the current organization and logs an audit event.",
},
})
.input(apiCreateDestination)
.mutation(async ({ input, ctx }) => {
try {
@@ -45,6 +51,12 @@ export const destinationRouter = createTRPCRouter({
}
}),
testConnection: withPermission("destination", "create")
.meta({
openapi: {
summary: "Test backup destination connection",
description: "Tests connectivity to an S3-compatible bucket using rclone. Runs locally or on a remote server depending on configuration.",
},
})
.input(apiCreateDestination)
.mutation(async ({ input }) => {
const {
@@ -102,6 +114,12 @@ export const destinationRouter = createTRPCRouter({
}
}),
one: withPermission("destination", "read")
.meta({
openapi: {
summary: "Get backup destination",
description: "Returns a single backup destination by ID. Verifies the caller belongs to the same organization.",
},
})
.input(apiFindOneDestination)
.query(async ({ input, ctx }) => {
const destination = await findDestinationById(input.destinationId);
@@ -113,13 +131,26 @@ export const destinationRouter = createTRPCRouter({
}
return destination;
}),
all: withPermission("destination", "read").query(async ({ ctx }) => {
all: withPermission("destination", "read")
.meta({
openapi: {
summary: "List all backup destinations",
description: "Returns all S3-compatible backup destinations for the current organization, ordered by creation date descending.",
},
})
.query(async ({ ctx }) => {
return await db.query.destinations.findMany({
where: eq(destinations.organizationId, ctx.session.activeOrganizationId),
orderBy: [desc(destinations.createdAt)],
});
}),
remove: withPermission("destination", "delete")
.meta({
openapi: {
summary: "Delete backup destination",
description: "Removes a backup destination by ID. Verifies organization ownership and logs an audit event before deletion.",
},
})
.input(apiRemoveDestination)
.mutation(async ({ input, ctx }) => {
try {
@@ -147,6 +178,12 @@ export const destinationRouter = createTRPCRouter({
}
}),
update: withPermission("destination", "create")
.meta({
openapi: {
summary: "Update backup destination",
description: "Updates an existing backup destination. Verifies organization ownership before applying changes and logs an audit event.",
},
})
.input(apiUpdateDestination)
.mutation(async ({ input, ctx }) => {
try {

View File

@@ -20,6 +20,12 @@ export const containerIdRegex = /^[a-zA-Z0-9.\-_]+$/;
export const dockerRouter = createTRPCRouter({
getContainers: withPermission("docker", "read")
.meta({
openapi: {
summary: "Get Docker containers",
description: "Retrieves a list of all Docker containers. Optionally targets a specific remote server by ID.",
},
})
.input(
z.object({
serverId: z.string().optional(),
@@ -36,6 +42,12 @@ export const dockerRouter = createTRPCRouter({
}),
restartContainer: withPermission("docker", "read")
.meta({
openapi: {
summary: "Restart a Docker container",
description: "Restarts a Docker container by its ID. An audit log entry is created for the action.",
},
})
.input(
z.object({
containerId: z
@@ -56,6 +68,12 @@ export const dockerRouter = createTRPCRouter({
}),
removeContainer: withPermission("docker", "read")
.meta({
openapi: {
summary: "Remove a Docker container",
description: "Removes a Docker container by its ID. Optionally targets a remote server. An audit log entry is created for the deletion.",
},
})
.input(
z.object({
containerId: z
@@ -82,6 +100,12 @@ export const dockerRouter = createTRPCRouter({
}),
getConfig: withPermission("docker", "read")
.meta({
openapi: {
summary: "Get Docker container configuration",
description: "Retrieves the configuration (inspect data) for a specific Docker container. Optionally targets a remote server.",
},
})
.input(
z.object({
containerId: z
@@ -102,6 +126,12 @@ export const dockerRouter = createTRPCRouter({
}),
getContainersByAppNameMatch: withPermission("service", "read")
.meta({
openapi: {
summary: "Get containers by app name match",
description: "Retrieves containers whose names match the given application name. Supports filtering by app type (stack or docker-compose) and optionally targets a remote server.",
},
})
.input(
z.object({
appType: z.enum(["stack", "docker-compose"]).optional(),
@@ -124,6 +154,12 @@ export const dockerRouter = createTRPCRouter({
}),
getContainersByAppLabel: withPermission("docker", "read")
.meta({
openapi: {
summary: "Get containers by app label",
description: "Retrieves containers filtered by application label. Supports standalone and swarm deployment types, and optionally targets a remote server.",
},
})
.input(
z.object({
appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."),
@@ -146,6 +182,12 @@ export const dockerRouter = createTRPCRouter({
}),
getStackContainersByAppName: withPermission("docker", "read")
.meta({
openapi: {
summary: "Get stack containers by app name",
description: "Retrieves all containers belonging to a Docker stack by application name. Optionally targets a remote server.",
},
})
.input(
z.object({
appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."),
@@ -163,6 +205,12 @@ export const dockerRouter = createTRPCRouter({
}),
getServiceContainersByAppName: withPermission("docker", "read")
.meta({
openapi: {
summary: "Get service containers by app name",
description: "Retrieves all containers belonging to a Docker Swarm service by application name. Optionally targets a remote server.",
},
})
.input(
z.object({
appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."),
@@ -180,6 +228,12 @@ export const dockerRouter = createTRPCRouter({
}),
uploadFileToContainer: withPermission("docker", "read")
.meta({
openapi: {
summary: "Upload a file to a Docker container",
description: "Uploads a file to a specified path inside a Docker container. The file is converted to a buffer and transferred to the container's filesystem.",
},
})
.input(uploadFileToContainerSchema)
.mutation(async ({ input, ctx }) => {
if (input.serverId) {

View File

@@ -33,6 +33,12 @@ import {
export const domainRouter = createTRPCRouter({
create: protectedProcedure
.meta({
openapi: {
summary: "Create a domain",
description: "Creates a new domain for an application or compose service. Validates permissions and logs an audit entry.",
},
})
.input(apiCreateDomain)
.mutation(async ({ input, ctx }) => {
try {
@@ -65,6 +71,12 @@ export const domainRouter = createTRPCRouter({
}
}),
byApplicationId: protectedProcedure
.meta({
openapi: {
summary: "List domains by application",
description: "Returns all domains associated with a given application ID.",
},
})
.input(apiFindOneApplication)
.query(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -73,6 +85,12 @@ export const domainRouter = createTRPCRouter({
return await findDomainsByApplicationId(input.applicationId);
}),
byComposeId: protectedProcedure
.meta({
openapi: {
summary: "List domains by compose service",
description: "Returns all domains associated with a given compose service ID.",
},
})
.input(apiFindCompose)
.query(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, {
@@ -81,6 +99,12 @@ export const domainRouter = createTRPCRouter({
return await findDomainsByComposeId(input.composeId);
}),
generateDomain: withPermission("domain", "create")
.meta({
openapi: {
summary: "Generate a traefik.me domain",
description: "Generates a free traefik.me domain for an application, using the server IP to create a wildcard subdomain.",
},
})
.input(z.object({ appName: z.string(), serverId: z.string().optional() }))
.mutation(async ({ input, ctx }) => {
return generateTraefikMeDomain(
@@ -90,6 +114,12 @@ export const domainRouter = createTRPCRouter({
);
}),
canGenerateTraefikMeDomains: withPermission("domain", "read")
.meta({
openapi: {
summary: "Check traefik.me domain availability",
description: "Checks whether traefik.me domains can be generated by returning the server IP address. Returns the IP from the server record or web server settings.",
},
})
.input(z.object({ serverId: z.string() }))
.query(async ({ input }) => {
if (input.serverId) {
@@ -101,6 +131,12 @@ export const domainRouter = createTRPCRouter({
}),
update: protectedProcedure
.meta({
openapi: {
summary: "Update a domain",
description: "Updates a domain's configuration and refreshes the Traefik routing rules for the associated application or preview deployment.",
},
})
.input(apiUpdateDomain)
.mutation(async ({ input, ctx }) => {
const currentDomain = await findDomainById(input.domainId);
@@ -141,7 +177,15 @@ export const domainRouter = createTRPCRouter({
}
return result;
}),
one: protectedProcedure.input(apiFindDomain).query(async ({ input, ctx }) => {
one: protectedProcedure
.meta({
openapi: {
summary: "Get a domain",
description: "Returns a single domain by its ID. Validates read permissions against the associated service or preview deployment.",
},
})
.input(apiFindDomain)
.query(async ({ input, ctx }) => {
const domain = await findDomainById(input.domainId);
const serviceId = domain.applicationId || domain.composeId;
if (serviceId) {
@@ -159,6 +203,12 @@ export const domainRouter = createTRPCRouter({
return domain;
}),
delete: protectedProcedure
.meta({
openapi: {
summary: "Delete a domain",
description: "Deletes a domain by its ID and removes the associated Traefik routing configuration for the application.",
},
})
.input(apiFindDomain)
.mutation(async ({ input, ctx }) => {
const domain = await findDomainById(input.domainId);
@@ -193,6 +243,12 @@ export const domainRouter = createTRPCRouter({
}),
validateDomain: withPermission("domain", "read")
.meta({
openapi: {
summary: "Validate a domain",
description: "Checks whether a domain's DNS records are correctly configured, optionally verifying against a specific server IP.",
},
})
.input(
z.object({
domain: z.string(),

View File

@@ -63,6 +63,12 @@ const filterEnvironmentServices = (
export const environmentRouter = createTRPCRouter({
create: protectedProcedure
.meta({
openapi: {
summary: "Create environment",
description: "Creates a new environment within a project. The name 'production' is reserved and cannot be used. Checks creation permissions and logs an audit event.",
},
})
.input(apiCreateEnvironment)
.mutation(async ({ input, ctx }) => {
try {
@@ -99,6 +105,12 @@ export const environmentRouter = createTRPCRouter({
}),
one: protectedProcedure
.meta({
openapi: {
summary: "Get environment",
description: "Returns a single environment by ID with all its services. Non-admin users only see services they have been granted access to.",
},
})
.input(apiFindOneEnvironment)
.query(async ({ input, ctx }) => {
const environment = await findEnvironmentById(input.environmentId);
@@ -137,6 +149,12 @@ export const environmentRouter = createTRPCRouter({
}),
byProjectId: protectedProcedure
.meta({
openapi: {
summary: "List environments by project",
description: "Returns all environments for a given project. Non-admin users only see environments and services they have been granted access to.",
},
})
.input(z.object({ projectId: z.string() }))
.query(async ({ input, ctx }) => {
try {
@@ -183,6 +201,12 @@ export const environmentRouter = createTRPCRouter({
}),
remove: protectedProcedure
.meta({
openapi: {
summary: "Delete environment",
description: "Deletes an environment by ID. The default environment cannot be deleted. Checks deletion permissions and environment access before removing.",
},
})
.input(apiRemoveEnvironment)
.mutation(async ({ input, ctx }) => {
try {
@@ -229,6 +253,12 @@ export const environmentRouter = createTRPCRouter({
}),
update: protectedProcedure
.meta({
openapi: {
summary: "Update environment",
description: "Updates an environment's name, description, or env variables. The default environment cannot be renamed. Checks environment access and env-var write permissions.",
},
})
.input(apiUpdateEnvironment)
.mutation(async ({ input, ctx }) => {
try {
@@ -296,6 +326,12 @@ export const environmentRouter = createTRPCRouter({
}),
duplicate: protectedProcedure
.meta({
openapi: {
summary: "Duplicate environment",
description: "Creates a copy of an existing environment including its services. Checks environment access and organization ownership before duplicating.",
},
})
.input(apiDuplicateEnvironment)
.mutation(async ({ input, ctx }) => {
try {
@@ -343,6 +379,12 @@ export const environmentRouter = createTRPCRouter({
}),
search: protectedProcedure
.meta({
openapi: {
summary: "Search environments",
description: "Searches environments by name, description, or project with pagination. Non-admin users only see environments they have been granted access to.",
},
})
.input(
z.object({
q: z.string().optional(),

View File

@@ -21,7 +21,14 @@ import {
} from "@/server/db/schema";
export const gitProviderRouter = createTRPCRouter({
getAll: protectedProcedure.query(async ({ ctx }) => {
getAll: protectedProcedure
.meta({
openapi: {
summary: "List all git providers",
description: "Returns all git providers (GitHub, GitLab, Bitbucket, Gitea) accessible to the current user within the active organization, ordered by creation date.",
},
})
.query(async ({ ctx }) => {
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
if (accessibleIds.size === 0) {
@@ -46,6 +53,12 @@ export const gitProviderRouter = createTRPCRouter({
}),
toggleShare: protectedProcedure
.meta({
openapi: {
summary: "Toggle git provider sharing",
description: "Toggles whether a git provider is shared with the entire organization. Only the owner of the provider can change this setting.",
},
})
.input(apiToggleShareGitProvider)
.mutation(async ({ input, ctx }) => {
const provider = await findGitProviderById(input.gitProviderId);
@@ -73,6 +86,12 @@ export const gitProviderRouter = createTRPCRouter({
}),
allForPermissions: withPermission("member", "update")
.meta({
openapi: {
summary: "List git providers for permissions",
description: "Returns a minimal list of all git providers in the organization for use in permission assignment UIs. Requires a valid enterprise license and member update permission.",
},
})
.use(async ({ ctx, next }) => {
const licensed = await hasValidLicense(ctx.session.activeOrganizationId);
if (!licensed) {
@@ -96,6 +115,12 @@ export const gitProviderRouter = createTRPCRouter({
}),
remove: withPermission("gitProviders", "delete")
.meta({
openapi: {
summary: "Remove git provider",
description: "Deletes a git provider from the organization. Requires gitProviders delete permission and the provider must belong to the active organization.",
},
})
.input(apiRemoveGitProvider)
.mutation(async ({ input, ctx }) => {
try {

View File

@@ -27,6 +27,12 @@ import {
export const giteaRouter = createTRPCRouter({
create: withPermission("gitProviders", "create")
.meta({
openapi: {
summary: "Create Gitea provider",
description: "Creates a new Gitea provider configuration linked to the active organization. Requires gitProviders create permission.",
},
})
.input(apiCreateGitea)
.mutation(async ({ input, ctx }) => {
try {
@@ -53,11 +59,26 @@ export const giteaRouter = createTRPCRouter({
}
}),
one: protectedProcedure.input(apiFindOneGitea).query(async ({ input }) => {
return await findGiteaById(input.giteaId);
}),
one: protectedProcedure
.meta({
openapi: {
summary: "Get Gitea provider",
description: "Returns a single Gitea provider configuration by its ID.",
},
})
.input(apiFindOneGitea)
.query(async ({ input }) => {
return await findGiteaById(input.giteaId);
}),
giteaProviders: protectedProcedure.query(async ({ ctx }) => {
giteaProviders: protectedProcedure
.meta({
openapi: {
summary: "List Gitea providers",
description: "Returns all Gitea providers accessible to the current user within the active organization, filtered to only those with valid credentials.",
},
})
.query(async ({ ctx }) => {
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
let result = await db.query.gitea.findMany({
@@ -88,6 +109,12 @@ export const giteaRouter = createTRPCRouter({
}),
getGiteaRepositories: protectedProcedure
.meta({
openapi: {
summary: "List Gitea repositories",
description: "Fetches the list of repositories accessible by the Gitea provider. Calls the Gitea API using the provider's credentials.",
},
})
.input(apiFindOneGitea)
.query(async ({ input }) => {
const { giteaId } = input;
@@ -112,6 +139,12 @@ export const giteaRouter = createTRPCRouter({
}),
getGiteaBranches: protectedProcedure
.meta({
openapi: {
summary: "List Gitea branches",
description: "Fetches the list of branches for a specific Gitea repository. Calls the Gitea API using the provider's credentials.",
},
})
.input(apiFindGiteaBranches)
.query(async ({ input }) => {
const { giteaId, owner, repositoryName } = input;
@@ -140,6 +173,12 @@ export const giteaRouter = createTRPCRouter({
}),
testConnection: protectedProcedure
.meta({
openapi: {
summary: "Test Gitea connection",
description: "Tests the connection to a Gitea provider by attempting to fetch its repositories. Returns the number of repositories found or throws an error on failure.",
},
})
.input(apiGiteaTestConnection)
.mutation(async ({ input }) => {
const giteaId = input.giteaId ?? "";
@@ -160,6 +199,12 @@ export const giteaRouter = createTRPCRouter({
}),
update: withPermission("gitProviders", "create")
.meta({
openapi: {
summary: "Update Gitea provider",
description: "Updates a Gitea provider configuration and its associated git provider record. Requires gitProviders create permission.",
},
})
.input(apiUpdateGitea)
.mutation(async ({ input, ctx }) => {
if (input.name) {
@@ -188,6 +233,12 @@ export const giteaRouter = createTRPCRouter({
}),
getGiteaUrl: protectedProcedure
.meta({
openapi: {
summary: "Get Gitea instance URL",
description: "Returns the base URL of the Gitea instance associated with the given provider ID.",
},
})
.input(apiFindOneGitea)
.query(async ({ input }) => {
const { giteaId } = input;

View File

@@ -22,20 +22,47 @@ import {
} from "@/server/db/schema";
export const githubRouter = createTRPCRouter({
one: protectedProcedure.input(apiFindOneGithub).query(async ({ input }) => {
return await findGithubById(input.githubId);
}),
one: protectedProcedure
.meta({
openapi: {
summary: "Get GitHub provider",
description: "Returns a single GitHub provider configuration by its ID.",
},
})
.input(apiFindOneGithub)
.query(async ({ input }) => {
return await findGithubById(input.githubId);
}),
getGithubRepositories: protectedProcedure
.meta({
openapi: {
summary: "List GitHub repositories",
description: "Fetches the list of repositories accessible by the GitHub provider. Calls the GitHub API using the provider's credentials.",
},
})
.input(apiFindOneGithub)
.query(async ({ input }) => {
return await getGithubRepositories(input.githubId);
}),
getGithubBranches: protectedProcedure
.meta({
openapi: {
summary: "List GitHub branches",
description: "Fetches the list of branches for a specific GitHub repository. Calls the GitHub API using the provider's credentials.",
},
})
.input(apiFindGithubBranches)
.query(async ({ input }) => {
return await getGithubBranches(input);
}),
githubProviders: protectedProcedure.query(async ({ ctx }) => {
githubProviders: protectedProcedure
.meta({
openapi: {
summary: "List GitHub providers",
description: "Returns all GitHub providers accessible to the current user within the active organization, filtered to only those with valid credentials.",
},
})
.query(async ({ ctx }) => {
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
let result = await db.query.github.findMany({
@@ -66,6 +93,12 @@ export const githubRouter = createTRPCRouter({
}),
testConnection: protectedProcedure
.meta({
openapi: {
summary: "Test GitHub connection",
description: "Tests the connection to a GitHub provider by attempting to fetch its repositories. Returns the number of repositories found or throws an error on failure.",
},
})
.input(apiFindOneGithub)
.mutation(async ({ input }) => {
try {
@@ -79,6 +112,12 @@ export const githubRouter = createTRPCRouter({
}
}),
update: withPermission("gitProviders", "create")
.meta({
openapi: {
summary: "Update GitHub provider",
description: "Updates a GitHub provider configuration and its associated git provider record. Requires gitProviders create permission.",
},
})
.input(apiUpdateGithub)
.mutation(async ({ input, ctx }) => {
await updateGitProvider(input.gitProviderId, {

View File

@@ -27,6 +27,12 @@ import {
export const gitlabRouter = createTRPCRouter({
create: withPermission("gitProviders", "create")
.meta({
openapi: {
summary: "Create GitLab provider",
description: "Creates a new GitLab provider configuration linked to the active organization. Requires gitProviders create permission.",
},
})
.input(apiCreateGitlab)
.mutation(async ({ input, ctx }) => {
try {
@@ -51,10 +57,25 @@ export const gitlabRouter = createTRPCRouter({
});
}
}),
one: protectedProcedure.input(apiFindOneGitlab).query(async ({ input }) => {
return await findGitlabById(input.gitlabId);
}),
gitlabProviders: protectedProcedure.query(async ({ ctx }) => {
one: protectedProcedure
.meta({
openapi: {
summary: "Get GitLab provider",
description: "Returns a single GitLab provider configuration by its ID.",
},
})
.input(apiFindOneGitlab)
.query(async ({ input }) => {
return await findGitlabById(input.gitlabId);
}),
gitlabProviders: protectedProcedure
.meta({
openapi: {
summary: "List GitLab providers",
description: "Returns all GitLab providers accessible to the current user within the active organization, filtered to only those with valid credentials.",
},
})
.query(async ({ ctx }) => {
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
let result = await db.query.gitlab.findMany({
@@ -85,17 +106,35 @@ export const gitlabRouter = createTRPCRouter({
return filtered;
}),
getGitlabRepositories: protectedProcedure
.meta({
openapi: {
summary: "List GitLab repositories",
description: "Fetches the list of repositories accessible by the GitLab provider. Calls the GitLab API using the provider's credentials.",
},
})
.input(apiFindOneGitlab)
.query(async ({ input }) => {
return await getGitlabRepositories(input.gitlabId);
}),
getGitlabBranches: protectedProcedure
.meta({
openapi: {
summary: "List GitLab branches",
description: "Fetches the list of branches for a specific GitLab repository. Calls the GitLab API using the provider's credentials.",
},
})
.input(apiFindGitlabBranches)
.query(async ({ input }) => {
return await getGitlabBranches(input);
}),
testConnection: protectedProcedure
.meta({
openapi: {
summary: "Test GitLab connection",
description: "Tests the connection to a GitLab provider by attempting to fetch its repositories. Returns the number of repositories found or throws an error on failure.",
},
})
.input(apiGitlabTestConnection)
.mutation(async ({ input }) => {
try {
@@ -110,6 +149,12 @@ export const gitlabRouter = createTRPCRouter({
}
}),
update: withPermission("gitProviders", "create")
.meta({
openapi: {
summary: "Update GitLab provider",
description: "Updates a GitLab provider configuration and its associated git provider record. Requires gitProviders create permission.",
},
})
.input(apiUpdateGitlab)
.mutation(async ({ input, ctx }) => {
if (input.name) {

View File

@@ -6,6 +6,7 @@ import {
findEnvironmentById,
findLibsqlById,
findProjectById,
getAccessibleServerIds,
getContainerLogs,
IS_CLOUD,
rebuildDatabase,
@@ -16,7 +17,6 @@ import {
stopService,
stopServiceRemote,
updateLibsqlById,
getAccessibleServerIds,
} from "@dokploy/server";
import {
addNewService,
@@ -43,6 +43,12 @@ import {
} from "@/server/db/schema";
export const libsqlRouter = createTRPCRouter({
create: protectedProcedure
.meta({
openapi: {
summary: "Create a LibSQL database",
description: "Creates a new LibSQL database service with the specified configuration, sets up a persistent data volume, and registers it in the project.",
},
})
.input(apiCreateLibsql)
.mutation(async ({ input, ctx }) => {
try {
@@ -100,6 +106,12 @@ export const libsqlRouter = createTRPCRouter({
}
}),
one: protectedProcedure
.meta({
openapi: {
summary: "Get a LibSQL database by ID",
description: "Returns the full details of a LibSQL database service, including its environment and project configuration.",
},
})
.input(apiFindOneLibsql)
.query(async ({ input, ctx }) => {
await checkServiceAccess(ctx, input.libsqlId, "read");
@@ -118,6 +130,12 @@ export const libsqlRouter = createTRPCRouter({
}),
start: protectedProcedure
.meta({
openapi: {
summary: "Start a LibSQL database",
description: "Starts the Docker container for the specified LibSQL database and sets its status to done.",
},
})
.input(apiFindOneLibsql)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
@@ -143,6 +161,12 @@ export const libsqlRouter = createTRPCRouter({
return libsql;
}),
stop: protectedProcedure
.meta({
openapi: {
summary: "Stop a LibSQL database",
description: "Stops the Docker container for the specified LibSQL database and sets its status to idle.",
},
})
.input(apiFindOneLibsql)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
@@ -168,6 +192,12 @@ export const libsqlRouter = createTRPCRouter({
return libsql;
}),
saveExternalPorts: protectedProcedure
.meta({
openapi: {
summary: "Save the external ports for a LibSQL database",
description: "Updates the external port mappings (HTTP, gRPC, admin) for the LibSQL database and triggers a redeployment. Validates that ports are not already in use.",
},
})
.input(apiSaveExternalPortsLibsql)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
@@ -230,6 +260,12 @@ export const libsqlRouter = createTRPCRouter({
return libsql;
}),
deploy: protectedProcedure
.meta({
openapi: {
summary: "Deploy a LibSQL database",
description: "Triggers a deployment for the specified LibSQL database, rebuilding and restarting its Docker container with the current configuration.",
},
})
.input(apiDeployLibsql)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
@@ -282,6 +318,12 @@ export const libsqlRouter = createTRPCRouter({
}
}),
changeStatus: protectedProcedure
.meta({
openapi: {
summary: "Change LibSQL database status",
description: "Updates the application status of a LibSQL database without starting or stopping the container.",
},
})
.input(apiChangeLibsqlStatus)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
@@ -300,6 +342,12 @@ export const libsqlRouter = createTRPCRouter({
return libsql;
}),
remove: protectedProcedure
.meta({
openapi: {
summary: "Delete a LibSQL database",
description: "Removes the LibSQL database service, its Docker container, and deletes the database record.",
},
})
.input(apiFindOneLibsql)
.mutation(async ({ input, ctx }) => {
await checkServiceAccess(ctx, input.libsqlId, "delete");
@@ -335,6 +383,12 @@ export const libsqlRouter = createTRPCRouter({
return libsql;
}),
saveEnvironment: protectedProcedure
.meta({
openapi: {
summary: "Save environment variables for a LibSQL database",
description: "Updates the environment variables for the specified LibSQL database service.",
},
})
.input(apiSaveEnvironmentVariablesLibsql)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
@@ -359,6 +413,12 @@ export const libsqlRouter = createTRPCRouter({
return true;
}),
reload: protectedProcedure
.meta({
openapi: {
summary: "Reload a LibSQL database",
description: "Restarts the LibSQL database by stopping and then starting its Docker container.",
},
})
.input(apiResetLibsql)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
@@ -391,6 +451,12 @@ export const libsqlRouter = createTRPCRouter({
return true;
}),
update: protectedProcedure
.meta({
openapi: {
summary: "Update a LibSQL database",
description: "Updates the configuration of an existing LibSQL database service.",
},
})
.input(apiUpdateLibsql)
.mutation(async ({ input, ctx }) => {
const { libsqlId, ...rest } = input;
@@ -417,6 +483,12 @@ export const libsqlRouter = createTRPCRouter({
return true;
}),
move: protectedProcedure
.meta({
openapi: {
summary: "Move a LibSQL database to another environment",
description: "Moves the LibSQL database to a different environment within the same project.",
},
})
.input(
z.object({
libsqlId: z.string(),
@@ -453,6 +525,12 @@ export const libsqlRouter = createTRPCRouter({
return updatedLibsql;
}),
rebuild: protectedProcedure
.meta({
openapi: {
summary: "Rebuild a LibSQL database",
description: "Rebuilds the LibSQL database Docker container from scratch, pulling the latest image and recreating the service.",
},
})
.input(apiRebuildLibsql)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
@@ -469,6 +547,12 @@ export const libsqlRouter = createTRPCRouter({
}),
readLogs: protectedProcedure
.meta({
openapi: {
summary: "Read LibSQL container logs",
description: "Retrieves the Docker container logs for the specified LibSQL database, with support for tail count, time-based filtering, and text search.",
},
})
.input(
apiFindOneLibsql.extend({
tail: z.number().int().min(1).max(10000).default(100),

View File

@@ -54,6 +54,12 @@ import {
import { cancelJobs } from "@/server/utils/backup";
export const mariadbRouter = createTRPCRouter({
create: protectedProcedure
.meta({
openapi: {
summary: "Create a MariaDB database",
description: "Creates a new MariaDB database service with the specified configuration, sets up a persistent data volume, and registers it in the project.",
},
})
.input(apiCreateMariaDB)
.mutation(async ({ input, ctx }) => {
try {
@@ -114,6 +120,12 @@ export const mariadbRouter = createTRPCRouter({
}
}),
one: protectedProcedure
.meta({
openapi: {
summary: "Get a MariaDB database by ID",
description: "Returns the full details of a MariaDB database service, including its environment and project configuration.",
},
})
.input(apiFindOneMariaDB)
.query(async ({ input, ctx }) => {
await checkServiceAccess(ctx, input.mariadbId, "read");
@@ -131,6 +143,12 @@ export const mariadbRouter = createTRPCRouter({
}),
start: protectedProcedure
.meta({
openapi: {
summary: "Start a MariaDB database",
description: "Starts the Docker container for the specified MariaDB database and sets its status to done.",
},
})
.input(apiFindOneMariaDB)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
@@ -155,6 +173,12 @@ export const mariadbRouter = createTRPCRouter({
return service;
}),
stop: protectedProcedure
.meta({
openapi: {
summary: "Stop a MariaDB database",
description: "Stops the Docker container for the specified MariaDB database and sets its status to idle.",
},
})
.input(apiFindOneMariaDB)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
@@ -180,6 +204,12 @@ export const mariadbRouter = createTRPCRouter({
return mariadb;
}),
saveExternalPort: protectedProcedure
.meta({
openapi: {
summary: "Save the external port for a MariaDB database",
description: "Updates the external port mapping for the MariaDB database and triggers a redeployment. Validates that the port is not already in use.",
},
})
.input(apiSaveExternalPortMariaDB)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
@@ -213,6 +243,12 @@ export const mariadbRouter = createTRPCRouter({
return mariadb;
}),
deploy: protectedProcedure
.meta({
openapi: {
summary: "Deploy a MariaDB database",
description: "Triggers a deployment for the specified MariaDB database, rebuilding and restarting its Docker container with the current configuration.",
},
})
.input(apiDeployMariaDB)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
@@ -250,6 +286,12 @@ export const mariadbRouter = createTRPCRouter({
});
}),
changeStatus: protectedProcedure
.meta({
openapi: {
summary: "Change MariaDB database status",
description: "Updates the application status of a MariaDB database without starting or stopping the container.",
},
})
.input(apiChangeMariaDBStatus)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
@@ -268,6 +310,12 @@ export const mariadbRouter = createTRPCRouter({
return mongo;
}),
remove: protectedProcedure
.meta({
openapi: {
summary: "Delete a MariaDB database",
description: "Removes the MariaDB database service, its Docker container, cancels associated backup jobs, and deletes the database record.",
},
})
.input(apiFindOneMariaDB)
.mutation(async ({ input, ctx }) => {
await checkServiceAccess(ctx, input.mariadbId, "delete");
@@ -305,6 +353,12 @@ export const mariadbRouter = createTRPCRouter({
return mongo;
}),
saveEnvironment: protectedProcedure
.meta({
openapi: {
summary: "Save environment variables for a MariaDB database",
description: "Updates the environment variables for the specified MariaDB database service.",
},
})
.input(apiSaveEnvironmentVariablesMariaDB)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
@@ -329,6 +383,12 @@ export const mariadbRouter = createTRPCRouter({
return true;
}),
reload: protectedProcedure
.meta({
openapi: {
summary: "Reload a MariaDB database",
description: "Restarts the MariaDB database by stopping and then starting its Docker container.",
},
})
.input(apiResetMariadb)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
@@ -361,6 +421,12 @@ export const mariadbRouter = createTRPCRouter({
return true;
}),
update: protectedProcedure
.meta({
openapi: {
summary: "Update a MariaDB database",
description: "Updates the configuration of an existing MariaDB database service.",
},
})
.input(apiUpdateMariaDB)
.mutation(async ({ input, ctx }) => {
const { mariadbId, ...rest } = input;
@@ -387,6 +453,12 @@ export const mariadbRouter = createTRPCRouter({
return true;
}),
changePassword: protectedProcedure
.meta({
openapi: {
summary: "Change MariaDB database password",
description: "Changes the password for a MariaDB user or root account by executing ALTER USER inside the running container and updating the stored password.",
},
})
.input(
z.object({
mariadbId: z.string().min(1),
@@ -444,6 +516,12 @@ export const mariadbRouter = createTRPCRouter({
return true;
}),
move: protectedProcedure
.meta({
openapi: {
summary: "Move a MariaDB database to another environment",
description: "Moves the MariaDB database to a different environment within the same project.",
},
})
.input(
z.object({
mariadbId: z.string(),
@@ -480,6 +558,12 @@ export const mariadbRouter = createTRPCRouter({
return updatedMariadb;
}),
rebuild: protectedProcedure
.meta({
openapi: {
summary: "Rebuild a MariaDB database",
description: "Rebuilds the MariaDB database Docker container from scratch, pulling the latest image and recreating the service.",
},
})
.input(apiRebuildMariadb)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
@@ -495,6 +579,12 @@ export const mariadbRouter = createTRPCRouter({
return true;
}),
search: protectedProcedure
.meta({
openapi: {
summary: "Search MariaDB databases",
description: "Returns a paginated list of MariaDB databases matching the given filters. Supports searching by name, appName, description, project, and environment.",
},
})
.input(
z.object({
q: z.string().optional(),
@@ -593,6 +683,12 @@ export const mariadbRouter = createTRPCRouter({
}),
readLogs: protectedProcedure
.meta({
openapi: {
summary: "Read MariaDB container logs",
description: "Retrieves the Docker container logs for the specified MariaDB database, with support for tail count, time-based filtering, and text search.",
},
})
.input(
apiFindOneMariaDB.extend({
tail: z.number().int().min(1).max(10000).default(100),

View File

@@ -53,6 +53,12 @@ import {
import { cancelJobs } from "@/server/utils/backup";
export const mongoRouter = createTRPCRouter({
create: protectedProcedure
.meta({
openapi: {
summary: "Create a MongoDB database",
description: "Creates a new MongoDB database service with the specified configuration, sets up a persistent data volume, and registers it in the project.",
},
})
.input(apiCreateMongo)
.mutation(async ({ input, ctx }) => {
try {
@@ -117,6 +123,12 @@ export const mongoRouter = createTRPCRouter({
}
}),
one: protectedProcedure
.meta({
openapi: {
summary: "Get a MongoDB database by ID",
description: "Returns the full details of a MongoDB database service, including its environment and project configuration.",
},
})
.input(apiFindOneMongo)
.query(async ({ input, ctx }) => {
await checkServiceAccess(ctx, input.mongoId, "read");
@@ -135,6 +147,12 @@ export const mongoRouter = createTRPCRouter({
}),
start: protectedProcedure
.meta({
openapi: {
summary: "Start a MongoDB database",
description: "Starts the Docker container for the specified MongoDB database and sets its status to done.",
},
})
.input(apiFindOneMongo)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mongoId, {
@@ -160,6 +178,12 @@ export const mongoRouter = createTRPCRouter({
return service;
}),
stop: protectedProcedure
.meta({
openapi: {
summary: "Stop a MongoDB database",
description: "Stops the Docker container for the specified MongoDB database and sets its status to idle.",
},
})
.input(apiFindOneMongo)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mongoId, {
@@ -185,6 +209,12 @@ export const mongoRouter = createTRPCRouter({
return mongo;
}),
saveExternalPort: protectedProcedure
.meta({
openapi: {
summary: "Save the external port for a MongoDB database",
description: "Updates the external port mapping for the MongoDB database and triggers a redeployment. Validates that the port is not already in use.",
},
})
.input(apiSaveExternalPortMongo)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mongoId, {
@@ -218,6 +248,12 @@ export const mongoRouter = createTRPCRouter({
return mongo;
}),
deploy: protectedProcedure
.meta({
openapi: {
summary: "Deploy a MongoDB database",
description: "Triggers a deployment for the specified MongoDB database, rebuilding and restarting its Docker container with the current configuration.",
},
})
.input(apiDeployMongo)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mongoId, {
@@ -271,6 +307,12 @@ export const mongoRouter = createTRPCRouter({
}),
changeStatus: protectedProcedure
.meta({
openapi: {
summary: "Change MongoDB database status",
description: "Updates the application status of a MongoDB database without starting or stopping the container.",
},
})
.input(apiChangeMongoStatus)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mongoId, {
@@ -289,6 +331,12 @@ export const mongoRouter = createTRPCRouter({
return mongo;
}),
reload: protectedProcedure
.meta({
openapi: {
summary: "Reload a MongoDB database",
description: "Restarts the MongoDB database by stopping and then starting its Docker container.",
},
})
.input(apiResetMongo)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mongoId, {
@@ -321,6 +369,12 @@ export const mongoRouter = createTRPCRouter({
return true;
}),
remove: protectedProcedure
.meta({
openapi: {
summary: "Delete a MongoDB database",
description: "Removes the MongoDB database service, its Docker container, cancels associated backup jobs, and deletes the database record.",
},
})
.input(apiFindOneMongo)
.mutation(async ({ input, ctx }) => {
await checkServiceAccess(ctx, input.mongoId, "delete");
@@ -359,6 +413,12 @@ export const mongoRouter = createTRPCRouter({
return mongo;
}),
saveEnvironment: protectedProcedure
.meta({
openapi: {
summary: "Save environment variables for a MongoDB database",
description: "Updates the environment variables for the specified MongoDB database service.",
},
})
.input(apiSaveEnvironmentVariablesMongo)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mongoId, {
@@ -383,6 +443,12 @@ export const mongoRouter = createTRPCRouter({
return true;
}),
update: protectedProcedure
.meta({
openapi: {
summary: "Update a MongoDB database",
description: "Updates the configuration of an existing MongoDB database service.",
},
})
.input(apiUpdateMongo)
.mutation(async ({ input, ctx }) => {
const { mongoId, ...rest } = input;
@@ -409,6 +475,12 @@ export const mongoRouter = createTRPCRouter({
return true;
}),
changePassword: protectedProcedure
.meta({
openapi: {
summary: "Change MongoDB database password",
description: "Changes the password for the MongoDB database user by executing changeUserPassword via mongosh inside the running container and updating the stored password.",
},
})
.input(
z.object({
mongoId: z.string().min(1),
@@ -459,6 +531,12 @@ export const mongoRouter = createTRPCRouter({
return true;
}),
move: protectedProcedure
.meta({
openapi: {
summary: "Move a MongoDB database to another environment",
description: "Moves the MongoDB database to a different environment within the same project.",
},
})
.input(
z.object({
mongoId: z.string(),
@@ -495,6 +573,12 @@ export const mongoRouter = createTRPCRouter({
return updatedMongo;
}),
rebuild: protectedProcedure
.meta({
openapi: {
summary: "Rebuild a MongoDB database",
description: "Rebuilds the MongoDB database Docker container from scratch, pulling the latest image and recreating the service.",
},
})
.input(apiRebuildMongo)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mongoId, {
@@ -511,6 +595,12 @@ export const mongoRouter = createTRPCRouter({
return true;
}),
search: protectedProcedure
.meta({
openapi: {
summary: "Search MongoDB databases",
description: "Returns a paginated list of MongoDB databases matching the given filters. Supports searching by name, appName, description, project, and environment.",
},
})
.input(
z.object({
q: z.string().optional(),
@@ -604,6 +694,12 @@ export const mongoRouter = createTRPCRouter({
}),
readLogs: protectedProcedure
.meta({
openapi: {
summary: "Read MongoDB container logs",
description: "Retrieves the Docker container logs for the specified MongoDB database, with support for tail count, time-based filtering, and text search.",
},
})
.input(
apiFindOneMongo.extend({
tail: z.number().int().min(1).max(10000).default(100),

View File

@@ -75,6 +75,12 @@ async function getServiceOrganizationId(
export const mountRouter = createTRPCRouter({
create: protectedProcedure
.meta({
openapi: {
summary: "Create mount",
description: "Creates a new volume, bind, or file mount for a service. Checks service-level volume permissions and logs an audit event.",
},
})
.input(apiCreateMount)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.serviceId, {
@@ -90,6 +96,12 @@ export const mountRouter = createTRPCRouter({
return mount;
}),
remove: protectedProcedure
.meta({
openapi: {
summary: "Delete mount",
description: "Removes a mount by ID. Resolves the owning service to check volume delete permissions and logs an audit event.",
},
})
.input(apiRemoveMount)
.mutation(async ({ input, ctx }) => {
const mount = await findMountById(input.mountId);
@@ -116,6 +128,12 @@ export const mountRouter = createTRPCRouter({
}),
one: protectedProcedure
.meta({
openapi: {
summary: "Get mount",
description: "Returns a single mount by ID. Resolves the owning service to check volume read permissions.",
},
})
.input(apiFindOneMount)
.query(async ({ input, ctx }) => {
const mount = await findMountById(input.mountId);
@@ -136,6 +154,12 @@ export const mountRouter = createTRPCRouter({
return mount;
}),
update: protectedProcedure
.meta({
openapi: {
summary: "Update mount",
description: "Updates an existing mount. Resolves the owning service to check volume create permissions and logs an audit event.",
},
})
.input(apiUpdateMount)
.mutation(async ({ input, ctx }) => {
const mount = await findMountById(input.mountId);
@@ -162,6 +186,12 @@ export const mountRouter = createTRPCRouter({
return await updateMount(input.mountId, input);
}),
allNamedByApplicationId: protectedProcedure
.meta({
openapi: {
summary: "List named volumes by application",
description: "Returns Docker named volumes attached to the running container of a given application. Inspects the live container to retrieve mount information.",
},
})
.input(z.object({ applicationId: z.string().min(1) }))
.query(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -175,6 +205,12 @@ export const mountRouter = createTRPCRouter({
return mounts;
}),
listByServiceId: protectedProcedure
.meta({
openapi: {
summary: "List mounts by service",
description: "Returns all configured mounts for a given service (application, compose, or database). Verifies service access and organization ownership.",
},
})
.input(apiFindMountByApplicationId)
.query(async ({ input, ctx }) => {
await checkServiceAccess(ctx, input.serviceId, "read");

View File

@@ -54,6 +54,12 @@ import { cancelJobs } from "@/server/utils/backup";
export const mysqlRouter = createTRPCRouter({
create: protectedProcedure
.meta({
openapi: {
summary: "Create a MySQL database",
description: "Creates a new MySQL database service with the specified configuration, sets up a persistent data volume, and registers it in the project.",
},
})
.input(apiCreateMySql)
.mutation(async ({ input, ctx }) => {
try {
@@ -118,6 +124,12 @@ export const mysqlRouter = createTRPCRouter({
}
}),
one: protectedProcedure
.meta({
openapi: {
summary: "Get a MySQL database by ID",
description: "Returns the full details of a MySQL database service, including its environment and project configuration.",
},
})
.input(apiFindOneMySql)
.query(async ({ input, ctx }) => {
await checkServiceAccess(ctx, input.mysqlId, "read");
@@ -135,6 +147,12 @@ export const mysqlRouter = createTRPCRouter({
}),
start: protectedProcedure
.meta({
openapi: {
summary: "Start a MySQL database",
description: "Starts the Docker container for the specified MySQL database and sets its status to done.",
},
})
.input(apiFindOneMySql)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
@@ -160,6 +178,12 @@ export const mysqlRouter = createTRPCRouter({
return service;
}),
stop: protectedProcedure
.meta({
openapi: {
summary: "Stop a MySQL database",
description: "Stops the Docker container for the specified MySQL database and sets its status to idle.",
},
})
.input(apiFindOneMySql)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
@@ -184,6 +208,12 @@ export const mysqlRouter = createTRPCRouter({
return mongo;
}),
saveExternalPort: protectedProcedure
.meta({
openapi: {
summary: "Save the external port for a MySQL database",
description: "Updates the external port mapping for the MySQL database and triggers a redeployment. Validates that the port is not already in use.",
},
})
.input(apiSaveExternalPortMySql)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
@@ -217,6 +247,12 @@ export const mysqlRouter = createTRPCRouter({
return mysql;
}),
deploy: protectedProcedure
.meta({
openapi: {
summary: "Deploy a MySQL database",
description: "Triggers a deployment for the specified MySQL database, rebuilding and restarting its Docker container with the current configuration.",
},
})
.input(apiDeployMySql)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
@@ -270,6 +306,12 @@ export const mysqlRouter = createTRPCRouter({
}
}),
changeStatus: protectedProcedure
.meta({
openapi: {
summary: "Change MySQL database status",
description: "Updates the application status of a MySQL database without starting or stopping the container.",
},
})
.input(apiChangeMySqlStatus)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
@@ -288,6 +330,12 @@ export const mysqlRouter = createTRPCRouter({
return mongo;
}),
reload: protectedProcedure
.meta({
openapi: {
summary: "Reload a MySQL database",
description: "Restarts the MySQL database by stopping and then starting its Docker container.",
},
})
.input(apiResetMysql)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
@@ -319,6 +367,12 @@ export const mysqlRouter = createTRPCRouter({
return true;
}),
remove: protectedProcedure
.meta({
openapi: {
summary: "Delete a MySQL database",
description: "Removes the MySQL database service, its Docker container, cancels associated backup jobs, and deletes the database record.",
},
})
.input(apiFindOneMySql)
.mutation(async ({ input, ctx }) => {
await checkServiceAccess(ctx, input.mysqlId, "delete");
@@ -355,6 +409,12 @@ export const mysqlRouter = createTRPCRouter({
return mongo;
}),
saveEnvironment: protectedProcedure
.meta({
openapi: {
summary: "Save environment variables for a MySQL database",
description: "Updates the environment variables for the specified MySQL database service.",
},
})
.input(apiSaveEnvironmentVariablesMySql)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
@@ -379,6 +439,12 @@ export const mysqlRouter = createTRPCRouter({
return true;
}),
update: protectedProcedure
.meta({
openapi: {
summary: "Update a MySQL database",
description: "Updates the configuration of an existing MySQL database service.",
},
})
.input(apiUpdateMySql)
.mutation(async ({ input, ctx }) => {
const { mysqlId, ...rest } = input;
@@ -405,6 +471,12 @@ export const mysqlRouter = createTRPCRouter({
return true;
}),
changePassword: protectedProcedure
.meta({
openapi: {
summary: "Change MySQL database password",
description: "Changes the password for a MySQL user or root account by executing ALTER USER inside the running container and updating the stored password.",
},
})
.input(
z.object({
mysqlId: z.string().min(1),
@@ -462,6 +534,12 @@ export const mysqlRouter = createTRPCRouter({
return true;
}),
move: protectedProcedure
.meta({
openapi: {
summary: "Move a MySQL database to another environment",
description: "Moves the MySQL database to a different environment within the same project.",
},
})
.input(
z.object({
mysqlId: z.string(),
@@ -498,6 +576,12 @@ export const mysqlRouter = createTRPCRouter({
return updatedMysql;
}),
rebuild: protectedProcedure
.meta({
openapi: {
summary: "Rebuild a MySQL database",
description: "Rebuilds the MySQL database Docker container from scratch, pulling the latest image and recreating the service.",
},
})
.input(apiRebuildMysql)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
@@ -514,6 +598,12 @@ export const mysqlRouter = createTRPCRouter({
return true;
}),
search: protectedProcedure
.meta({
openapi: {
summary: "Search MySQL databases",
description: "Returns a paginated list of MySQL databases matching the given filters. Supports searching by name, appName, description, project, and environment.",
},
})
.input(
z.object({
q: z.string().optional(),
@@ -607,6 +697,12 @@ export const mysqlRouter = createTRPCRouter({
}),
readLogs: protectedProcedure
.meta({
openapi: {
summary: "Read MySQL container logs",
description: "Retrieves the Docker container logs for the specified MySQL database, with support for tail count, time-based filtering, and text search.",
},
})
.input(
apiFindOneMySql.extend({
tail: z.number().int().min(1).max(10000).default(100),

View File

@@ -95,6 +95,12 @@ import {
export const notificationRouter = createTRPCRouter({
createSlack: withPermission("notification", "create")
.meta({
openapi: {
summary: "Create Slack notification",
description: "Creates a new Slack notification provider for the current organization and logs an audit event.",
},
})
.input(apiCreateSlack)
.mutation(async ({ input, ctx }) => {
try {
@@ -114,6 +120,12 @@ export const notificationRouter = createTRPCRouter({
}
}),
updateSlack: withPermission("notification", "update")
.meta({
openapi: {
summary: "Update Slack notification",
description: "Updates an existing Slack notification provider. Verifies organization ownership before applying changes.",
},
})
.input(apiUpdateSlack)
.mutation(async ({ input, ctx }) => {
try {
@@ -140,6 +152,12 @@ export const notificationRouter = createTRPCRouter({
}
}),
testSlackConnection: withPermission("notification", "create")
.meta({
openapi: {
summary: "Test Slack connection",
description: "Sends a test message to the configured Slack channel to verify the webhook connection works.",
},
})
.input(apiTestSlackConnection)
.mutation(async ({ input }) => {
try {
@@ -157,6 +175,12 @@ export const notificationRouter = createTRPCRouter({
}
}),
createTelegram: withPermission("notification", "create")
.meta({
openapi: {
summary: "Create Telegram notification",
description: "Creates a new Telegram notification provider for the current organization and logs an audit event.",
},
})
.input(apiCreateTelegram)
.mutation(async ({ input, ctx }) => {
try {
@@ -179,6 +203,12 @@ export const notificationRouter = createTRPCRouter({
}),
updateTelegram: withPermission("notification", "update")
.meta({
openapi: {
summary: "Update Telegram notification",
description: "Updates an existing Telegram notification provider. Verifies organization ownership before applying changes.",
},
})
.input(apiUpdateTelegram)
.mutation(async ({ input, ctx }) => {
try {
@@ -209,6 +239,12 @@ export const notificationRouter = createTRPCRouter({
}
}),
testTelegramConnection: withPermission("notification", "create")
.meta({
openapi: {
summary: "Test Telegram connection",
description: "Sends a test message to the configured Telegram chat to verify the bot token and chat ID work.",
},
})
.input(apiTestTelegramConnection)
.mutation(async ({ input }) => {
try {
@@ -223,6 +259,12 @@ export const notificationRouter = createTRPCRouter({
}
}),
createDiscord: withPermission("notification", "create")
.meta({
openapi: {
summary: "Create Discord notification",
description: "Creates a new Discord notification provider for the current organization and logs an audit event.",
},
})
.input(apiCreateDiscord)
.mutation(async ({ input, ctx }) => {
try {
@@ -245,6 +287,12 @@ export const notificationRouter = createTRPCRouter({
}),
updateDiscord: withPermission("notification", "update")
.meta({
openapi: {
summary: "Update Discord notification",
description: "Updates an existing Discord notification provider. Verifies organization ownership before applying changes.",
},
})
.input(apiUpdateDiscord)
.mutation(async ({ input, ctx }) => {
try {
@@ -276,6 +324,12 @@ export const notificationRouter = createTRPCRouter({
}),
testDiscordConnection: withPermission("notification", "create")
.meta({
openapi: {
summary: "Test Discord connection",
description: "Sends a test embed message to the configured Discord webhook to verify the connection works.",
},
})
.input(apiTestDiscordConnection)
.mutation(async ({ input }) => {
try {
@@ -298,6 +352,12 @@ export const notificationRouter = createTRPCRouter({
}
}),
createEmail: withPermission("notification", "create")
.meta({
openapi: {
summary: "Create Email notification",
description: "Creates a new SMTP email notification provider for the current organization and logs an audit event.",
},
})
.input(apiCreateEmail)
.mutation(async ({ input, ctx }) => {
try {
@@ -316,6 +376,12 @@ export const notificationRouter = createTRPCRouter({
}
}),
updateEmail: withPermission("notification", "update")
.meta({
openapi: {
summary: "Update Email notification",
description: "Updates an existing SMTP email notification provider. Verifies organization ownership before applying changes.",
},
})
.input(apiUpdateEmail)
.mutation(async ({ input, ctx }) => {
try {
@@ -346,6 +412,12 @@ export const notificationRouter = createTRPCRouter({
}
}),
testEmailConnection: withPermission("notification", "create")
.meta({
openapi: {
summary: "Test Email connection",
description: "Sends a test email via the configured SMTP settings to verify the connection works.",
},
})
.input(apiTestEmailConnection)
.mutation(async ({ input }) => {
try {
@@ -364,6 +436,12 @@ export const notificationRouter = createTRPCRouter({
}
}),
createResend: withPermission("notification", "create")
.meta({
openapi: {
summary: "Create Resend notification",
description: "Creates a new Resend email notification provider for the current organization and logs an audit event.",
},
})
.input(apiCreateResend)
.mutation(async ({ input, ctx }) => {
try {
@@ -382,6 +460,12 @@ export const notificationRouter = createTRPCRouter({
}
}),
updateResend: withPermission("notification", "update")
.meta({
openapi: {
summary: "Update Resend notification",
description: "Updates an existing Resend email notification provider. Verifies organization ownership before applying changes.",
},
})
.input(apiUpdateResend)
.mutation(async ({ input, ctx }) => {
try {
@@ -412,6 +496,12 @@ export const notificationRouter = createTRPCRouter({
}
}),
testResendConnection: withPermission("notification", "create")
.meta({
openapi: {
summary: "Test Resend connection",
description: "Sends a test email via Resend to verify the API key and configuration work.",
},
})
.input(apiTestResendConnection)
.mutation(async ({ input }) => {
try {
@@ -430,6 +520,12 @@ export const notificationRouter = createTRPCRouter({
}
}),
remove: withPermission("notification", "delete")
.meta({
openapi: {
summary: "Delete notification",
description: "Removes a notification provider by ID. Verifies organization ownership and logs an audit event before deletion.",
},
})
.input(apiFindOneNotification)
.mutation(async ({ input, ctx }) => {
try {
@@ -458,6 +554,12 @@ export const notificationRouter = createTRPCRouter({
}
}),
one: withPermission("notification", "read")
.meta({
openapi: {
summary: "Get notification",
description: "Returns a single notification provider by ID. Verifies the caller belongs to the same organization.",
},
})
.input(apiFindOneNotification)
.query(async ({ input, ctx }) => {
const notification = await findNotificationById(input.notificationId);
@@ -469,7 +571,14 @@ export const notificationRouter = createTRPCRouter({
}
return notification;
}),
all: withPermission("notification", "read").query(async ({ ctx }) => {
all: withPermission("notification", "read")
.meta({
openapi: {
summary: "List all notifications",
description: "Returns all notification providers for the current organization, including all provider-specific details (Slack, Telegram, Discord, etc.).",
},
})
.query(async ({ ctx }) => {
return await db.query.notifications.findMany({
with: {
slack: true,
@@ -490,6 +599,12 @@ export const notificationRouter = createTRPCRouter({
});
}),
receiveNotification: publicProcedure
.meta({
openapi: {
summary: "Receive server threshold notification",
description: "Public endpoint that receives CPU/memory threshold alerts from Dokploy or remote servers. Validates the token and dispatches notifications to all configured providers.",
},
})
.input(
z.object({
ServerType: z.enum(["Dokploy", "Remote"]).default("Dokploy"),
@@ -551,6 +666,12 @@ export const notificationRouter = createTRPCRouter({
}
}),
createGotify: withPermission("notification", "create")
.meta({
openapi: {
summary: "Create Gotify notification",
description: "Creates a new Gotify notification provider for the current organization and logs an audit event.",
},
})
.input(apiCreateGotify)
.mutation(async ({ input, ctx }) => {
try {
@@ -569,6 +690,12 @@ export const notificationRouter = createTRPCRouter({
}
}),
updateGotify: withPermission("notification", "update")
.meta({
openapi: {
summary: "Update Gotify notification",
description: "Updates an existing Gotify notification provider. Verifies organization ownership before applying changes.",
},
})
.input(apiUpdateGotify)
.mutation(async ({ input, ctx }) => {
try {
@@ -598,6 +725,12 @@ export const notificationRouter = createTRPCRouter({
}
}),
testGotifyConnection: withPermission("notification", "create")
.meta({
openapi: {
summary: "Test Gotify connection",
description: "Sends a test notification to the configured Gotify server to verify the connection works.",
},
})
.input(apiTestGotifyConnection)
.mutation(async ({ input }) => {
try {
@@ -616,6 +749,12 @@ export const notificationRouter = createTRPCRouter({
}
}),
createNtfy: withPermission("notification", "create")
.meta({
openapi: {
summary: "Create ntfy notification",
description: "Creates a new ntfy notification provider for the current organization and logs an audit event.",
},
})
.input(apiCreateNtfy)
.mutation(async ({ input, ctx }) => {
try {
@@ -634,6 +773,12 @@ export const notificationRouter = createTRPCRouter({
}
}),
updateNtfy: withPermission("notification", "update")
.meta({
openapi: {
summary: "Update ntfy notification",
description: "Updates an existing ntfy notification provider. Verifies organization ownership before applying changes.",
},
})
.input(apiUpdateNtfy)
.mutation(async ({ input, ctx }) => {
try {
@@ -663,6 +808,12 @@ export const notificationRouter = createTRPCRouter({
}
}),
testNtfyConnection: withPermission("notification", "create")
.meta({
openapi: {
summary: "Test ntfy connection",
description: "Sends a test notification to the configured ntfy topic to verify the connection works.",
},
})
.input(apiTestNtfyConnection)
.mutation(async ({ input }) => {
try {
@@ -686,6 +837,12 @@ export const notificationRouter = createTRPCRouter({
}
}),
createMattermost: withPermission("notification", "create")
.meta({
openapi: {
summary: "Create Mattermost notification",
description: "Creates a new Mattermost notification provider for the current organization and logs an audit event.",
},
})
.input(apiCreateMattermost)
.mutation(async ({ input, ctx }) => {
try {
@@ -707,6 +864,12 @@ export const notificationRouter = createTRPCRouter({
}
}),
updateMattermost: withPermission("notification", "update")
.meta({
openapi: {
summary: "Update Mattermost notification",
description: "Updates an existing Mattermost notification provider. Verifies organization ownership before applying changes.",
},
})
.input(apiUpdateMattermost)
.mutation(async ({ input, ctx }) => {
try {
@@ -736,6 +899,12 @@ export const notificationRouter = createTRPCRouter({
}
}),
testMattermostConnection: withPermission("notification", "create")
.meta({
openapi: {
summary: "Test Mattermost connection",
description: "Sends a test message to the configured Mattermost webhook to verify the connection works.",
},
})
.input(apiTestMattermostConnection)
.mutation(async ({ input }) => {
try {
@@ -754,6 +923,12 @@ export const notificationRouter = createTRPCRouter({
}
}),
createCustom: withPermission("notification", "create")
.meta({
openapi: {
summary: "Create custom webhook notification",
description: "Creates a new custom webhook notification provider for the current organization and logs an audit event.",
},
})
.input(apiCreateCustom)
.mutation(async ({ input, ctx }) => {
try {
@@ -772,6 +947,12 @@ export const notificationRouter = createTRPCRouter({
}
}),
updateCustom: withPermission("notification", "update")
.meta({
openapi: {
summary: "Update custom webhook notification",
description: "Updates an existing custom webhook notification provider. Verifies organization ownership before applying changes.",
},
})
.input(apiUpdateCustom)
.mutation(async ({ input, ctx }) => {
try {
@@ -798,6 +979,12 @@ export const notificationRouter = createTRPCRouter({
}
}),
testCustomConnection: withPermission("notification", "create")
.meta({
openapi: {
summary: "Test custom webhook connection",
description: "Sends a test payload to the configured custom webhook URL to verify the connection works.",
},
})
.input(apiTestCustomConnection)
.mutation(async ({ input }) => {
try {
@@ -816,6 +1003,12 @@ export const notificationRouter = createTRPCRouter({
}
}),
createLark: withPermission("notification", "create")
.meta({
openapi: {
summary: "Create Lark notification",
description: "Creates a new Lark notification provider for the current organization and logs an audit event.",
},
})
.input(apiCreateLark)
.mutation(async ({ input, ctx }) => {
try {
@@ -834,6 +1027,12 @@ export const notificationRouter = createTRPCRouter({
}
}),
updateLark: withPermission("notification", "update")
.meta({
openapi: {
summary: "Update Lark notification",
description: "Updates an existing Lark notification provider. Verifies organization ownership before applying changes.",
},
})
.input(apiUpdateLark)
.mutation(async ({ input, ctx }) => {
try {
@@ -863,6 +1062,12 @@ export const notificationRouter = createTRPCRouter({
}
}),
testLarkConnection: withPermission("notification", "create")
.meta({
openapi: {
summary: "Test Lark connection",
description: "Sends a test message to the configured Lark webhook to verify the connection works.",
},
})
.input(apiTestLarkConnection)
.mutation(async ({ input }) => {
try {
@@ -882,6 +1087,12 @@ export const notificationRouter = createTRPCRouter({
}
}),
createTeams: withPermission("notification", "create")
.meta({
openapi: {
summary: "Create Teams notification",
description: "Creates a new Microsoft Teams notification provider for the current organization and logs an audit event.",
},
})
.input(apiCreateTeams)
.mutation(async ({ input, ctx }) => {
try {
@@ -900,6 +1111,12 @@ export const notificationRouter = createTRPCRouter({
}
}),
updateTeams: withPermission("notification", "update")
.meta({
openapi: {
summary: "Update Teams notification",
description: "Updates an existing Microsoft Teams notification provider. Verifies organization ownership before applying changes.",
},
})
.input(apiUpdateTeams)
.mutation(async ({ input, ctx }) => {
try {
@@ -929,6 +1146,12 @@ export const notificationRouter = createTRPCRouter({
}
}),
testTeamsConnection: withPermission("notification", "create")
.meta({
openapi: {
summary: "Test Teams connection",
description: "Sends a test message to the configured Microsoft Teams webhook to verify the connection works.",
},
})
.input(apiTestTeamsConnection)
.mutation(async ({ input }) => {
try {
@@ -946,6 +1169,12 @@ export const notificationRouter = createTRPCRouter({
}
}),
createPushover: withPermission("notification", "create")
.meta({
openapi: {
summary: "Create Pushover notification",
description: "Creates a new Pushover notification provider for the current organization and logs an audit event.",
},
})
.input(apiCreatePushover)
.mutation(async ({ input, ctx }) => {
try {
@@ -967,6 +1196,12 @@ export const notificationRouter = createTRPCRouter({
}
}),
updatePushover: withPermission("notification", "update")
.meta({
openapi: {
summary: "Update Pushover notification",
description: "Updates an existing Pushover notification provider. Verifies organization ownership before applying changes.",
},
})
.input(apiUpdatePushover)
.mutation(async ({ input, ctx }) => {
try {
@@ -996,6 +1231,12 @@ export const notificationRouter = createTRPCRouter({
}
}),
testPushoverConnection: withPermission("notification", "create")
.meta({
openapi: {
summary: "Test Pushover connection",
description: "Sends a test notification to the configured Pushover account to verify the connection works.",
},
})
.input(apiTestPushoverConnection)
.mutation(async ({ input }) => {
try {
@@ -1013,7 +1254,14 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
getEmailProviders: withPermission("notification", "read").query(
getEmailProviders: withPermission("notification", "read")
.meta({
openapi: {
summary: "List email notification providers",
description: "Returns all notification providers that support email (SMTP and Resend) for the current organization.",
},
})
.query(
async ({ ctx }) => {
return await db.query.notifications.findMany({
where: eq(

View File

@@ -15,6 +15,12 @@ import {
import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc";
export const organizationRouter = createTRPCRouter({
create: protectedProcedure
.meta({
openapi: {
summary: "Create an organization",
description: "Create a new organization and add the current user as the owner. Only owners and admins can create organizations in self-hosted mode.",
},
})
.input(
z.object({
name: z.string(),
@@ -65,7 +71,14 @@ export const organizationRouter = createTRPCRouter({
});
return result;
}),
all: protectedProcedure.query(async ({ ctx }) => {
all: protectedProcedure
.meta({
openapi: {
summary: "List all organizations",
description: "Retrieve all organizations the current user is a member of, including their membership details.",
},
})
.query(async ({ ctx }) => {
const memberResult = await db.query.organization.findMany({
where: (organization) =>
exists(
@@ -88,6 +101,12 @@ export const organizationRouter = createTRPCRouter({
return memberResult;
}),
one: protectedProcedure
.meta({
openapi: {
summary: "Get an organization by ID",
description: "Retrieve a single organization by its ID. The current user must be a member of the organization.",
},
})
.input(
z.object({
organizationId: z.string(),
@@ -114,6 +133,12 @@ export const organizationRouter = createTRPCRouter({
});
}),
update: protectedProcedure
.meta({
openapi: {
summary: "Update an organization",
description: "Update the name and logo of an organization. Only the organization owner can perform this action.",
},
})
.input(
z.object({
organizationId: z.string(),
@@ -178,6 +203,12 @@ export const organizationRouter = createTRPCRouter({
return result[0];
}),
delete: protectedProcedure
.meta({
openapi: {
summary: "Delete an organization",
description: "Delete an organization by ID. Only the owner can delete it, and they must retain at least one organization.",
},
})
.input(
z.object({
organizationId: z.string(),
@@ -248,6 +279,12 @@ export const organizationRouter = createTRPCRouter({
return result;
}),
inviteMember: withPermission("member", "create")
.meta({
openapi: {
summary: "Invite a member to organization",
description: "Create a pending invitation for a user by email to join the active organization with the specified role. Checks for existing membership and pending invitations. Supports custom roles.",
},
})
.input(
z.object({
email: z.string().email(),
@@ -335,13 +372,26 @@ export const organizationRouter = createTRPCRouter({
return created;
}),
allInvitations: withPermission("member", "create").query(async ({ ctx }) => {
allInvitations: withPermission("member", "create")
.meta({
openapi: {
summary: "List all organization invitations",
description: "Retrieve all invitations for the active organization, ordered by status and expiration date.",
},
})
.query(async ({ ctx }) => {
return await db.query.invitation.findMany({
where: eq(invitation.organizationId, ctx.session.activeOrganizationId),
orderBy: [desc(invitation.status), desc(invitation.expiresAt)],
});
}),
removeInvitation: withPermission("member", "create")
.meta({
openapi: {
summary: "Remove an invitation",
description: "Delete a pending invitation by ID. Only invitations belonging to the active organization can be removed.",
},
})
.input(z.object({ invitationId: z.string() }))
.mutation(async ({ ctx, input }) => {
const invitationResult = await db.query.invitation.findFirst({
@@ -377,6 +427,12 @@ export const organizationRouter = createTRPCRouter({
return result;
}),
updateMemberRole: withPermission("member", "update")
.meta({
openapi: {
summary: "Update member role",
description: "Change the role of a member in the active organization. Users cannot change their own role, and the owner role is nontransferable. Only owners can change admin roles. Supports custom roles.",
},
})
.input(
z.object({
memberId: z.string(),
@@ -463,6 +519,12 @@ export const organizationRouter = createTRPCRouter({
return true;
}),
setDefault: protectedProcedure
.meta({
openapi: {
summary: "Set default organization",
description: "Set an organization as the default for the current user. Unsets any previous default and marks the specified organization as the new default.",
},
})
.input(
z.object({
organizationId: z.string().min(1),
@@ -509,7 +571,14 @@ export const organizationRouter = createTRPCRouter({
});
return { success: true };
}),
active: protectedProcedure.query(async ({ ctx }) => {
active: protectedProcedure
.meta({
openapi: {
summary: "Get active organization",
description: "Retrieve the organization that is currently active in the user's session. Returns null if no organization is active.",
},
})
.query(async ({ ctx }) => {
if (!ctx.session.activeOrganizationId) {
return null;
}

View File

@@ -50,6 +50,12 @@ const resolvePatchServiceId = (patch: {
export const patchRouter = createTRPCRouter({
create: protectedProcedure
.meta({
openapi: {
summary: "Create patch",
description: "Creates a new file patch for an application or compose service. Checks service-level permissions and logs an audit event.",
},
})
.input(apiCreatePatch)
.mutation(async ({ input, ctx }) => {
const serviceId = input.applicationId ?? input.composeId;
@@ -73,7 +79,15 @@ export const patchRouter = createTRPCRouter({
return result;
}),
one: protectedProcedure.input(apiFindPatch).query(async ({ input, ctx }) => {
one: protectedProcedure
.meta({
openapi: {
summary: "Get patch",
description: "Returns a single patch by ID. Resolves the associated service to verify read permissions.",
},
})
.input(apiFindPatch)
.query(async ({ input, ctx }) => {
const patch = await findPatchById(input.patchId);
const serviceId = resolvePatchServiceId(patch);
await checkServicePermissionAndAccess(ctx, serviceId, {
@@ -83,6 +97,12 @@ export const patchRouter = createTRPCRouter({
}),
byEntityId: protectedProcedure
.meta({
openapi: {
summary: "List patches by entity",
description: "Returns all patches associated with a given application or compose service.",
},
})
.input(
z.object({ id: z.string(), type: z.enum(["application", "compose"]) }),
)
@@ -94,6 +114,12 @@ export const patchRouter = createTRPCRouter({
}),
update: protectedProcedure
.meta({
openapi: {
summary: "Update patch",
description: "Updates the content or configuration of an existing patch. Resolves the associated service to verify permissions and logs an audit event.",
},
})
.input(apiUpdatePatch)
.mutation(async ({ input, ctx }) => {
const patch = await findPatchById(input.patchId);
@@ -114,6 +140,12 @@ export const patchRouter = createTRPCRouter({
}),
delete: protectedProcedure
.meta({
openapi: {
summary: "Delete patch",
description: "Deletes a patch by ID. Resolves the associated service to verify delete permissions and logs an audit event.",
},
})
.input(apiDeletePatch)
.mutation(async ({ input, ctx }) => {
const patch = await findPatchById(input.patchId);
@@ -133,6 +165,12 @@ export const patchRouter = createTRPCRouter({
}),
toggleEnabled: protectedProcedure
.meta({
openapi: {
summary: "Toggle patch enabled state",
description: "Enables or disables a patch without deleting it. Resolves the associated service to verify permissions and logs an audit event.",
},
})
.input(apiTogglePatchEnabled)
.mutation(async ({ input, ctx }) => {
const patch = await findPatchById(input.patchId);
@@ -155,6 +193,12 @@ export const patchRouter = createTRPCRouter({
// Repository Operations
ensureRepo: protectedProcedure
.meta({
openapi: {
summary: "Ensure patch repository exists",
description: "Ensures a patch repository is initialized for the given application or compose service. Creates the repo if it does not exist and logs an audit event.",
},
})
.input(
z.object({
id: z.string(),
@@ -179,6 +223,12 @@ export const patchRouter = createTRPCRouter({
}),
readRepoDirectories: protectedProcedure
.meta({
openapi: {
summary: "List patch repository directories",
description: "Reads the directory listing at a given path inside the patch repository for an application or compose service.",
},
})
.input(
z.object({
id: z.string().min(1),
@@ -202,6 +252,12 @@ export const patchRouter = createTRPCRouter({
}),
readRepoFile: protectedProcedure
.meta({
openapi: {
summary: "Read patch repository file",
description: "Reads a file from the patch repository. For delete-type patches it returns the current repo content; otherwise returns the patch content if available, falling back to the repo file.",
},
})
.input(
z.object({
id: z.string().min(1),
@@ -241,6 +297,12 @@ export const patchRouter = createTRPCRouter({
}),
saveFileAsPatch: protectedProcedure
.meta({
openapi: {
summary: "Save file as patch",
description: "Creates or updates a patch record from file content. If a patch already exists for the file path, it updates the existing patch; otherwise creates a new one.",
},
})
.input(
z.object({
id: z.string().min(1),
@@ -291,6 +353,12 @@ export const patchRouter = createTRPCRouter({
}),
markFileForDeletion: protectedProcedure
.meta({
openapi: {
summary: "Mark file for deletion",
description: "Creates a delete-type patch that will remove the specified file from the service on next deployment. Logs an audit event.",
},
})
.input(
z.object({
id: z.string().min(1),
@@ -318,6 +386,12 @@ export const patchRouter = createTRPCRouter({
}),
cleanPatchRepos: adminProcedure
.meta({
openapi: {
summary: "Clean patch repositories",
description: "Removes all patch repository working directories on the local or a specified remote server. Admin-only operation that logs an audit event.",
},
})
.input(z.object({ serverId: z.string().optional() }))
.mutation(async ({ input, ctx }) => {
await cleanPatchRepos(input.serverId);

View File

@@ -16,6 +16,12 @@ import {
export const portRouter = createTRPCRouter({
create: protectedProcedure
.meta({
openapi: {
summary: "Create a port",
description: "Creates a new port mapping for an application, binding a published port to a target port. Logs an audit entry.",
},
})
.input(apiCreatePort)
.mutation(async ({ input, ctx }) => {
try {
@@ -39,6 +45,12 @@ export const portRouter = createTRPCRouter({
}
}),
one: protectedProcedure
.meta({
openapi: {
summary: "Get a port",
description: "Returns a single port mapping by its ID, including the associated application details.",
},
})
.input(apiFindOnePort)
.query(async ({ input, ctx }) => {
try {
@@ -58,6 +70,12 @@ export const portRouter = createTRPCRouter({
}
}),
delete: protectedProcedure
.meta({
openapi: {
summary: "Delete a port",
description: "Deletes a port mapping by its ID and logs an audit entry with the published and target port details.",
},
})
.input(apiFindOnePort)
.mutation(async ({ input, ctx }) => {
const port = await finPortById(input.portId);
@@ -85,6 +103,12 @@ export const portRouter = createTRPCRouter({
}
}),
update: protectedProcedure
.meta({
openapi: {
summary: "Update a port",
description: "Updates an existing port mapping's configuration and logs an audit entry.",
},
})
.input(apiUpdatePort)
.mutation(async ({ input, ctx }) => {
const port = await finPortById(input.portId);

View File

@@ -9,6 +9,7 @@ import {
findEnvironmentById,
findPostgresById,
findProjectById,
getAccessibleServerIds,
getContainerLogs,
getMountPath,
getServiceContainerCommand,
@@ -21,7 +22,6 @@ import {
stopService,
stopServiceRemote,
updatePostgresById,
getAccessibleServerIds,
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import {
@@ -55,6 +55,12 @@ import { cancelJobs } from "@/server/utils/backup";
export const postgresRouter = createTRPCRouter({
create: protectedProcedure
.meta({
openapi: {
summary: "Create a PostgreSQL database",
description: "Creates a new PostgreSQL database service with the specified configuration, sets up a persistent data volume, and registers it in the project.",
},
})
.input(apiCreatePostgres)
.mutation(async ({ input, ctx }) => {
try {
@@ -121,6 +127,12 @@ export const postgresRouter = createTRPCRouter({
}
}),
one: protectedProcedure
.meta({
openapi: {
summary: "Get a PostgreSQL database by ID",
description: "Returns the full details of a PostgreSQL database service, including its environment and project configuration.",
},
})
.input(apiFindOnePostgres)
.query(async ({ input, ctx }) => {
await checkServiceAccess(ctx, input.postgresId, "read");
@@ -139,6 +151,12 @@ export const postgresRouter = createTRPCRouter({
}),
start: protectedProcedure
.meta({
openapi: {
summary: "Start a PostgreSQL database",
description: "Starts the Docker container for the specified PostgreSQL database and sets its status to done.",
},
})
.input(apiFindOnePostgres)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.postgresId, {
@@ -164,6 +182,12 @@ export const postgresRouter = createTRPCRouter({
return service;
}),
stop: protectedProcedure
.meta({
openapi: {
summary: "Stop a PostgreSQL database",
description: "Stops the Docker container for the specified PostgreSQL database and sets its status to idle.",
},
})
.input(apiFindOnePostgres)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.postgresId, {
@@ -188,6 +212,12 @@ export const postgresRouter = createTRPCRouter({
return postgres;
}),
saveExternalPort: protectedProcedure
.meta({
openapi: {
summary: "Save the external port for a PostgreSQL database",
description: "Updates the external port mapping for the PostgreSQL database and triggers a redeployment. Validates that the port is not already in use.",
},
})
.input(apiSaveExternalPortPostgres)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.postgresId, {
@@ -221,6 +251,12 @@ export const postgresRouter = createTRPCRouter({
return postgres;
}),
deploy: protectedProcedure
.meta({
openapi: {
summary: "Deploy a PostgreSQL database",
description: "Triggers a deployment for the specified PostgreSQL database, rebuilding and restarting its Docker container with the current configuration.",
},
})
.input(apiDeployPostgres)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.postgresId, {
@@ -276,6 +312,12 @@ export const postgresRouter = createTRPCRouter({
}),
changeStatus: protectedProcedure
.meta({
openapi: {
summary: "Change PostgreSQL database status",
description: "Updates the application status of a PostgreSQL database without starting or stopping the container.",
},
})
.input(apiChangePostgresStatus)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.postgresId, {
@@ -294,6 +336,12 @@ export const postgresRouter = createTRPCRouter({
return postgres;
}),
remove: protectedProcedure
.meta({
openapi: {
summary: "Delete a PostgreSQL database",
description: "Removes the PostgreSQL database service, its Docker container, cancels associated backup jobs, and deletes the database record.",
},
})
.input(apiFindOnePostgres)
.mutation(async ({ input, ctx }) => {
await checkServiceAccess(ctx, input.postgresId, "delete");
@@ -332,6 +380,12 @@ export const postgresRouter = createTRPCRouter({
return postgres;
}),
saveEnvironment: protectedProcedure
.meta({
openapi: {
summary: "Save environment variables for a PostgreSQL database",
description: "Updates the environment variables for the specified PostgreSQL database service.",
},
})
.input(apiSaveEnvironmentVariablesPostgres)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.postgresId, {
@@ -356,6 +410,12 @@ export const postgresRouter = createTRPCRouter({
return true;
}),
reload: protectedProcedure
.meta({
openapi: {
summary: "Reload a PostgreSQL database",
description: "Restarts the PostgreSQL database by stopping and then starting its Docker container.",
},
})
.input(apiResetPostgres)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.postgresId, {
@@ -388,6 +448,12 @@ export const postgresRouter = createTRPCRouter({
return true;
}),
update: protectedProcedure
.meta({
openapi: {
summary: "Update a PostgreSQL database",
description: "Updates the configuration of an existing PostgreSQL database service.",
},
})
.input(apiUpdatePostgres)
.mutation(async ({ input, ctx }) => {
const { postgresId, ...rest } = input;
@@ -415,6 +481,12 @@ export const postgresRouter = createTRPCRouter({
return true;
}),
changePassword: protectedProcedure
.meta({
openapi: {
summary: "Change PostgreSQL database password",
description: "Changes the password for the PostgreSQL database user by executing ALTER USER inside the running container and updating the stored password.",
},
})
.input(
z.object({
postgresId: z.string().min(1),
@@ -465,6 +537,12 @@ export const postgresRouter = createTRPCRouter({
return true;
}),
move: protectedProcedure
.meta({
openapi: {
summary: "Move a PostgreSQL database to another environment",
description: "Moves the PostgreSQL database to a different environment within the same project.",
},
})
.input(
z.object({
postgresId: z.string(),
@@ -501,6 +579,12 @@ export const postgresRouter = createTRPCRouter({
return updatedPostgres;
}),
rebuild: protectedProcedure
.meta({
openapi: {
summary: "Rebuild a PostgreSQL database",
description: "Rebuilds the PostgreSQL database Docker container from scratch, pulling the latest image and recreating the service.",
},
})
.input(apiRebuildPostgres)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.postgresId, {
@@ -517,6 +601,12 @@ export const postgresRouter = createTRPCRouter({
return true;
}),
search: protectedProcedure
.meta({
openapi: {
summary: "Search PostgreSQL databases",
description: "Returns a paginated list of PostgreSQL databases matching the given filters. Supports searching by name, appName, description, project, and environment.",
},
})
.input(
z.object({
q: z.string().optional(),
@@ -617,6 +707,12 @@ export const postgresRouter = createTRPCRouter({
}),
readLogs: protectedProcedure
.meta({
openapi: {
summary: "Read PostgreSQL container logs",
description: "Retrieves the Docker container logs for the specified PostgreSQL database, with support for tail count, time-based filtering, and text search.",
},
})
.input(
apiFindOnePostgres.extend({
tail: z.number().int().min(1).max(10000).default(100),

View File

@@ -16,6 +16,12 @@ import { createTRPCRouter, protectedProcedure } from "../trpc";
export const previewDeploymentRouter = createTRPCRouter({
all: protectedProcedure
.meta({
openapi: {
summary: "List preview deployments",
description: "Returns all preview deployments associated with the given application.",
},
})
.input(apiFindAllByApplication)
.query(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -25,6 +31,12 @@ export const previewDeploymentRouter = createTRPCRouter({
}),
one: protectedProcedure
.meta({
openapi: {
summary: "Get a preview deployment",
description: "Returns the details of a specific preview deployment by its ID.",
},
})
.input(z.object({ previewDeploymentId: z.string() }))
.query(async ({ input, ctx }) => {
const previewDeployment = await findPreviewDeploymentById(
@@ -39,6 +51,12 @@ export const previewDeploymentRouter = createTRPCRouter({
}),
delete: protectedProcedure
.meta({
openapi: {
summary: "Delete a preview deployment",
description: "Permanently removes a preview deployment and its associated resources.",
},
})
.input(z.object({ previewDeploymentId: z.string() }))
.mutation(async ({ input, ctx }) => {
const previewDeployment = await findPreviewDeploymentById(
@@ -59,6 +77,12 @@ export const previewDeploymentRouter = createTRPCRouter({
}),
redeploy: protectedProcedure
.meta({
openapi: {
summary: "Redeploy a preview deployment",
description: "Triggers a rebuild of an existing preview deployment by adding a new job to the deployment queue.",
},
})
.input(
z.object({
previewDeploymentId: z.string(),

View File

@@ -67,6 +67,12 @@ import {
export const projectRouter = createTRPCRouter({
create: protectedProcedure
.meta({
openapi: {
summary: "Create a project",
description: "Creates a new project in the current organization with a default environment. Validates server availability for cloud deployments.",
},
})
.input(apiCreateProject)
.mutation(async ({ ctx, input }) => {
try {
@@ -106,6 +112,12 @@ export const projectRouter = createTRPCRouter({
}),
one: protectedProcedure
.meta({
openapi: {
summary: "Get a project",
description: "Retrieves a project by its ID with all environments and services. Filters services based on the user's access permissions.",
},
})
.input(apiFindOneProject)
.query(async ({ input, ctx }) => {
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
@@ -193,7 +205,14 @@ export const projectRouter = createTRPCRouter({
}
return project;
}),
all: protectedProcedure.query(async ({ ctx }) => {
all: protectedProcedure
.meta({
openapi: {
summary: "List all projects",
description: "Returns all projects in the current organization with their environments and services. Filters results based on the user's access permissions.",
},
})
.query(async ({ ctx }) => {
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
const { accessedProjects, accessedEnvironments, accessedServices } =
await findMemberByUserId(ctx.user.id, ctx.session.activeOrganizationId);
@@ -375,7 +394,14 @@ export const projectRouter = createTRPCRouter({
});
}),
allForPermissions: withPermission("member", "update").query(
allForPermissions: withPermission("member", "update")
.meta({
openapi: {
summary: "List all projects for permissions",
description: "Returns all projects with their environments and services for the permissions management UI. Requires member update permission.",
},
})
.query(
async ({ ctx }) => {
return await db.query.projects.findMany({
where: eq(projects.organizationId, ctx.session.activeOrganizationId),
@@ -488,6 +514,12 @@ export const projectRouter = createTRPCRouter({
),
search: protectedProcedure
.meta({
openapi: {
summary: "Search projects",
description: "Searches projects by name or description with pagination. Respects project-level access control for non-admin users.",
},
})
.input(
z.object({
q: z.string().optional(),
@@ -565,6 +597,12 @@ export const projectRouter = createTRPCRouter({
}),
remove: protectedProcedure
.meta({
openapi: {
summary: "Delete a project",
description: "Permanently deletes a project and all its associated environments, services, and resources.",
},
})
.input(apiRemoveProject)
.mutation(async ({ input, ctx }) => {
try {
@@ -592,6 +630,12 @@ export const projectRouter = createTRPCRouter({
}
}),
update: protectedProcedure
.meta({
openapi: {
summary: "Update a project",
description: "Updates a project's name, description, or environment variables. Validates organization ownership and project-level access permissions.",
},
})
.input(apiUpdateProject)
.mutation(async ({ input, ctx }) => {
try {
@@ -640,6 +684,12 @@ export const projectRouter = createTRPCRouter({
}
}),
duplicate: protectedProcedure
.meta({
openapi: {
summary: "Duplicate a project or environment",
description: "Duplicates services from a source environment into a new project or into the same project. Copies applications, compose services, databases, and their associated domains, mounts, ports, and backups.",
},
})
.input(
z.object({
sourceEnvironmentId: z.string(),

View File

@@ -15,6 +15,12 @@ import {
export const redirectsRouter = createTRPCRouter({
create: protectedProcedure
.meta({
openapi: {
summary: "Create a redirect",
description: "Creates a new redirect rule for an application using a regex pattern. Logs an audit entry.",
},
})
.input(apiCreateRedirect)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -31,6 +37,12 @@ export const redirectsRouter = createTRPCRouter({
}),
one: protectedProcedure
.meta({
openapi: {
summary: "Get a redirect",
description: "Returns a single redirect rule by its ID.",
},
})
.input(apiFindOneRedirect)
.query(async ({ input, ctx }) => {
const redirect = await findRedirectById(input.redirectId);
@@ -41,6 +53,12 @@ export const redirectsRouter = createTRPCRouter({
}),
delete: protectedProcedure
.meta({
openapi: {
summary: "Delete a redirect",
description: "Deletes a redirect rule by its ID and logs an audit entry.",
},
})
.input(apiFindOneRedirect)
.mutation(async ({ input, ctx }) => {
const redirect = await findRedirectById(input.redirectId);
@@ -57,6 +75,12 @@ export const redirectsRouter = createTRPCRouter({
}),
update: protectedProcedure
.meta({
openapi: {
summary: "Update a redirect",
description: "Updates an existing redirect rule's configuration and logs an audit entry.",
},
})
.input(apiUpdateRedirect)
.mutation(async ({ input, ctx }) => {
const redirect = await findRedirectById(input.redirectId);

View File

@@ -51,6 +51,12 @@ import {
} from "@/server/db/schema";
export const redisRouter = createTRPCRouter({
create: protectedProcedure
.meta({
openapi: {
summary: "Create a Redis database",
description: "Creates a new Redis database service with the specified configuration, sets up a persistent data volume, and registers it in the project.",
},
})
.input(apiCreateRedis)
.mutation(async ({ input, ctx }) => {
try {
@@ -108,6 +114,12 @@ export const redisRouter = createTRPCRouter({
}
}),
one: protectedProcedure
.meta({
openapi: {
summary: "Get a Redis database by ID",
description: "Returns the full details of a Redis database service, including its environment and project configuration.",
},
})
.input(apiFindOneRedis)
.query(async ({ input, ctx }) => {
await checkServiceAccess(ctx, input.redisId, "read");
@@ -126,6 +138,12 @@ export const redisRouter = createTRPCRouter({
}),
start: protectedProcedure
.meta({
openapi: {
summary: "Start a Redis database",
description: "Starts the Docker container for the specified Redis database and sets its status to done.",
},
})
.input(apiFindOneRedis)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.redisId, {
@@ -151,6 +169,12 @@ export const redisRouter = createTRPCRouter({
return redis;
}),
reload: protectedProcedure
.meta({
openapi: {
summary: "Reload a Redis database",
description: "Restarts the Redis database by stopping and then starting its Docker container.",
},
})
.input(apiResetRedis)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.redisId, {
@@ -184,6 +208,12 @@ export const redisRouter = createTRPCRouter({
}),
stop: protectedProcedure
.meta({
openapi: {
summary: "Stop a Redis database",
description: "Stops the Docker container for the specified Redis database and sets its status to idle.",
},
})
.input(apiFindOneRedis)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.redisId, {
@@ -208,6 +238,12 @@ export const redisRouter = createTRPCRouter({
return redis;
}),
saveExternalPort: protectedProcedure
.meta({
openapi: {
summary: "Save the external port for a Redis database",
description: "Updates the external port mapping for the Redis database and triggers a redeployment. Validates that the port is not already in use.",
},
})
.input(apiSaveExternalPortRedis)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.redisId, {
@@ -241,6 +277,12 @@ export const redisRouter = createTRPCRouter({
return redis;
}),
deploy: protectedProcedure
.meta({
openapi: {
summary: "Deploy a Redis database",
description: "Triggers a deployment for the specified Redis database, rebuilding and restarting its Docker container with the current configuration.",
},
})
.input(apiDeployRedis)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.redisId, {
@@ -293,6 +335,12 @@ export const redisRouter = createTRPCRouter({
}
}),
changeStatus: protectedProcedure
.meta({
openapi: {
summary: "Change Redis database status",
description: "Updates the application status of a Redis database without starting or stopping the container.",
},
})
.input(apiChangeRedisStatus)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.redisId, {
@@ -311,6 +359,12 @@ export const redisRouter = createTRPCRouter({
return mongo;
}),
remove: protectedProcedure
.meta({
openapi: {
summary: "Delete a Redis database",
description: "Removes the Redis database service, its Docker container, and deletes the database record.",
},
})
.input(apiFindOneRedis)
.mutation(async ({ input, ctx }) => {
await checkServiceAccess(ctx, input.redisId, "delete");
@@ -346,6 +400,12 @@ export const redisRouter = createTRPCRouter({
return redis;
}),
saveEnvironment: protectedProcedure
.meta({
openapi: {
summary: "Save environment variables for a Redis database",
description: "Updates the environment variables for the specified Redis database service.",
},
})
.input(apiSaveEnvironmentVariablesRedis)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.redisId, {
@@ -370,6 +430,12 @@ export const redisRouter = createTRPCRouter({
return true;
}),
update: protectedProcedure
.meta({
openapi: {
summary: "Update a Redis database",
description: "Updates the configuration of an existing Redis database service.",
},
})
.input(apiUpdateRedis)
.mutation(async ({ input, ctx }) => {
const { redisId, ...rest } = input;
@@ -396,6 +462,12 @@ export const redisRouter = createTRPCRouter({
return true;
}),
changePassword: protectedProcedure
.meta({
openapi: {
summary: "Change Redis database password",
description: "Changes the password for the Redis database by executing CONFIG SET requirepass inside the running container and updating the stored password.",
},
})
.input(
z.object({
redisId: z.string().min(1),
@@ -446,6 +518,12 @@ export const redisRouter = createTRPCRouter({
return true;
}),
move: protectedProcedure
.meta({
openapi: {
summary: "Move a Redis database to another environment",
description: "Moves the Redis database to a different environment within the same project.",
},
})
.input(
z.object({
redisId: z.string(),
@@ -482,6 +560,12 @@ export const redisRouter = createTRPCRouter({
return updatedRedis;
}),
rebuild: protectedProcedure
.meta({
openapi: {
summary: "Rebuild a Redis database",
description: "Rebuilds the Redis database Docker container from scratch, pulling the latest image and recreating the service.",
},
})
.input(apiRebuildRedis)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.redisId, {
@@ -497,6 +581,12 @@ export const redisRouter = createTRPCRouter({
return true;
}),
search: protectedProcedure
.meta({
openapi: {
summary: "Search Redis databases",
description: "Returns a paginated list of Redis databases matching the given filters. Supports searching by name, appName, description, project, and environment.",
},
})
.input(
z.object({
q: z.string().optional(),
@@ -590,6 +680,12 @@ export const redisRouter = createTRPCRouter({
}),
readLogs: protectedProcedure
.meta({
openapi: {
summary: "Read Redis container logs",
description: "Retrieves the Docker container logs for the specified Redis database, with support for tail count, time-based filtering, and text search.",
},
})
.input(
apiFindOneRedis.extend({
tail: z.number().int().min(1).max(10000).default(100),

View File

@@ -23,6 +23,12 @@ import {
import { createTRPCRouter, withPermission } from "../trpc";
export const registryRouter = createTRPCRouter({
create: withPermission("registry", "create")
.meta({
openapi: {
summary: "Create registry",
description: "Creates a new Docker registry entry for the current organization and logs an audit event.",
},
})
.input(apiCreateRegistry)
.mutation(async ({ ctx, input }) => {
const reg = await createRegistry(input, ctx.session.activeOrganizationId);
@@ -35,6 +41,12 @@ export const registryRouter = createTRPCRouter({
return reg;
}),
remove: withPermission("registry", "delete")
.meta({
openapi: {
summary: "Delete registry",
description: "Removes a Docker registry entry by ID. Verifies organization ownership and logs an audit event before deletion.",
},
})
.input(apiRemoveRegistry)
.mutation(async ({ ctx, input }) => {
const registry = await findRegistryById(input.registryId);
@@ -53,6 +65,12 @@ export const registryRouter = createTRPCRouter({
return await removeRegistry(input.registryId);
}),
update: withPermission("registry", "create")
.meta({
openapi: {
summary: "Update registry",
description: "Updates an existing Docker registry entry. Verifies organization ownership before applying changes and logs an audit event.",
},
})
.input(apiUpdateRegistry)
.mutation(async ({ input, ctx }) => {
const { registryId, ...rest } = input;
@@ -82,13 +100,26 @@ export const registryRouter = createTRPCRouter({
});
return true;
}),
all: withPermission("registry", "read").query(async ({ ctx }) => {
all: withPermission("registry", "read")
.meta({
openapi: {
summary: "List all registries",
description: "Returns all Docker registry entries for the current organization.",
},
})
.query(async ({ ctx }) => {
const registryResponse = await db.query.registry.findMany({
where: eq(registry.organizationId, ctx.session.activeOrganizationId),
});
return registryResponse;
}),
one: withPermission("registry", "read")
.meta({
openapi: {
summary: "Get registry",
description: "Returns a single Docker registry entry by ID. Verifies the caller belongs to the same organization.",
},
})
.input(apiFindOneRegistry)
.query(async ({ input, ctx }) => {
const registry = await findRegistryById(input.registryId);
@@ -101,6 +132,12 @@ export const registryRouter = createTRPCRouter({
return registry;
}),
testRegistry: withPermission("registry", "read")
.meta({
openapi: {
summary: "Test registry credentials",
description: "Attempts a docker login with the provided credentials to verify the registry URL, username, and password are valid. Can run locally or on a remote server.",
},
})
.input(apiTestRegistry)
.mutation(async ({ input }) => {
try {
@@ -143,6 +180,12 @@ export const registryRouter = createTRPCRouter({
}
}),
testRegistryById: withPermission("registry", "read")
.meta({
openapi: {
summary: "Test registry connection by ID",
description: "Looks up a saved registry by ID and attempts a docker login with its stored credentials. Verifies organization ownership before testing.",
},
})
.input(apiTestRegistryById)
.mutation(async ({ input, ctx }) => {
try {

View File

@@ -11,6 +11,12 @@ import { createTRPCRouter, protectedProcedure } from "../trpc";
export const rollbackRouter = createTRPCRouter({
delete: protectedProcedure
.meta({
openapi: {
summary: "Delete a rollback",
description: "Permanently removes a rollback record by its ID.",
},
})
.input(apiFindOneRollback)
.mutation(async ({ input, ctx }) => {
try {
@@ -40,6 +46,12 @@ export const rollbackRouter = createTRPCRouter({
}
}),
rollback: protectedProcedure
.meta({
openapi: {
summary: "Perform a rollback",
description: "Rolls back an application to a previous deployment by restoring its Docker image and redeploying.",
},
})
.input(apiFindOneRollback)
.mutation(async ({ input, ctx }) => {
try {

View File

@@ -22,6 +22,12 @@ import { removeJob, schedule } from "@/server/utils/backup";
import { createTRPCRouter, protectedProcedure } from "../trpc";
export const scheduleRouter = createTRPCRouter({
create: protectedProcedure
.meta({
openapi: {
summary: "Create a scheduled job",
description: "Creates a new scheduled job for an application or compose service. If enabled, the job is automatically scheduled using the provided cron expression and timezone.",
},
})
.input(createScheduleSchema)
.mutation(async ({ input, ctx }) => {
const serviceId = input.applicationId || input.composeId;
@@ -54,6 +60,12 @@ export const scheduleRouter = createTRPCRouter({
}),
update: protectedProcedure
.meta({
openapi: {
summary: "Update a scheduled job",
description: "Updates an existing scheduled job configuration. Reschedules or removes the job depending on the enabled state.",
},
})
.input(updateScheduleSchema)
.mutation(async ({ input, ctx }) => {
const existingSchedule = await findScheduleById(input.scheduleId);
@@ -99,6 +111,12 @@ export const scheduleRouter = createTRPCRouter({
}),
delete: protectedProcedure
.meta({
openapi: {
summary: "Delete a scheduled job",
description: "Permanently removes a scheduled job and unschedules any associated cron job.",
},
})
.input(z.object({ scheduleId: z.string() }))
.mutation(async ({ input, ctx }) => {
const scheduleItem = await findScheduleById(input.scheduleId);
@@ -129,6 +147,12 @@ export const scheduleRouter = createTRPCRouter({
}),
list: protectedProcedure
.meta({
openapi: {
summary: "List scheduled jobs",
description: "Returns all scheduled jobs for a given service (application, compose, server, or dokploy-server), including their deployment history.",
},
})
.input(
z.object({
id: z.string(),
@@ -170,6 +194,12 @@ export const scheduleRouter = createTRPCRouter({
}),
one: protectedProcedure
.meta({
openapi: {
summary: "Get a scheduled job",
description: "Returns the details of a specific scheduled job by its ID.",
},
})
.input(z.object({ scheduleId: z.string() }))
.query(async ({ input, ctx }) => {
const schedule = await findScheduleById(input.scheduleId);
@@ -183,6 +213,12 @@ export const scheduleRouter = createTRPCRouter({
}),
runManually: protectedProcedure
.meta({
openapi: {
summary: "Run a scheduled job manually",
description: "Immediately executes a scheduled job outside of its normal cron schedule.",
},
})
.input(z.object({ scheduleId: z.string().min(1) }))
.mutation(async ({ input, ctx }) => {
const scheduleItem = await findScheduleById(input.scheduleId);

View File

@@ -15,6 +15,12 @@ import {
export const securityRouter = createTRPCRouter({
create: protectedProcedure
.meta({
openapi: {
summary: "Create a security entry",
description: "Creates a new HTTP basic auth security entry for an application with the provided username and password. Logs an audit entry.",
},
})
.input(apiCreateSecurity)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, {
@@ -31,6 +37,12 @@ export const securityRouter = createTRPCRouter({
}),
one: protectedProcedure
.meta({
openapi: {
summary: "Get a security entry",
description: "Returns a single HTTP basic auth security entry by its ID.",
},
})
.input(apiFindOneSecurity)
.query(async ({ input, ctx }) => {
const security = await findSecurityById(input.securityId);
@@ -41,6 +53,12 @@ export const securityRouter = createTRPCRouter({
}),
delete: protectedProcedure
.meta({
openapi: {
summary: "Delete a security entry",
description: "Deletes an HTTP basic auth security entry by its ID and logs an audit entry.",
},
})
.input(apiFindOneSecurity)
.mutation(async ({ input, ctx }) => {
const security = await findSecurityById(input.securityId);
@@ -57,6 +75,12 @@ export const securityRouter = createTRPCRouter({
}),
update: protectedProcedure
.meta({
openapi: {
summary: "Update a security entry",
description: "Updates an existing HTTP basic auth security entry's configuration and logs an audit entry.",
},
})
.input(apiUpdateSecurity)
.mutation(async ({ input, ctx }) => {
const security = await findSecurityById(input.securityId);

View File

@@ -48,6 +48,12 @@ import {
export const serverRouter = createTRPCRouter({
create: withPermission("server", "create")
.meta({
openapi: {
summary: "Create a server",
description: "Creates a new server in the organization. In cloud mode, enforces the user's server quantity limit. Returns the newly created server.",
},
})
.input(apiCreateServer)
.mutation(async ({ ctx, input }) => {
try {
@@ -80,6 +86,12 @@ export const serverRouter = createTRPCRouter({
}),
one: withPermission("server", "read")
.meta({
openapi: {
summary: "Get a server",
description: "Retrieves a single server by its ID. Validates that the user has access to the server within their organization.",
},
})
.input(apiFindOneServer)
.query(async ({ input, ctx }) => {
const server = await findServerById(input.serverId);
@@ -101,13 +113,26 @@ export const serverRouter = createTRPCRouter({
return server;
}),
getDefaultCommand: withPermission("server", "read")
.meta({
openapi: {
summary: "Get default server command",
description: "Returns the default setup command for a server. The command varies depending on whether the server is a build server or a deploy server.",
},
})
.input(apiFindOneServer)
.query(async ({ input }) => {
const server = await findServerById(input.serverId);
const isBuildServer = server.serverType === "build";
return defaultCommand(isBuildServer);
}),
all: withPermission("server", "read").query(async ({ ctx }) => {
all: withPermission("server", "read")
.meta({
openapi: {
summary: "List all servers",
description: "Returns all servers in the current organization along with a count of associated services (applications, compose, databases). Results are filtered by the user's accessible server permissions.",
},
})
.query(async ({ ctx }) => {
const accessibleIds = await getAccessibleServerIds(ctx.session);
const result = await db
@@ -130,6 +155,12 @@ export const serverRouter = createTRPCRouter({
return result.filter((s) => accessibleIds.has(s.serverId));
}),
allForPermissions: withPermission("member", "update")
.meta({
openapi: {
summary: "List all servers for permissions",
description: "Returns a minimal list of servers (ID, name, IP, type) used for configuring member permissions. Requires a valid enterprise license.",
},
})
.use(async ({ ctx, next }) => {
const licensed = await hasValidLicense(ctx.session.activeOrganizationId);
if (!licensed) {
@@ -152,7 +183,14 @@ export const serverRouter = createTRPCRouter({
where: eq(server.organizationId, ctx.session.activeOrganizationId),
});
}),
count: protectedProcedure.query(async ({ ctx }) => {
count: protectedProcedure
.meta({
openapi: {
summary: "Get server count",
description: "Returns the total number of servers across all organizations owned by the current user.",
},
})
.query(async ({ ctx }) => {
const organizations = await db.query.organization.findMany({
where: eq(organization.ownerId, ctx.user.id),
with: {
@@ -164,7 +202,14 @@ export const serverRouter = createTRPCRouter({
return servers.length ?? 0;
}),
withSSHKey: withPermission("server", "read").query(async ({ ctx }) => {
withSSHKey: withPermission("server", "read")
.meta({
openapi: {
summary: "List servers with SSH keys",
description: "Returns all deploy-type servers that have an SSH key configured. In cloud mode, only active servers are included. Results are filtered by the user's accessible server permissions.",
},
})
.query(async ({ ctx }) => {
const accessibleIds = await getAccessibleServerIds(ctx.session);
const result = await db.query.server.findMany({
@@ -184,7 +229,14 @@ export const serverRouter = createTRPCRouter({
});
return result.filter((s) => accessibleIds.has(s.serverId));
}),
buildServers: withPermission("server", "read").query(async ({ ctx }) => {
buildServers: withPermission("server", "read")
.meta({
openapi: {
summary: "List build servers",
description: "Returns all build-type servers that have an SSH key configured. In cloud mode, only active servers are included. Results are filtered by the user's accessible server permissions.",
},
})
.query(async ({ ctx }) => {
const accessibleIds = await getAccessibleServerIds(ctx.session);
const result = await db.query.server.findMany({
@@ -205,6 +257,12 @@ export const serverRouter = createTRPCRouter({
return result.filter((s) => accessibleIds.has(s.serverId));
}),
setup: withPermission("server", "create")
.meta({
openapi: {
summary: "Setup a server",
description: "Runs the initial setup process on a remote server, installing required dependencies and configuring Docker. An audit log entry is created.",
},
})
.input(apiFindOneServer)
.mutation(async ({ input, ctx }) => {
try {
@@ -256,6 +314,12 @@ export const serverRouter = createTRPCRouter({
}
}),
validate: withPermission("server", "read")
.meta({
openapi: {
summary: "Validate server configuration",
description: "Checks the server for required tools and configuration including Docker, Rclone, Nixpacks, Buildpacks, Railpack, Swarm mode, network setup, and privilege mode.",
},
})
.input(apiFindOneServer)
.query(async ({ input, ctx }) => {
try {
@@ -304,6 +368,12 @@ export const serverRouter = createTRPCRouter({
}),
security: withPermission("server", "read")
.meta({
openapi: {
summary: "Get server security audit",
description: "Performs a security audit on the server, checking UFW firewall, SSH configuration, non-root user setup, unattended upgrades, and Fail2Ban status.",
},
})
.input(apiFindOneServer)
.query(async ({ input, ctx }) => {
try {
@@ -354,6 +424,12 @@ export const serverRouter = createTRPCRouter({
}
}),
setupMonitoring: withPermission("server", "create")
.meta({
openapi: {
summary: "Setup server monitoring",
description: "Configures and deploys the monitoring agent on a server with the specified metrics configuration including refresh rates, retention, thresholds, and container service filters.",
},
})
.input(apiUpdateServerMonitoring)
.mutation(async ({ input, ctx }) => {
try {
@@ -402,6 +478,12 @@ export const serverRouter = createTRPCRouter({
}
}),
remove: withPermission("server", "delete")
.meta({
openapi: {
summary: "Remove a server",
description: "Deletes a server and removes all associated deployments. Fails if the server has active services. In cloud mode, updates the user's server quantity allocation.",
},
})
.input(apiRemoveServer)
.mutation(async ({ input, ctx }) => {
try {
@@ -435,6 +517,12 @@ export const serverRouter = createTRPCRouter({
}
}),
update: withPermission("server", "create")
.meta({
openapi: {
summary: "Update a server",
description: "Updates the configuration of an existing server. Fails if the server is inactive. An audit log entry is created for the update.",
},
})
.input(apiUpdateServer)
.mutation(async ({ input, ctx }) => {
try {
@@ -467,14 +555,28 @@ export const serverRouter = createTRPCRouter({
throw error;
}
}),
publicIp: protectedProcedure.query(async () => {
publicIp: protectedProcedure
.meta({
openapi: {
summary: "Get public IP address",
description: "Returns the public IP address of the local server. Returns an empty string in cloud mode.",
},
})
.query(async () => {
if (IS_CLOUD) {
return "";
}
const ip = await getPublicIpWithFallback();
return ip;
}),
getServerTime: protectedProcedure.query(() => {
getServerTime: protectedProcedure
.meta({
openapi: {
summary: "Get server time",
description: "Returns the current server time and timezone. Returns null in cloud mode.",
},
})
.query(() => {
if (IS_CLOUD) {
return null;
}
@@ -484,6 +586,12 @@ export const serverRouter = createTRPCRouter({
};
}),
getServerMetrics: withPermission("monitoring", "read")
.meta({
openapi: {
summary: "Get server metrics",
description: "Fetches monitoring metrics (CPU, memory, disk, network) from the server's monitoring agent endpoint. Requires the monitoring service to be configured and running.",
},
})
.input(
z.object({
url: z.string(),

File diff suppressed because it is too large Load Diff

View File

@@ -25,6 +25,12 @@ import {
export const sshRouter = createTRPCRouter({
create: withPermission("sshKeys", "create")
.meta({
openapi: {
summary: "Create SSH key",
description: "Stores a new SSH key for the current organization and logs an audit event.",
},
})
.input(apiCreateSshKey)
.mutation(async ({ input, ctx }) => {
try {
@@ -46,6 +52,12 @@ export const sshRouter = createTRPCRouter({
}
}),
remove: withPermission("sshKeys", "delete")
.meta({
openapi: {
summary: "Delete SSH key",
description: "Removes an SSH key by ID. Verifies organization ownership and logs an audit event before deletion.",
},
})
.input(apiRemoveSshKey)
.mutation(async ({ input, ctx }) => {
try {
@@ -69,6 +81,12 @@ export const sshRouter = createTRPCRouter({
}
}),
one: withPermission("sshKeys", "read")
.meta({
openapi: {
summary: "Get SSH key",
description: "Returns a single SSH key by ID. Verifies the caller belongs to the same organization.",
},
})
.input(apiFindOneSshKey)
.query(async ({ input, ctx }) => {
const sshKey = await findSSHKeyById(input.sshKeyId);
@@ -81,13 +99,27 @@ export const sshRouter = createTRPCRouter({
}
return sshKey;
}),
all: withPermission("sshKeys", "read").query(async ({ ctx }) => {
all: withPermission("sshKeys", "read")
.meta({
openapi: {
summary: "List all SSH keys",
description: "Returns all SSH keys for the current organization, ordered by creation date descending.",
},
})
.query(async ({ ctx }) => {
return await db.query.sshKeys.findMany({
where: eq(sshKeys.organizationId, ctx.session.activeOrganizationId),
orderBy: desc(sshKeys.createdAt),
});
}),
allForApps: protectedProcedure.query(async ({ ctx }) => {
allForApps: protectedProcedure
.meta({
openapi: {
summary: "List SSH keys for app selection",
description: "Returns a lightweight list of SSH keys (ID and name only) for the current organization, suitable for dropdown selectors in application forms.",
},
})
.query(async ({ ctx }) => {
return await db.query.sshKeys.findMany({
columns: {
sshKeyId: true,
@@ -98,11 +130,23 @@ export const sshRouter = createTRPCRouter({
});
}),
generate: withPermission("sshKeys", "read")
.meta({
openapi: {
summary: "Generate SSH key pair",
description: "Generates a new SSH key pair of the specified type (RSA, ED25519, etc.) and returns both public and private keys.",
},
})
.input(apiGenerateSSHKey)
.mutation(async ({ input }) => {
return await generateSSHKey(input.type);
}),
update: withPermission("sshKeys", "create")
.meta({
openapi: {
summary: "Update SSH key",
description: "Updates an existing SSH key. Verifies organization ownership before applying changes and logs an audit event.",
},
})
.input(apiUpdateSshKey)
.mutation(async ({ input, ctx }) => {
try {

View File

@@ -30,7 +30,14 @@ import {
export const stripeRouter = createTRPCRouter({
/** Returns the current billing plan for the user's organization. Used to gate features like chat (Startup only). */
getCurrentPlan: protectedProcedure.query(async ({ ctx }) => {
getCurrentPlan: protectedProcedure
.meta({
openapi: {
summary: "Get current billing plan",
description: "Returns the active Stripe billing plan (hobby, startup, or legacy) for the caller's organization owner. Returns null if not on cloud or no subscription exists.",
},
})
.query(async ({ ctx }) => {
if (!IS_CLOUD) return null;
const owner = await findUserById(ctx.user.ownerId);
if (!owner?.stripeCustomerId) return null;
@@ -71,7 +78,14 @@ export const stripeRouter = createTRPCRouter({
return null;
}),
getProducts: adminProcedure.query(async ({ ctx }) => {
getProducts: adminProcedure
.meta({
openapi: {
summary: "List Stripe products and subscriptions",
description: "Returns available Stripe products, the user's active subscriptions, current plan tier, billing interval, and price amount.",
},
})
.query(async ({ ctx }) => {
const user = await findUserById(ctx.user.ownerId);
const stripeCustomerId = user.stripeCustomerId;
@@ -162,6 +176,12 @@ export const stripeRouter = createTRPCRouter({
};
}),
createCheckoutSession: adminProcedure
.meta({
openapi: {
summary: "Create Stripe checkout session",
description: "Creates a Stripe checkout session for subscribing to a billing plan. Handles customer creation or reuse and returns the session ID for redirect.",
},
})
.input(
z
.object({
@@ -222,7 +242,14 @@ export const stripeRouter = createTRPCRouter({
return { sessionId: session.id };
}),
createCustomerPortalSession: adminProcedure.mutation(async ({ ctx }) => {
createCustomerPortalSession: adminProcedure
.meta({
openapi: {
summary: "Create Stripe customer portal session",
description: "Creates a Stripe billing portal session URL so the user can manage their subscription, payment methods, and invoices.",
},
})
.mutation(async ({ ctx }) => {
// Use the organization's owner account for billing portal
const owner = await findUserById(ctx.user.ownerId);
@@ -253,6 +280,12 @@ export const stripeRouter = createTRPCRouter({
}),
upgradeSubscription: adminProcedure
.meta({
openapi: {
summary: "Upgrade subscription",
description: "Upgrades or changes the current Stripe subscription to a different tier or server quantity. Applies prorated charges for the billing period change.",
},
})
.input(
z
.object({
@@ -324,7 +357,14 @@ export const stripeRouter = createTRPCRouter({
return { ok: true };
}),
canCreateMoreServers: withPermission("server", "create").query(
canCreateMoreServers: withPermission("server", "create")
.meta({
openapi: {
summary: "Check server creation quota",
description: "Returns whether the organization can create more servers based on their subscription's server quantity limit. Always returns true for self-hosted instances.",
},
})
.query(
async ({ ctx }) => {
const user = await findUserById(ctx.user.ownerId);
const servers = await findServersByUserId(user.id);
@@ -338,6 +378,12 @@ export const stripeRouter = createTRPCRouter({
),
updateInvoiceNotifications: adminProcedure
.meta({
openapi: {
summary: "Update invoice notification preference",
description: "Enables or disables email notifications for invoice events. Only available on Dokploy Cloud.",
},
})
.input(z.object({ enabled: z.boolean() }))
.mutation(async ({ ctx, input }) => {
if (!IS_CLOUD) {
@@ -353,7 +399,14 @@ export const stripeRouter = createTRPCRouter({
return { ok: true };
}),
getInvoices: adminProcedure.query(async ({ ctx }) => {
getInvoices: adminProcedure
.meta({
openapi: {
summary: "List invoices",
description: "Returns up to 100 Stripe invoices for the organization owner, including status, amounts, and download links.",
},
})
.query(async ({ ctx }) => {
const user = await findUserById(ctx.user.ownerId);
const stripeCustomerId = user.stripeCustomerId;

View File

@@ -13,6 +13,12 @@ import { containerIdRegex } from "./docker";
export const swarmRouter = createTRPCRouter({
getNodes: withPermission("server", "read")
.meta({
openapi: {
summary: "Get Swarm nodes",
description: "Retrieves all nodes in the Docker Swarm. Optionally targets a remote server by ID.",
},
})
.input(
z.object({
serverId: z.string().optional(),
@@ -22,11 +28,23 @@ export const swarmRouter = createTRPCRouter({
return await getSwarmNodes(input.serverId);
}),
getNodeInfo: withPermission("server", "read")
.meta({
openapi: {
summary: "Get Swarm node info",
description: "Retrieves detailed information about a specific Docker Swarm node by its node ID. Optionally targets a remote server.",
},
})
.input(z.object({ nodeId: z.string(), serverId: z.string().optional() }))
.query(async ({ input }) => {
return await getNodeInfo(input.nodeId, input.serverId);
}),
getNodeApps: withPermission("server", "read")
.meta({
openapi: {
summary: "Get Swarm node applications",
description: "Retrieves all applications (services) running across Docker Swarm nodes. Optionally targets a remote server.",
},
})
.input(
z.object({
serverId: z.string().optional(),
@@ -58,6 +76,12 @@ export const swarmRouter = createTRPCRouter({
return await getApplicationInfo(input.appName, input.serverId);
}),
getContainerStats: withPermission("server", "read")
.meta({
openapi: {
summary: "Get container stats",
description: "Retrieves resource usage statistics for all containers. Optionally targets a remote server and validates organization access.",
},
})
.input(
z.object({
serverId: z.string().optional(),

View File

@@ -16,6 +16,12 @@ import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc";
export const tagRouter = createTRPCRouter({
create: withPermission("tag", "create")
.meta({
openapi: {
summary: "Create tag",
description: "Creates a new tag with a name and color for the current organization. Tag names must be unique within the organization.",
},
})
.input(apiCreateTag)
.mutation(async ({ input, ctx }) => {
try {
@@ -47,7 +53,14 @@ export const tagRouter = createTRPCRouter({
}
}),
all: protectedProcedure.query(async ({ ctx }) => {
all: protectedProcedure
.meta({
openapi: {
summary: "List all tags",
description: "Returns all tags for the current organization, ordered alphabetically by name.",
},
})
.query(async ({ ctx }) => {
try {
const organizationTags = await db.query.tags.findMany({
where: eq(tags.organizationId, ctx.session.activeOrganizationId),
@@ -64,7 +77,15 @@ export const tagRouter = createTRPCRouter({
}
}),
one: protectedProcedure.input(apiFindOneTag).query(async ({ input, ctx }) => {
one: protectedProcedure
.meta({
openapi: {
summary: "Get tag",
description: "Returns a single tag by ID. Only returns tags belonging to the caller's organization.",
},
})
.input(apiFindOneTag)
.query(async ({ input, ctx }) => {
try {
const tag = await db.query.tags.findFirst({
where: and(
@@ -94,6 +115,12 @@ export const tagRouter = createTRPCRouter({
}),
update: withPermission("tag", "update")
.meta({
openapi: {
summary: "Update tag",
description: "Updates an existing tag's name and/or color. Verifies the tag belongs to the caller's organization. Tag names must remain unique.",
},
})
.input(apiUpdateTag)
.mutation(async ({ input, ctx }) => {
try {
@@ -144,6 +171,12 @@ export const tagRouter = createTRPCRouter({
}),
remove: withPermission("tag", "delete")
.meta({
openapi: {
summary: "Delete tag",
description: "Deletes a tag by ID. Cascade-deletes all project-tag associations. Verifies the tag belongs to the caller's organization.",
},
})
.input(apiRemoveTag)
.mutation(async ({ input, ctx }) => {
try {
@@ -179,6 +212,12 @@ export const tagRouter = createTRPCRouter({
}),
assignToProject: protectedProcedure
.meta({
openapi: {
summary: "Assign tag to project",
description: "Associates a tag with a project. Verifies that both the tag and project belong to the caller's organization and that the caller has project access.",
},
})
.input(
z.object({
projectId: z.string().min(1),
@@ -267,6 +306,12 @@ export const tagRouter = createTRPCRouter({
}),
removeFromProject: protectedProcedure
.meta({
openapi: {
summary: "Remove tag from project",
description: "Removes a tag-project association. Verifies that both the tag and project belong to the caller's organization and that the caller has project access.",
},
})
.input(
z.object({
projectId: z.string().min(1),
@@ -347,6 +392,12 @@ export const tagRouter = createTRPCRouter({
}),
bulkAssign: protectedProcedure
.meta({
openapi: {
summary: "Bulk assign tags to project",
description: "Replaces all tag associations for a project with the provided list of tag IDs. Removes existing associations first, then inserts the new set.",
},
})
.input(
z.object({
projectId: z.string().min(1),

View File

@@ -60,7 +60,14 @@ const apiCreateApiKey = z.object({
});
export const userRouter = createTRPCRouter({
all: withPermission("member", "read").query(async ({ ctx }) => {
all: withPermission("member", "read")
.meta({
openapi: {
summary: "List all organization members",
description: "Retrieve all members of the current active organization, including their associated user data, ordered by creation date.",
},
})
.query(async ({ ctx }) => {
return await db.query.member.findMany({
where: eq(member.organizationId, ctx.session.activeOrganizationId),
with: {
@@ -70,6 +77,12 @@ export const userRouter = createTRPCRouter({
});
}),
one: protectedProcedure
.meta({
openapi: {
summary: "Get a user by ID",
description: "Retrieve a specific user's membership and profile within the active organization. Users can view their own data; admins and owners can view any member. Requires member.update permission for non-self lookups.",
},
})
.input(
z.object({
userId: z.string(),
@@ -114,7 +127,14 @@ export const userRouter = createTRPCRouter({
return memberResult;
}),
session: publicProcedure.query(async ({ ctx }) => {
session: publicProcedure
.meta({
openapi: {
summary: "Get current session",
description: "Return the current user's ID and active organization ID from the session. Returns null if no valid session exists.",
},
})
.query(async ({ ctx }) => {
if (!ctx.user || !ctx.session || !ctx.session.activeOrganizationId) {
return null;
}
@@ -127,7 +147,14 @@ export const userRouter = createTRPCRouter({
},
};
}),
get: protectedProcedure.query(async ({ ctx }) => {
get: protectedProcedure
.meta({
openapi: {
summary: "Get current user profile",
description: "Retrieve the current authenticated user's membership record including user profile and API keys for the active organization.",
},
})
.query(async ({ ctx }) => {
const memberResult = await db.query.member.findFirst({
where: and(
eq(member.userId, ctx.user.id),
@@ -144,10 +171,24 @@ export const userRouter = createTRPCRouter({
return memberResult;
}),
getPermissions: protectedProcedure.query(async ({ ctx }) => {
getPermissions: protectedProcedure
.meta({
openapi: {
summary: "Get resolved permissions",
description: "Return the fully resolved permissions for the current user in the active organization, combining role-based and custom permissions.",
},
})
.query(async ({ ctx }) => {
return resolvePermissions(ctx);
}),
haveRootAccess: protectedProcedure.query(async ({ ctx }) => {
haveRootAccess: protectedProcedure
.meta({
openapi: {
summary: "Check root access",
description: "Check whether the current user has root admin access. Only returns true in cloud mode for the designated admin user or impersonating sessions.",
},
})
.query(async ({ ctx }) => {
if (!IS_CLOUD) {
return false;
}
@@ -159,7 +200,14 @@ export const userRouter = createTRPCRouter({
}
return false;
}),
getBackups: adminProcedure.query(async ({ ctx }) => {
getBackups: adminProcedure
.meta({
openapi: {
summary: "Get user backups",
description: "Retrieve the current admin user's backup configurations including destinations, deployments, and API keys.",
},
})
.query(async ({ ctx }) => {
const memberResult = await db.query.member.findFirst({
where: and(
eq(member.userId, ctx.user.id),
@@ -182,8 +230,14 @@ export const userRouter = createTRPCRouter({
return memberResult?.user;
}),
getServerMetrics: withPermission("monitoring", "read").query(
async ({ ctx }) => {
getServerMetrics: withPermission("monitoring", "read")
.meta({
openapi: {
summary: "Get server metrics user",
description: "Retrieve the user record associated with server metrics access for the current organization membership.",
},
})
.query(async ({ ctx }) => {
const memberResult = await db.query.member.findFirst({
where: and(
eq(member.userId, ctx.user.id),
@@ -198,6 +252,12 @@ export const userRouter = createTRPCRouter({
},
),
update: protectedProcedure
.meta({
openapi: {
summary: "Update current user",
description: "Update the current user's profile. If changing the password, the current password must be provided and verified. Logs an audit event on success.",
},
})
.input(apiUpdateUser)
.mutation(async ({ input, ctx }) => {
if (input.password || input.currentPassword) {
@@ -248,12 +308,24 @@ export const userRouter = createTRPCRouter({
}
}),
getUserByToken: publicProcedure
.meta({
openapi: {
summary: "Get user by token",
description: "Look up a user by their authentication token. This is a public endpoint that does not require an active session.",
},
})
.input(apiFindOneToken)
.query(async ({ input }) => {
return await getUserByToken(input.token);
}),
getMetricsToken: withPermission("monitoring", "read").query(
async ({ ctx }) => {
getMetricsToken: withPermission("monitoring", "read")
.meta({
openapi: {
summary: "Get metrics token and configuration",
description: "Retrieve the server IP, paid features flag, and monitoring configuration needed for metrics collection.",
},
})
.query(async ({ ctx }) => {
const user = await findUserById(ctx.user.ownerId);
const settings = await getWebServerSettings();
return {
@@ -264,6 +336,12 @@ export const userRouter = createTRPCRouter({
},
),
remove: protectedProcedure
.meta({
openapi: {
summary: "Remove a user",
description: "Delete a user from the organization. Only owners and admins can remove users; owners cannot be removed, and admins cannot remove themselves or other admins. Disabled on cloud.",
},
})
.input(
z.object({
userId: z.string(),
@@ -333,6 +411,12 @@ export const userRouter = createTRPCRouter({
return result;
}),
assignPermissions: withPermission("member", "update")
.meta({
openapi: {
summary: "Assign member permissions",
description: "Update permissions for a specific member in the organization. Only the organization owner can assign permissions. Git provider and server access restrictions require a valid license.",
},
})
.input(apiAssignPermissions)
.mutation(async ({ input, ctx }) => {
try {
@@ -383,7 +467,14 @@ export const userRouter = createTRPCRouter({
throw error;
}
}),
getInvitations: protectedProcedure.query(async ({ ctx }) => {
getInvitations: protectedProcedure
.meta({
openapi: {
summary: "Get pending invitations for current user",
description: "Retrieve all pending organization invitations for the current user's email that have not yet expired.",
},
})
.query(async ({ ctx }) => {
return await db.query.invitation.findMany({
where: and(
eq(invitation.email, ctx.user.email),
@@ -397,6 +488,12 @@ export const userRouter = createTRPCRouter({
}),
getContainerMetrics: withPermission("monitoring", "read")
.meta({
openapi: {
summary: "Get container metrics",
description: "Fetch monitoring metrics for a specific container by querying the metrics endpoint. Requires an application name, metrics URL, and authentication token.",
},
})
.input(
z.object({
url: z.string(),
@@ -455,11 +552,24 @@ export const userRouter = createTRPCRouter({
}
}),
generateToken: protectedProcedure.mutation(async () => {
generateToken: protectedProcedure
.meta({
openapi: {
summary: "Generate authentication token",
description: "Generate a new authentication token for the current user.",
},
})
.mutation(async () => {
return "token";
}),
deleteApiKey: protectedProcedure
.meta({
openapi: {
summary: "Delete an API key",
description: "Delete an API key by ID. Only the owner of the API key can delete it. Logs an audit event on success.",
},
})
.input(
z.object({
apiKeyId: z.string(),
@@ -499,6 +609,12 @@ export const userRouter = createTRPCRouter({
}),
createApiKey: protectedProcedure
.meta({
openapi: {
summary: "Create an API key",
description: "Create a new API key for the current user, scoped to a specific organization. Supports optional rate limiting and request limiting configuration.",
},
})
.input(apiCreateApiKey)
.mutation(async ({ input, ctx }) => {
// Verify user is a member of the organization specified in metadata
@@ -529,6 +645,12 @@ export const userRouter = createTRPCRouter({
}),
checkUserOrganizations: protectedProcedure
.meta({
openapi: {
summary: "Check user organization count",
description: "Return the number of organizations a user belongs to. Users can check their own count; admins and owners can check counts for members in the active organization.",
},
})
.input(
z.object({
userId: z.string(),
@@ -570,6 +692,12 @@ export const userRouter = createTRPCRouter({
return organizations.length;
}),
createUserWithCredentials: withPermission("member", "create")
.meta({
openapi: {
summary: "Create user with credentials",
description: "Create a new user with email and password and add them to the active organization with the specified role. Only available in self-hosted mode.",
},
})
.input(
z.object({
email: z.string().email(),
@@ -601,6 +729,12 @@ export const userRouter = createTRPCRouter({
});
}),
sendInvitation: withPermission("member", "create")
.meta({
openapi: {
summary: "Send invitation email",
description: "Send an invitation email to a pending invitee using a configured email or Resend notification provider. Returns the generated invite link. Disabled on cloud.",
},
})
.input(
z.object({
invitationId: z.string().min(1),
@@ -676,7 +810,14 @@ export const userRouter = createTRPCRouter({
return inviteLink;
}),
getBookmarkedTemplates: protectedProcedure.query(async ({ ctx }) => {
getBookmarkedTemplates: protectedProcedure
.meta({
openapi: {
summary: "Get bookmarked templates",
description: "Retrieve the list of template IDs that the current user has bookmarked.",
},
})
.query(async ({ ctx }) => {
const result = await db.query.user.findFirst({
where: eq(user.id, ctx.user.id),
columns: { bookmarkedTemplates: true },
@@ -686,6 +827,12 @@ export const userRouter = createTRPCRouter({
}),
toggleTemplateBookmark: protectedProcedure
.meta({
openapi: {
summary: "Toggle template bookmark",
description: "Add or remove a template from the current user's bookmarks. Returns whether the template is now bookmarked.",
},
})
.input(
z.object({
templateId: z.string().min(1),

View File

@@ -30,6 +30,12 @@ import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc";
export const volumeBackupsRouter = createTRPCRouter({
list: protectedProcedure
.meta({
openapi: {
summary: "List volume backups",
description: "Returns all volume backup configurations for a given service, including related service details.",
},
})
.input(
z.object({
id: z.string().min(1),
@@ -65,6 +71,12 @@ export const volumeBackupsRouter = createTRPCRouter({
});
}),
create: protectedProcedure
.meta({
openapi: {
summary: "Create a volume backup",
description: "Creates a new volume backup configuration for a service. If enabled, automatically schedules the backup using the provided cron expression.",
},
})
.input(createVolumeBackupSchema)
.mutation(async ({ input, ctx }) => {
const serviceId =
@@ -102,6 +114,12 @@ export const volumeBackupsRouter = createTRPCRouter({
return newVolumeBackup;
}),
one: protectedProcedure
.meta({
openapi: {
summary: "Get a volume backup",
description: "Returns the details of a specific volume backup configuration by its ID.",
},
})
.input(
z.object({
volumeBackupId: z.string().min(1),
@@ -126,6 +144,12 @@ export const volumeBackupsRouter = createTRPCRouter({
return vb;
}),
delete: protectedProcedure
.meta({
openapi: {
summary: "Delete a volume backup",
description: "Permanently removes a volume backup configuration by its ID.",
},
})
.input(
z.object({
volumeBackupId: z.string().min(1),
@@ -156,6 +180,12 @@ export const volumeBackupsRouter = createTRPCRouter({
return result;
}),
update: protectedProcedure
.meta({
openapi: {
summary: "Update a volume backup",
description: "Updates an existing volume backup configuration. Reschedules or removes the backup job depending on the enabled state.",
},
})
.input(updateVolumeBackupSchema)
.mutation(async ({ input, ctx }) => {
const existingVb = await findVolumeBackupById(input.volumeBackupId);
@@ -216,6 +246,12 @@ export const volumeBackupsRouter = createTRPCRouter({
}),
runManually: protectedProcedure
.meta({
openapi: {
summary: "Run a volume backup manually",
description: "Immediately executes a volume backup outside of its normal cron schedule.",
},
})
.input(z.object({ volumeBackupId: z.string().min(1) }))
.mutation(async ({ input, ctx }) => {
const vb = await findVolumeBackupById(input.volumeBackupId);

View File

@@ -13,7 +13,12 @@ import { hasValidLicense } from "@dokploy/server/index";
import type { statements } from "@dokploy/server/lib/access-control";
import { validateRequest } from "@dokploy/server/lib/auth";
import { checkPermission } from "@dokploy/server/services/permission";
import type { OpenApiMeta } from "@dokploy/trpc-openapi";
import type { OpenApiMeta as _OpenApiMeta } from "@dokploy/trpc-openapi";
// method and path are auto-generated by @dokploy/trpc-openapi, make them optional
type OpenApiMeta = {
openapi?: Partial<NonNullable<_OpenApiMeta["openapi"]>>;
};
import { initTRPC, TRPCError } from "@trpc/server";
import type { CreateNextContextOptions } from "@trpc/server/adapters/next";
import type { Session, User } from "better-auth";

54248
openapi.json

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,429 @@
import { dynamicTool } from "ai";
import { z } from "zod";
import type { ChatContext } from "./chat-tools";
interface OpenApiSpec {
paths: Record<
string,
Record<
string,
{
operationId?: string;
summary?: string;
description?: string;
tags?: string[];
deprecated?: boolean;
parameters?: {
name: string;
in: string;
required?: boolean;
schema?: { type?: string };
}[];
requestBody?: {
content: Record<
string,
{ schema: { properties?: Record<string, any>; required?: string[] } }
>;
};
}
>
>;
}
interface ToolConfig {
baseUrl: string;
cookie: string;
}
interface EndpointInfo {
method: string;
path: string;
operationId: string;
}
/**
* Semantic hints for properties that the OpenAPI spec doesn't describe well.
* These are appended to parameter names in the catalog so the model knows what values to use.
*/
export const PROPERTY_HINTS: Record<string, string> = {
composePath: "path to the directory containing docker-compose.yml in the repo, e.g. '.' or 'docker/'",
composeFile: "raw YAML content of the docker-compose.yml file",
env: "environment variables as a single string, one per line in KEY=VALUE format",
dockerImage: "Docker image name with optional tag, e.g. 'nginx:alpine' or 'postgres:16'",
command: "Docker CMD override, e.g. 'npm start' or 'python app.py'",
args: "Docker command arguments as an array of strings",
buildPath: "path to the build context directory in the repo, e.g. '.' or './app'",
publishDirectory: "output directory for static builds, e.g. 'dist' or '.next'",
dockerfile: "path to the Dockerfile relative to buildPath, e.g. 'Dockerfile' or 'docker/Dockerfile.prod'",
repository: "Git repository name, e.g. 'my-app' (not the full URL)",
branch: "Git branch name, e.g. 'main' or 'develop'",
customGitUrl: "full Git clone URL, e.g. 'https://github.com/user/repo.git'",
databasePassword: "password string for the database",
externalPort: "port number exposed to the host, e.g. 5432",
host: "domain hostname, e.g. 'myapp.example.com'",
appName: "unique internal service name (auto-generated, alphanumeric with dots/hyphens)",
watchPaths: "array of file paths that trigger auto-deploy on change, e.g. ['src/**', 'package.json']",
suffix: "custom suffix appended to the service name",
repoPath: "path within the cloned repository to browse, use '.' for root directory",
filePath: "path to a specific file in the repository, e.g. 'docker-compose.yml' or 'src/index.ts'",
tail: "number of log lines to return from the end, e.g. 100",
since: "time duration for log filtering, e.g. '1h' or '30m'",
search: "text to search for in logs",
};
const EXCLUDED_TAGS = new Set([
"notification",
"sso",
"stripe",
"auditLog",
"ai",
"customRole",
"whitelabeling",
]);
/** Minimal shared tags — only project/environment for navigation */
const SHARED_TAGS = ["project", "environment"];
/** Tags allowed per context type (on top of SHARED_TAGS) */
const CONTEXT_TAGS: Record<ChatContext["type"], string[]> = {
application: [
"application",
"deployment",
"domain",
"docker",
"mounts",
"port",
"security",
"redirects",
"registry",
"sshKey",
"backup",
"volumeBackups",
"rollback",
"schedule",
"patch",
"previewDeployment",
"bitbucket",
"gitea",
"github",
"gitlab",
"gitProvider",
"destination",
"tag",
],
compose: [
"compose",
"deployment",
"domain",
"docker",
"backup",
"patch",
"sshKey",
"bitbucket",
"gitea",
"github",
"gitlab",
"gitProvider",
"tag",
],
postgres: ["postgres", "backup", "docker", "destination"],
mysql: ["mysql", "backup", "docker", "destination"],
redis: ["redis", "docker"],
mongo: ["mongo", "backup", "docker", "destination"],
mariadb: ["mariadb", "backup", "docker", "destination"],
libsql: ["libsql", "docker"],
project: [
"application",
"compose",
"postgres",
"mysql",
"redis",
"mongo",
"mariadb",
"libsql",
"domain",
"deployment",
"docker",
"tag",
],
server: [
"server",
"docker",
"cluster",
"swarm",
"certificates",
"registry",
"settings",
],
general: [], // empty = allow all non-excluded tags
};
/**
* Get the set of allowed tags for a given context type.
* Returns null for "general" context (no filtering, allow all).
*/
function getAllowedTags(contextType: ChatContext["type"]): Set<string> | null {
if (contextType === "general") return null;
const contextSpecific = CONTEXT_TAGS[contextType];
return new Set([...SHARED_TAGS, ...contextSpecific]);
}
/**
* Extract enum values from a JSON Schema property (handles anyOf wrappers).
*/
function extractEnum(prop: any): string[] | null {
if (prop?.enum) return prop.enum;
if (Array.isArray(prop?.anyOf)) {
for (const variant of prop.anyOf) {
if (variant?.enum) return variant.enum;
}
}
return null;
}
/** Human-readable description for each tag group in the catalog */
const TAG_DESCRIPTIONS: Record<string, string> = {
application: "Manage application services — create, update, deploy, start, stop, and configure applications",
compose: "Manage Docker Compose/Stack services — create, update, deploy, and configure compose files",
postgres: "Manage PostgreSQL database services",
mysql: "Manage MySQL database services",
redis: "Manage Redis database services",
mongo: "Manage MongoDB database services",
mariadb: "Manage MariaDB database services",
libsql: "Manage LibSQL database services",
deployment: "View deployment history, build logs, and manage deployment lifecycle",
domain: "Manage domains, SSL certificates, and routing for services",
docker: "Interact with Docker containers — inspect, restart, remove, and view logs",
backup: "Create and manage database backups, run manual backups, and restore from backups",
patch: "Browse and modify source code files in a service's cloned repository — read directories, read files, and create file patches",
mounts: "Manage persistent volume mounts for services",
port: "Manage exposed port mappings for services",
security: "Manage HTTP basic auth security rules for services",
redirects: "Manage HTTP redirect rules for domains",
registry: "Manage Docker registries for pulling private images",
sshKey: "Manage SSH keys for Git repository access",
rollback: "Rollback a service to a previous deployment",
schedule: "Create and manage scheduled tasks (cron jobs) for services",
previewDeployment: "Manage preview deployments for pull requests",
volumeBackups: "Create and manage volume-level backups and restores",
project: "Manage projects — create, update, delete, and list projects",
environment: "Manage environments within projects — create, duplicate, and configure",
server: "Manage servers — configure, monitor, and connect remote servers",
settings: "View and update global Dokploy settings",
destination: "Manage S3/storage destinations for backups",
tag: "Manage tags for organizing and labeling services",
cluster: "Manage Docker Swarm cluster nodes",
swarm: "Manage Docker Swarm settings and configuration",
certificates: "Manage SSL/TLS certificates",
gitProvider: "Manage Git provider integrations",
github: "Manage GitHub provider connections and repositories",
gitlab: "Manage GitLab provider connections and repositories",
bitbucket: "Manage Bitbucket provider connections and repositories",
gitea: "Manage Gitea provider connections and repositories",
user: "Manage user accounts and permissions",
};
export interface CatalogResult {
catalog: string;
count: number;
operationIds: Set<string>;
}
export function buildEndpointCatalog(
spec: OpenApiSpec,
contextType: ChatContext["type"] = "general",
relevantOperationIds?: Set<string>,
): CatalogResult {
const operationIds = new Set<string>();
const allowedTags = getAllowedTags(contextType);
const groups = new Map<string, string[]>();
for (const methods of Object.values(spec.paths)) {
for (const op of Object.values(methods)) {
if (!op.operationId || op.deprecated) continue;
if (op.tags?.some((t) => EXCLUDED_TAGS.has(t))) continue;
if (allowedTags && !op.tags?.some((t) => allowedTags.has(t))) continue;
if (relevantOperationIds && !relevantOperationIds.has(op.operationId)) continue;
operationIds.add(op.operationId);
const requiredParams: string[] = [];
const optionalParams: string[] = [];
if (op.parameters) {
for (const p of op.parameters) {
if (p.in === "header") continue;
if (p.required) {
requiredParams.push(`${p.name}*`);
} else {
optionalParams.push(`${p.name}?`);
}
}
}
if (op.requestBody?.content?.["application/json"]?.schema) {
const schema = op.requestBody.content["application/json"].schema;
const requiredSet = new Set(schema.required ?? []);
if (schema.properties) {
for (const [key, prop] of Object.entries(
schema.properties as Record<string, any>,
)) {
const enumVals = extractEnum(prop);
const hint = PROPERTY_HINTS[key];
const suffix = enumVals
? `[${enumVals.join("|")}]`
: hint
? `(${hint})`
: "";
if (requiredSet.has(key)) {
requiredParams.push(`${key}*${suffix}`);
} else {
optionalParams.push(`${key}?${suffix}`);
}
}
}
}
const allParams = [...requiredParams, ...optionalParams];
const paramStr =
allParams.length > 0 ? `(${allParams.join(", ")})` : "";
const summary = op.summary ? `${op.summary}` : "";
const desc = op.description ? `\n ${op.description}` : "";
const line = `${op.operationId}${paramStr}${summary}${desc}`;
const tag = op.tags?.[0] ?? "other";
if (!groups.has(tag)) groups.set(tag, []);
groups.get(tag)!.push(line);
}
}
// Order sections: context-specific tags first (in CONTEXT_TAGS order), then shared, then rest
const contextOrder = CONTEXT_TAGS[contextType];
const sharedOrder = SHARED_TAGS;
const orderedTags: string[] = [];
const seen = new Set<string>();
for (const t of contextOrder) {
if (groups.has(t) && !seen.has(t)) { orderedTags.push(t); seen.add(t); }
}
for (const t of sharedOrder) {
if (groups.has(t) && !seen.has(t)) { orderedTags.push(t); seen.add(t); }
}
for (const t of groups.keys()) {
if (!seen.has(t)) { orderedTags.push(t); seen.add(t); }
}
const sections: string[] = [];
for (const tag of orderedTags) {
const lines = groups.get(tag)!;
const tagDesc = TAG_DESCRIPTIONS[tag];
const header = tagDesc ? `## ${tag}${tagDesc}` : `## ${tag}`;
sections.push(`${header}\n${lines.join("\n")}`);
}
return {
catalog: sections.join("\n\n"),
count: operationIds.size,
operationIds,
};
}
/**
* Build a lookup map from operationId to endpoint info for execution.
*/
function buildEndpointMap(
spec: OpenApiSpec,
): Map<string, EndpointInfo> {
const map = new Map<string, EndpointInfo>();
for (const [path, methods] of Object.entries(spec.paths)) {
for (const [method, op] of Object.entries(methods)) {
if (!op.operationId) continue;
map.set(op.operationId, { method, path, operationId: op.operationId });
}
}
return map;
}
/**
* Creates a single "call_api" tool that only allows endpoints present in allowedOperationIds.
*/
export function createApiTool(
spec: OpenApiSpec,
config: ToolConfig,
allowedOperationIds?: Set<string>,
maxResponseSize = 4000,
) {
const endpointMap = buildEndpointMap(spec);
return {
call_api: dynamicTool({
description:
"Call a Dokploy API endpoint. Use the operationId from the endpoint catalog and pass the required parameters.",
inputSchema: z.object({
operationId: z
.string()
.describe("The operationId from the endpoint catalog"),
params: z
.record(z.string(), z.any())
.optional()
.describe("Parameters for the endpoint (* = required)"),
}),
execute: async (rawInput: unknown) => {
const { operationId, params } = rawInput as {
operationId: string;
params?: Record<string, unknown>;
};
if (allowedOperationIds && !allowedOperationIds.has(operationId)) {
return `Error: "${operationId}" is not available in the current context. Only use operationIds from the ENDPOINT CATALOG.`;
}
const endpoint = endpointMap.get(operationId);
if (!endpoint) {
return `Error: Unknown endpoint "${operationId}". Check the endpoint catalog for valid operationIds.`;
}
const headers: Record<string, string> = {
"Content-Type": "application/json",
Cookie: config.cookie,
};
let url = `${config.baseUrl}${endpoint.path}`;
if (endpoint.method === "get" && params) {
const searchParams = new URLSearchParams();
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null) {
searchParams.append(key, String(value));
}
}
const qs = searchParams.toString();
if (qs) url += `?${qs}`;
}
try {
const response = await fetch(url, {
method: endpoint.method.toUpperCase(),
headers,
...(endpoint.method !== "get" && params
? { body: JSON.stringify(params) }
: {}),
});
if (!response.ok) {
const errorText = await response.text();
return `API error (${response.status}): ${errorText.slice(0, 500)}\n\nHint: Check the ENDPOINT CATALOG for required parameters (*). You called "${operationId}" with params: ${JSON.stringify(params ?? {})}`;
}
const json = JSON.stringify(await response.json(), null, 2);
if (json.length > maxResponseSize) {
return `${json.slice(0, maxResponseSize)}\n\n... [Truncated — ${json.length} chars total]`;
}
return json;
} catch (error) {
return `Error: ${error instanceof Error ? error.message : "Unknown error"}`;
}
},
}),
};
}

View File

@@ -0,0 +1,319 @@
import { readFile } from "node:fs/promises";
import { dynamicTool } from "ai";
import { z } from "zod";
import { findDeploymentById } from "../../services/deployment";
export type ServiceType =
| "application"
| "compose"
| "postgres"
| "mysql"
| "redis"
| "mongo"
| "mariadb"
| "libsql";
export interface ChatContext {
type: ServiceType | "project" | "server" | "general";
id: string;
projectId?: string;
environmentId?: string;
serverId?: string;
}
interface ToolConfig {
baseUrl: string;
cookie: string;
}
async function callApi(
config: ToolConfig,
method: "GET" | "POST",
path: string,
params?: Record<string, unknown>,
) {
const headers: Record<string, string> = {
"Content-Type": "application/json",
Cookie: config.cookie,
};
let url = `${config.baseUrl}${path}`;
if (method === "GET" && params) {
const searchParams = new URLSearchParams();
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null) {
searchParams.append(key, String(value));
}
}
const qs = searchParams.toString();
if (qs) url += `?${qs}`;
}
const response = await fetch(url, {
method,
headers,
...(method === "POST" && params ? { body: JSON.stringify(params) } : {}),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`API error (${response.status}): ${errorText.slice(0, 500)}`);
}
return response.json();
}
function makeTool(
description: string,
inputSchema: z.ZodObject<z.ZodRawShape>,
executeFn: (input: Record<string, unknown>) => Promise<unknown>,
) {
return dynamicTool({
description,
inputSchema,
execute: async (rawInput: unknown) => {
try {
const input = (rawInput ?? {}) as Record<string, unknown>;
const result = await executeFn(input);
const json = JSON.stringify(result, null, 2);
// Truncate very large responses
if (json.length > 15000) {
return `${json.slice(0, 15000)}\n\n... [Truncated — ${json.length} chars total]`;
}
return json;
} catch (error) {
return `Error: ${error instanceof Error ? error.message : "Unknown error"}`;
}
},
});
}
// ─── READ TOOLS ──────────────────────────────────────────────
function readTools(context: ChatContext, config: ToolConfig) {
const tools: Record<string, ReturnType<typeof dynamicTool>> = {};
if (context.type === "application") {
tools["get-application-info"] = makeTool(
"Get the full configuration of the current application: name, status, build type, source, env vars, resource limits, and more. Call this first to understand the app state.",
z.object({}),
() => callApi(config, "GET", "/application.one", { applicationId: context.id }),
);
tools["list-deployments"] = makeTool(
"List the 10 most recent deployments for this application. Each deployment has a status (done/error/running), title, error message, and timestamps. Use this to find failed builds.",
z.object({}),
() => callApi(config, "GET", "/deployment.allByType", { id: context.id, type: "application" }),
);
tools["list-domains"] = makeTool(
"List all domains configured for this application.",
z.object({}),
() => callApi(config, "GET", "/domain.byApplicationId", { applicationId: context.id }),
);
tools["get-containers"] = makeTool(
"List running Docker containers for this application. Shows container state, status, and names.",
z.object({}),
async () => {
const app = await callApi(config, "GET", "/application.one", { applicationId: context.id });
return callApi(config, "GET", "/docker.getContainersByAppNameMatch", {
appName: (app as { appName: string }).appName,
});
},
);
}
if (context.type === "compose") {
tools["get-compose-info"] = makeTool(
"Get the full configuration of the current compose service: name, status, compose file content, env vars, and more.",
z.object({}),
() => callApi(config, "GET", "/compose.one", { composeId: context.id }),
);
tools["list-deployments"] = makeTool(
"List the 10 most recent deployments for this compose service.",
z.object({}),
() => callApi(config, "GET", "/deployment.allByType", { id: context.id, type: "compose" }),
);
tools["list-domains"] = makeTool(
"List all domains configured for this compose service.",
z.object({}),
() => callApi(config, "GET", "/domain.byComposeId", { composeId: context.id }),
);
}
if (context.type === "project") {
tools["get-project-info"] = makeTool(
"Get the full project details including ALL environments and ALL services (applications, compose, databases). Use this to count services, see what's deployed, and find failing services.",
z.object({}),
() => callApi(config, "GET", "/project.one", { projectId: context.id }),
);
}
if (context.type === "general") {
tools["list-projects"] = makeTool(
"List all projects in the organization.",
z.object({}),
() => callApi(config, "GET", "/project.all"),
);
}
// Available in both application and compose contexts
if (context.type === "application" || context.type === "compose") {
tools["read-deployment-logs"] = dynamicTool({
description:
"Read the build/deployment logs for a specific deployment. ALWAYS call list-deployments first to find the deploymentId. This reads the actual log file content to diagnose build failures.",
inputSchema: z.object({
deploymentId: z.string().describe("The deployment ID from list-deployments"),
}),
execute: async (rawInput: unknown) => {
const { deploymentId } = rawInput as { deploymentId: string };
try {
const deployment = await findDeploymentById(deploymentId);
const content = await readFile(deployment.logPath, "utf-8");
const lines = content.split("\n");
const last200 = lines.slice(-200).join("\n");
return `Deployment status: ${deployment.status}\nError message: ${deployment.errorMessage || "none"}\n\nLast 200 lines of build log:\n${last200}`;
} catch {
return "Could not read deployment logs — the log file may not exist.";
}
},
});
tools["read-runtime-logs"] = makeTool(
"Read the runtime/container logs (stdout/stderr) of this application. Shows the last N lines of the running application output. Use this to diagnose runtime errors, crashes, or check if the app is working.",
z.object({
tail: z.number().optional().describe("Number of lines to read (default 200, max 500)"),
}),
(input) => {
const tail = Math.min((input.tail as number) || 200, 500);
const endpoint = context.type === "compose" ? "/compose.readLogs" : "/application.readLogs";
const idKey = context.type === "compose" ? "composeId" : "applicationId";
return callApi(config, "GET", endpoint, {
[idKey]: context.id,
tail,
since: "all",
});
},
);
}
return tools;
}
// ─── WRITE TOOLS ─────────────────────────────────────────────
function writeTools(context: ChatContext, config: ToolConfig) {
const tools: Record<string, ReturnType<typeof dynamicTool>> = {};
if (context.type === "application") {
tools["update-env-vars"] = makeTool(
"Update the environment variables for this application. Pass the FULL env string (KEY=VALUE format, one per line). This REPLACES all existing env vars, so include the ones you want to keep.",
z.object({
env: z.string().describe("Full environment variables, one KEY=VALUE per line"),
}),
(input) =>
callApi(config, "POST", "/application.saveEnvironment", {
applicationId: context.id,
env: input.env,
}),
);
tools["deploy-application"] = makeTool(
"Trigger a new deployment/build for this application. The build will run in the background.",
z.object({}),
() =>
callApi(config, "POST", "/application.deploy", {
applicationId: context.id,
title: "AI-triggered deployment",
description: "Deployed via AI Assistant",
}),
);
tools["redeploy-application"] = makeTool(
"Redeploy the application using the existing build (no new build). Faster than deploy.",
z.object({}),
() =>
callApi(config, "POST", "/application.redeploy", {
applicationId: context.id,
title: "AI-triggered redeployment",
description: "Redeployed via AI Assistant",
}),
);
tools["stop-application"] = makeTool(
"Stop the application. This will stop all containers.",
z.object({}),
() => callApi(config, "POST", "/application.stop", { applicationId: context.id }),
);
tools["start-application"] = makeTool(
"Start a stopped application.",
z.object({}),
() => callApi(config, "POST", "/application.start", { applicationId: context.id }),
);
tools["restart-container"] = makeTool(
"Restart a specific Docker container. Use get-containers first to find the container ID.",
z.object({
containerId: z.string().describe("The container ID from get-containers"),
}),
(input) => callApi(config, "POST", "/docker.restartContainer", { containerId: input.containerId }),
);
}
if (context.type === "compose") {
tools["update-compose-env"] = makeTool(
"Update the environment variables for this compose service. Pass the FULL env string.",
z.object({
env: z.string().describe("Full environment variables, one KEY=VALUE per line"),
}),
(input) =>
callApi(config, "POST", "/compose.update", {
composeId: context.id,
env: input.env,
}),
);
tools["deploy-compose"] = makeTool(
"Trigger a new deployment for this compose service.",
z.object({}),
() =>
callApi(config, "POST", "/compose.deploy", {
composeId: context.id,
title: "AI-triggered deployment",
description: "Deployed via AI Assistant",
}),
);
tools["stop-compose"] = makeTool(
"Stop the compose service.",
z.object({}),
() => callApi(config, "POST", "/compose.stop", { composeId: context.id }),
);
tools["start-compose"] = makeTool(
"Start a stopped compose service.",
z.object({}),
() => callApi(config, "POST", "/compose.start", { composeId: context.id }),
);
}
return tools;
}
// ─── PUBLIC API ──────────────────────────────────────────────
export function getReadTools(context: ChatContext, config: ToolConfig) {
return readTools(context, config);
}
export function getAllTools(context: ChatContext, config: ToolConfig) {
return {
...readTools(context, config),
...writeTools(context, config),
};
}

View File

@@ -0,0 +1,317 @@
import { dynamicTool } from "ai";
import { z } from "zod";
/**
* Converts an OpenAPI spec into AI SDK tool definitions automatically.
*
* Each endpoint becomes a tool that the agent can call. The tool name
* is the operationId, the description comes from the endpoint's
* summary/description, and the input schema is derived from the
* request body or query parameters.
*/
interface OpenApiSpec {
paths: Record<string, Record<string, OpenApiOperation>>;
}
interface OpenApiOperation {
operationId: string;
summary?: string;
description?: string;
tags?: string[];
deprecated?: boolean;
parameters?: OpenApiParameter[];
requestBody?: {
required?: boolean;
content: Record<
string,
{
schema: JsonSchema;
}
>;
};
}
interface OpenApiParameter {
name: string;
in: "query" | "path" | "header";
required?: boolean;
schema: JsonSchema;
description?: string;
}
interface JsonSchema {
type?: string;
properties?: Record<string, JsonSchema>;
required?: string[];
items?: JsonSchema;
enum?: unknown[];
description?: string;
default?: unknown;
nullable?: boolean;
anyOf?: JsonSchema[];
oneOf?: JsonSchema[];
allOf?: JsonSchema[];
format?: string;
minimum?: number;
maximum?: number;
minLength?: number;
maxLength?: number;
}
interface ToolConfig {
baseUrl: string;
cookie: string;
}
interface GenerateToolsOptions {
/** Only include tools whose tag matches one of these */
tags?: string[];
/** Only include these specific operationIds */
operationIds?: string[];
/** Exclude these operationIds */
exclude?: string[];
/** Max response size in chars before truncating (default: 15000) */
maxResponseSize?: number;
}
// ─── JSON Schema → Zod conversion ──────────────────────────────
function jsonSchemaToZod(schema: JsonSchema): z.ZodTypeAny {
if (!schema || !schema.type) {
// anyOf / oneOf / allOf — just accept anything
if (schema?.anyOf || schema?.oneOf || schema?.allOf) {
return z.any();
}
return z.any();
}
switch (schema.type) {
case "string": {
let s = z.string();
if (schema.enum) {
return z.enum(schema.enum as [string, ...string[]]);
}
if (schema.minLength) s = s.min(schema.minLength);
if (schema.maxLength) s = s.max(schema.maxLength);
if (schema.description) s = s.describe(schema.description);
return s;
}
case "number":
case "integer": {
let n = z.number();
if (schema.minimum !== undefined) n = n.min(schema.minimum);
if (schema.maximum !== undefined) n = n.max(schema.maximum);
if (schema.description) n = n.describe(schema.description);
return n;
}
case "boolean":
return z.boolean();
case "array": {
const itemSchema = schema.items
? jsonSchemaToZod(schema.items)
: z.any();
return z.array(itemSchema);
}
case "object": {
if (!schema.properties) {
return z.object({});
}
const shape: Record<string, z.ZodTypeAny> = {};
const required = new Set(schema.required ?? []);
for (const [key, propSchema] of Object.entries(schema.properties)) {
const zodProp = jsonSchemaToZod(propSchema);
shape[key] = required.has(key) ? zodProp : zodProp.optional();
}
return z.object(shape);
}
default:
return z.any();
}
}
function buildInputSchema(
operation: OpenApiOperation,
): z.ZodObject<z.ZodRawShape> {
const shape: Record<string, z.ZodTypeAny> = {};
// Query/path parameters → flat keys
if (operation.parameters) {
for (const param of operation.parameters) {
if (param.in === "header") continue;
const zodParam = jsonSchemaToZod(param.schema);
const described = param.description
? zodParam.describe(param.description)
: zodParam;
shape[param.name] = param.required ? described : described.optional();
}
}
// Request body → merge properties into the same object
if (operation.requestBody) {
const content = operation.requestBody.content;
const jsonContent = content["application/json"];
if (jsonContent?.schema) {
const bodySchema = jsonContent.schema;
if (bodySchema.properties) {
const required = new Set(bodySchema.required ?? []);
for (const [key, propSchema] of Object.entries(
bodySchema.properties,
)) {
const zodProp = jsonSchemaToZod(propSchema);
shape[key] = required.has(key) ? zodProp : zodProp.optional();
}
}
}
}
return z.object(shape);
}
// ─── API caller ─────────────────────────────────────────────────
async function callApi(
config: ToolConfig,
method: string,
path: string,
params: Record<string, unknown> | undefined,
maxResponseSize: number,
): Promise<string> {
const headers: Record<string, string> = {
"Content-Type": "application/json",
Cookie: config.cookie,
};
let url = `${config.baseUrl}${path}`;
if (method === "get" && params) {
const searchParams = new URLSearchParams();
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null) {
searchParams.append(key, String(value));
}
}
const qs = searchParams.toString();
if (qs) url += `?${qs}`;
}
const response = await fetch(url, {
method: method.toUpperCase(),
headers,
...(method !== "get" && params
? { body: JSON.stringify(params) }
: {}),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(
`API error (${response.status}): ${errorText.slice(0, 500)}`,
);
}
const json = JSON.stringify(await response.json(), null, 2);
if (json.length > maxResponseSize) {
return `${json.slice(0, maxResponseSize)}\n\n... [Truncated — ${json.length} chars total]`;
}
return json;
}
// ─── Main conversion ────────────────────────────────────────────
export function openApiToTools(
spec: OpenApiSpec,
config: ToolConfig,
options: GenerateToolsOptions = {},
) {
const {
tags,
operationIds,
exclude,
maxResponseSize = 15000,
} = options;
const tagSet = tags ? new Set(tags) : null;
const idSet = operationIds ? new Set(operationIds) : null;
const excludeSet = exclude ? new Set(exclude) : null;
const tools: Record<string, ReturnType<typeof dynamicTool>> = {};
for (const [path, methods] of Object.entries(spec.paths)) {
for (const [method, operation] of Object.entries(methods)) {
const opId = operation.operationId;
if (!opId) continue;
if (operation.deprecated) continue;
// Filtering
if (excludeSet?.has(opId)) continue;
if (idSet && !idSet.has(opId)) continue;
if (tagSet && !operation.tags?.some((t) => tagSet.has(t))) continue;
const description = [operation.summary, operation.description]
.filter(Boolean)
.join(". ");
const inputSchema = buildInputSchema(operation);
const isWriteAction = method !== "get";
tools[opId] = dynamicTool({
description: description || `Call ${method.toUpperCase()} ${path}`,
inputSchema,
needsApproval: isWriteAction,
execute: async (rawInput: unknown) => {
const input = (rawInput ?? {}) as Record<string, unknown>;
const params =
Object.keys(input).length > 0 ? input : undefined;
return callApi(config, method, path, params, maxResponseSize);
},
});
}
}
return tools;
}
/**
* Returns a summary of all available tools (name + description).
* Useful for debugging or for the system prompt.
*/
export function getToolsSummary(
spec: OpenApiSpec,
options: GenerateToolsOptions = {},
): { name: string; description: string; tag: string; method: string }[] {
const { tags, operationIds, exclude } = options;
const tagSet = tags ? new Set(tags) : null;
const idSet = operationIds ? new Set(operationIds) : null;
const excludeSet = exclude ? new Set(exclude) : null;
const summary: {
name: string;
description: string;
tag: string;
method: string;
}[] = [];
for (const [_path, methods] of Object.entries(spec.paths)) {
for (const [method, operation] of Object.entries(methods)) {
const opId = operation.operationId;
if (!opId) continue;
if (operation.deprecated) continue;
if (excludeSet?.has(opId)) continue;
if (idSet && !idSet.has(opId)) continue;
if (tagSet && !operation.tags?.some((t) => tagSet.has(t))) continue;
summary.push({
name: opId,
description:
[operation.summary, operation.description]
.filter(Boolean)
.join(". ") || "",
tag: operation.tags?.[0] ?? "default",
method: method.toUpperCase(),
});
}
}
return summary;
}

View File

@@ -0,0 +1,250 @@
import { readFileSync, writeFileSync, existsSync } from "node:fs";
import { join } from "node:path";
import { PROPERTY_HINTS } from "./api-tool";
interface EndpointEmbedding {
operationId: string;
text: string;
tags: string[];
embedding: number[];
}
const VOYAGE_MODEL = "voyage-3-lite";
const VOYAGE_API = "https://api.voyageai.com/v1/embeddings";
const BATCH_SIZE = 128;
/**
* Call Voyage AI to embed an array of texts.
*/
async function embedTexts(
texts: string[],
apiKey: string,
inputType: "document" | "query" = "document",
): Promise<number[][]> {
const results: number[][] = [];
for (let i = 0; i < texts.length; i += BATCH_SIZE) {
const batch = texts.slice(i, i + BATCH_SIZE);
const response = await fetch(VOYAGE_API, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: VOYAGE_MODEL,
input: batch,
input_type: inputType,
}),
});
if (!response.ok) {
throw new Error(
`Voyage API error: ${response.status} ${await response.text()}`,
);
}
const data = (await response.json()) as {
data: { embedding: number[] }[];
};
for (const item of data.data) {
results.push(item.embedding);
}
}
return results;
}
/**
* Cosine similarity between two vectors.
*/
function cosineSimilarity(a: number[], b: number[]): number {
let dot = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i]! * b[i]!;
normA += a[i]! * a[i]!;
normB += b[i]! * b[i]!;
}
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}
// In-memory cache
let cachedEmbeddings: EndpointEmbedding[] | null = null;
/**
* Extract enum values from a JSON Schema property (handles anyOf wrappers).
*/
function extractEnum(prop: any): string[] | null {
if (prop?.enum) return prop.enum;
if (Array.isArray(prop?.anyOf)) {
for (const variant of prop.anyOf) {
if (variant?.enum) return variant.enum;
}
}
return null;
}
/**
* Build a rich text representation for an endpoint (used for embedding).
* Includes: operationId, method, path, params with enums, summary, description.
*/
function buildEndpointText(
op: any,
method: string,
path: string,
): string {
const parts: string[] = [];
// Operation identity
parts.push(`${op.operationId} [${method.toUpperCase()} ${path}]`);
// Tags
if (op.tags?.length) {
parts.push(`Tags: ${op.tags.join(", ")}`);
}
// Summary + description
if (op.summary) parts.push(op.summary);
if (op.description) parts.push(op.description);
// Parameters
const params: string[] = [];
if (op.parameters) {
for (const p of op.parameters) {
if (p.in === "header") continue;
const req = p.required ? "required" : "optional";
params.push(`${p.name} (${req})`);
}
}
if (op.requestBody?.content?.["application/json"]?.schema) {
const schema = op.requestBody.content["application/json"].schema;
const requiredSet = new Set(schema.required ?? []);
if (schema.properties) {
for (const [key, prop] of Object.entries(
schema.properties as Record<string, any>,
)) {
const req = requiredSet.has(key) ? "required" : "optional";
const enumVals = extractEnum(prop);
const hint = PROPERTY_HINTS[key];
const extra = enumVals
? ` [${enumVals.join("|")}]`
: hint
? `${hint}`
: "";
params.push(`${key} (${req})${extra}`);
}
}
}
if (params.length > 0) {
parts.push(`Parameters: ${params.join(", ")}`);
}
return parts.join(". ");
}
/**
* Generate or load embeddings for all endpoints in the OpenAPI spec.
* Embeddings are cached in .tool-embeddings.json and in memory.
*/
export async function getOrCreateEmbeddings(
spec: any,
voyageApiKey: string,
cachePath?: string,
): Promise<EndpointEmbedding[]> {
// Return from memory cache
if (cachedEmbeddings) return cachedEmbeddings;
// Try loading from file cache
const filePath =
cachePath || join(process.cwd(), ".tool-embeddings.json");
if (existsSync(filePath)) {
try {
const data = JSON.parse(readFileSync(filePath, "utf-8"));
if (Array.isArray(data) && data.length > 0 && data[0].embedding) {
cachedEmbeddings = data;
return cachedEmbeddings;
}
} catch {
// Corrupted file — regenerate
}
}
// Generate embeddings from spec
const endpoints: { operationId: string; text: string; tags: string[] }[] =
[];
for (const [path, methods] of Object.entries(spec.paths ?? {})) {
for (const [method, op] of Object.entries(methods as Record<string, any>)) {
if (!op.operationId || op.deprecated) continue;
endpoints.push({
operationId: op.operationId,
text: buildEndpointText(op, method, path),
tags: op.tags ?? [],
});
}
}
if (endpoints.length === 0) {
cachedEmbeddings = [];
return cachedEmbeddings;
}
const texts = endpoints.map((e) => e.text);
const embeddings = await embedTexts(texts, voyageApiKey, "document");
cachedEmbeddings = endpoints.map((e, i) => ({
...e,
embedding: embeddings[i]!,
}));
// Persist to file
try {
writeFileSync(filePath, JSON.stringify(cachedEmbeddings));
} catch {
// Non-critical — will regenerate next time
}
return cachedEmbeddings;
}
/**
* Retrieve the top-K most relevant endpoints for a user query,
* optionally filtered to a pre-computed set of allowed operationIds.
*/
export async function retrieveRelevantEndpoints(
query: string,
allEmbeddings: EndpointEmbedding[],
voyageApiKey: string,
options?: {
allowedOperationIds?: Set<string>;
topK?: number;
},
): Promise<string[]> {
const { allowedOperationIds, topK = 20 } = options ?? {};
// Filter to allowed operationIds (from tag filtering)
const candidates = allowedOperationIds
? allEmbeddings.filter((e) => allowedOperationIds.has(e.operationId))
: allEmbeddings;
if (candidates.length === 0) return [];
// Embed the user query
const [queryEmbedding] = await embedTexts([query], voyageApiKey, "query");
if (!queryEmbedding) return [];
// Score and rank
const scored = candidates.map((e) => ({
operationId: e.operationId,
score: cosineSimilarity(queryEmbedding, e.embedding),
}));
scored.sort((a, b) => b.score - a.score);
return scored.slice(0, topK).map((s) => s.operationId);
}

142
pnpm-lock.yaml generated
View File

@@ -113,12 +113,15 @@ importers:
'@ai-sdk/openai-compatible':
specifier: ^2.0.30
version: 2.0.30(zod@4.3.6)
'@ai-sdk/react':
specifier: ^3.0.156
version: 3.0.156(react@18.2.0)(zod@4.3.6)
'@better-auth/api-key':
specifier: 1.5.4
version: 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(febde88eaf587188179e6ecc47119e50))
version: 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(219be630f7f6fef2e235cef94eaddc25))
'@better-auth/sso':
specifier: 1.5.4
version: 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(febde88eaf587188179e6ecc47119e50))(better-call@2.0.2(zod@4.3.6))
version: 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(219be630f7f6fef2e235cef94eaddc25))(better-call@2.0.2(zod@4.3.6))
'@codemirror/autocomplete':
specifier: ^6.18.6
version: 6.20.0
@@ -147,8 +150,8 @@ importers:
specifier: workspace:*
version: link:../../packages/server
'@dokploy/trpc-openapi':
specifier: 0.0.18
version: 0.0.18(@trpc/server@11.10.0(typescript@5.9.3))(zod-openapi@5.4.6(zod@4.3.6))(zod@4.3.6)
specifier: 0.0.19
version: 0.0.19(@trpc/server@11.10.0(typescript@5.9.3))(zod-openapi@5.4.6(zod@4.3.6))(zod@4.3.6)
'@faker-js/faker':
specifier: ^8.4.1
version: 8.4.1
@@ -277,7 +280,7 @@ importers:
version: 5.1.1
better-auth:
specifier: 1.5.4
version: 1.5.4(febde88eaf587188179e6ecc47119e50)
version: 1.5.4(219be630f7f6fef2e235cef94eaddc25)
bl:
specifier: 6.0.11
version: 6.0.11
@@ -536,10 +539,10 @@ importers:
version: 5.9.3
vite-tsconfig-paths:
specifier: 4.3.2
version: 4.3.2(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.13)(jiti@1.21.7)(tsx@4.16.2)(yaml@2.8.1))
version: 4.3.2(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1))
vitest:
specifier: ^4.0.18
version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@1.21.7)(tsx@4.16.2)(yaml@2.8.1)
version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1)
apps/schedules:
dependencies:
@@ -627,10 +630,10 @@ importers:
version: 2.0.30(zod@4.3.6)
'@better-auth/api-key':
specifier: 1.5.4
version: 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(334901c35c1fcda64bb596793b2e4934))
version: 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(8933545d763d3f096150f97f9213a424))
'@better-auth/sso':
specifier: 1.5.4
version: 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(334901c35c1fcda64bb596793b2e4934))(better-call@2.0.2(zod@4.3.6))
version: 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(8933545d763d3f096150f97f9213a424))(better-call@2.0.2(zod@4.3.6))
'@better-auth/utils':
specifier: 0.3.1
version: 0.3.1
@@ -669,7 +672,7 @@ importers:
version: 5.1.1
better-auth:
specifier: 1.5.4
version: 1.5.4(334901c35c1fcda64bb596793b2e4934)
version: 1.5.4(8933545d763d3f096150f97f9213a424)
better-call:
specifier: 2.0.2
version: 2.0.2(zod@4.3.6)
@@ -772,7 +775,7 @@ importers:
devDependencies:
'@better-auth/cli':
specifier: 1.4.21
version: 1.4.21(@better-fetch/fetch@1.1.21)(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(better-call@2.0.2(zod@4.3.6))(drizzle-kit@0.31.9)(jose@6.1.3)(kysely@0.28.11)(mongodb@7.1.0)(mysql2@3.15.3)(nanostores@1.1.1)(next@16.2.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1))
version: 1.4.21(@better-fetch/fetch@1.1.21)(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(better-call@2.0.2(zod@4.3.6))(drizzle-kit@0.31.9)(jose@6.1.3)(kysely@0.28.11)(mongodb@7.1.0)(mysql2@3.15.3)(nanostores@1.1.1)(next@16.2.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@1.21.7)(tsx@4.16.2)(yaml@2.8.1))
'@types/adm-zip':
specifier: ^0.5.7
version: 0.5.7
@@ -878,6 +881,12 @@ packages:
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/gateway@3.0.94':
resolution: {integrity: sha512-uDDwLZhCkvC89crVS3S90D5L7AcVN8WriGuYVNYgVAaVcvy3Mthy3R9ICfzG75BObhz6pm2FWnhxDfNRK+t69Q==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/mistral@3.0.20':
resolution: {integrity: sha512-oZcx2pE6nJ+Qj/U6HFV5mJ52jXJPBSpvki/NtIocZkI/rKxphKBaecOH1h0Y7yK3HIbBxsMqefB1pb72cAHGVg==}
engines: {node: '>=18'}
@@ -902,10 +911,22 @@ packages:
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/provider-utils@4.0.23':
resolution: {integrity: sha512-z8GlDaCmRSDlqkMF2f4/RFgWxdarvIbyuk+m6WXT1LYgsnGiXRJGTD2Z1+SDl3LqtFuRtGX1aghYvQLoHL/9pg==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/provider@3.0.8':
resolution: {integrity: sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==}
engines: {node: '>=18'}
'@ai-sdk/react@3.0.156':
resolution: {integrity: sha512-/6rmGxOJlCNS6wJBUNsO49aeSK740fS2wVcA3Xn8IOBRFFz3hWm6auQTMoA0nHKu4hnH6ivA6hog6Ul+1Bv4Rg==}
engines: {node: '>=18'}
peerDependencies:
react: ^18 || ~19.0.1 || ~19.1.2 || ^19.2.1
'@alloc/quick-lru@5.2.0':
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
@@ -1291,8 +1312,8 @@ packages:
'@codemirror/view@6.39.15':
resolution: {integrity: sha512-aCWjgweIIXLBHh7bY6cACvXuyrZ0xGafjQ2VInjp4RM4gMfscK5uESiNdrH0pE+e1lZr2B4ONGsjchl2KsKZzg==}
'@dokploy/trpc-openapi@0.0.18':
resolution: {integrity: sha512-CbppvUEe8eK1fiNGQL5AH8KIRRlHk5bGPUEIyc2VBZE0un4kfUs5DXKSKsMLDomoES5ZEdrjT4nKpwYvhDha0w==}
'@dokploy/trpc-openapi@0.0.19':
resolution: {integrity: sha512-pmajIu1tIU3yqqbYdRHKFbA8gP37gO7F61PL9AFNtoyhh6gVxALXQLDabtE7dobKAhNDmIj6vV1a2vyP1Zi7/w==}
peerDependencies:
'@trpc/server': ^11.1.0
zod: ^4.3.6
@@ -4241,6 +4262,12 @@ packages:
peerDependencies:
ai: ^6.0.89
ai@6.0.154:
resolution: {integrity: sha512-HfKJKCTJsDZxqrIUDSVnBQ7DpQlx5WI4ExqtLd7Bl70epLmvkpc/HYMzU1hP9W+g9VEAcvZo4fbMqc3v5D+9gQ==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
ai@6.0.97:
resolution: {integrity: sha512-eZIAcBymwGhBwncRH/v9pillZNMeRCDkc4BwcvwXerXd7sxjVxRis3ZNCNCpP02pVH4NLs81ljm4cElC4vbNcQ==}
engines: {node: '>=18'}
@@ -7707,6 +7734,11 @@ packages:
react: '>=16.8.0 <20'
react-dom: '>=16.8.0 <20'
swr@2.4.1:
resolution: {integrity: sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==}
peerDependencies:
react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
symbol-observable@1.2.0:
resolution: {integrity: sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==}
engines: {node: '>=0.10.0'}
@@ -7764,6 +7796,10 @@ packages:
thread-stream@3.1.0:
resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==}
throttleit@2.1.0:
resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==}
engines: {node: '>=18'}
tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
@@ -8329,6 +8365,13 @@ snapshots:
'@vercel/oidc': 3.1.0
zod: 4.3.6
'@ai-sdk/gateway@3.0.94(zod@4.3.6)':
dependencies:
'@ai-sdk/provider': 3.0.8
'@ai-sdk/provider-utils': 4.0.23(zod@4.3.6)
'@vercel/oidc': 3.1.0
zod: 4.3.6
'@ai-sdk/mistral@3.0.20(zod@4.3.6)':
dependencies:
'@ai-sdk/provider': 3.0.8
@@ -8354,10 +8397,27 @@ snapshots:
eventsource-parser: 3.0.6
zod: 4.3.6
'@ai-sdk/provider-utils@4.0.23(zod@4.3.6)':
dependencies:
'@ai-sdk/provider': 3.0.8
'@standard-schema/spec': 1.1.0
eventsource-parser: 3.0.6
zod: 4.3.6
'@ai-sdk/provider@3.0.8':
dependencies:
json-schema: 0.4.0
'@ai-sdk/react@3.0.156(react@18.2.0)(zod@4.3.6)':
dependencies:
'@ai-sdk/provider-utils': 4.0.23(zod@4.3.6)
ai: 6.0.154(zod@4.3.6)
react: 18.2.0
swr: 2.4.1(react@18.2.0)
throttleit: 2.1.0
transitivePeerDependencies:
- zod
'@alloc/quick-lru@5.2.0': {}
'@authenio/xml-encryption@2.0.2':
@@ -8601,21 +8661,21 @@ snapshots:
'@balena/dockerignore@1.0.2': {}
'@better-auth/api-key@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(334901c35c1fcda64bb596793b2e4934))':
'@better-auth/api-key@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(219be630f7f6fef2e235cef94eaddc25))':
dependencies:
'@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1)
'@better-auth/utils': 0.3.1
better-auth: 1.5.4(334901c35c1fcda64bb596793b2e4934)
better-auth: 1.5.4(219be630f7f6fef2e235cef94eaddc25)
zod: 4.3.6
'@better-auth/api-key@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(febde88eaf587188179e6ecc47119e50))':
'@better-auth/api-key@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(8933545d763d3f096150f97f9213a424))':
dependencies:
'@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1)
'@better-auth/utils': 0.3.1
better-auth: 1.5.4(febde88eaf587188179e6ecc47119e50)
better-auth: 1.5.4(8933545d763d3f096150f97f9213a424)
zod: 4.3.6
'@better-auth/cli@1.4.21(@better-fetch/fetch@1.1.21)(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(better-call@2.0.2(zod@4.3.6))(drizzle-kit@0.31.9)(jose@6.1.3)(kysely@0.28.11)(mongodb@7.1.0)(mysql2@3.15.3)(nanostores@1.1.1)(next@16.2.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1))':
'@better-auth/cli@1.4.21(@better-fetch/fetch@1.1.21)(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(better-call@2.0.2(zod@4.3.6))(drizzle-kit@0.31.9)(jose@6.1.3)(kysely@0.28.11)(mongodb@7.1.0)(mysql2@3.15.3)(nanostores@1.1.1)(next@16.2.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@1.21.7)(tsx@4.16.2)(yaml@2.8.1))':
dependencies:
'@babel/core': 7.29.0
'@babel/preset-react': 7.28.5(@babel/core@7.29.0)
@@ -8627,7 +8687,7 @@ snapshots:
'@mrleebo/prisma-ast': 0.13.1
'@prisma/client': 5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))
'@types/pg': 8.16.0
better-auth: 1.4.21(b1bc00b9e18c5e6af4e13b03fc4304ac)
better-auth: 1.4.21(db78b83f9b5449d160708cdf9d272aa3)
better-sqlite3: 12.6.2
c12: 3.3.3
chalk: 5.6.2
@@ -8761,24 +8821,24 @@ snapshots:
'@prisma/client': 5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))
prisma: 7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)
'@better-auth/sso@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(334901c35c1fcda64bb596793b2e4934))(better-call@2.0.2(zod@4.3.6))':
'@better-auth/sso@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(219be630f7f6fef2e235cef94eaddc25))(better-call@2.0.2(zod@4.3.6))':
dependencies:
'@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1)
'@better-auth/utils': 0.3.1
'@better-fetch/fetch': 1.1.21
better-auth: 1.5.4(334901c35c1fcda64bb596793b2e4934)
better-auth: 1.5.4(219be630f7f6fef2e235cef94eaddc25)
better-call: 2.0.2(zod@4.3.6)
fast-xml-parser: 5.5.1
jose: 6.1.3
samlify: 2.10.2
zod: 4.3.6
'@better-auth/sso@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(febde88eaf587188179e6ecc47119e50))(better-call@2.0.2(zod@4.3.6))':
'@better-auth/sso@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(8933545d763d3f096150f97f9213a424))(better-call@2.0.2(zod@4.3.6))':
dependencies:
'@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1)
'@better-auth/utils': 0.3.1
'@better-fetch/fetch': 1.1.21
better-auth: 1.5.4(febde88eaf587188179e6ecc47119e50)
better-auth: 1.5.4(8933545d763d3f096150f97f9213a424)
better-call: 2.0.2(zod@4.3.6)
fast-xml-parser: 5.5.1
jose: 6.1.3
@@ -8946,7 +9006,7 @@ snapshots:
style-mod: 4.1.3
w3c-keyname: 2.2.8
'@dokploy/trpc-openapi@0.0.18(@trpc/server@11.10.0(typescript@5.9.3))(zod-openapi@5.4.6(zod@4.3.6))(zod@4.3.6)':
'@dokploy/trpc-openapi@0.0.19(@trpc/server@11.10.0(typescript@5.9.3))(zod-openapi@5.4.6(zod@4.3.6))(zod@4.3.6)':
dependencies:
'@trpc/server': 11.10.0(typescript@5.9.3)
co-body: 6.2.0
@@ -12244,6 +12304,7 @@ snapshots:
magic-string: 0.30.21
optionalDependencies:
vite: 7.3.1(@types/node@24.10.13)(jiti@1.21.7)(tsx@4.16.2)(yaml@2.8.1)
optional: true
'@vitest/mocker@4.0.18(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1))':
dependencies:
@@ -12252,7 +12313,6 @@ snapshots:
magic-string: 0.30.21
optionalDependencies:
vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1)
optional: true
'@vitest/pretty-format@4.0.18':
dependencies:
@@ -12337,6 +12397,14 @@ snapshots:
transitivePeerDependencies:
- zod
ai@6.0.154(zod@4.3.6):
dependencies:
'@ai-sdk/gateway': 3.0.94(zod@4.3.6)
'@ai-sdk/provider': 3.0.8
'@ai-sdk/provider-utils': 4.0.23(zod@4.3.6)
'@opentelemetry/api': 1.9.0
zod: 4.3.6
ai@6.0.97(zod@4.3.6):
dependencies:
'@ai-sdk/gateway': 3.0.53(zod@4.3.6)
@@ -12457,7 +12525,7 @@ snapshots:
before-after-hook@2.2.3: {}
better-auth@1.4.21(b1bc00b9e18c5e6af4e13b03fc4304ac):
better-auth@1.4.21(db78b83f9b5449d160708cdf9d272aa3):
dependencies:
'@better-auth/core': 1.4.21(@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.21(@better-auth/core@1.4.21(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))
@@ -12483,9 +12551,9 @@ snapshots:
prisma: 7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1)
vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@1.21.7)(tsx@4.16.2)(yaml@2.8.1)
better-auth@1.5.4(334901c35c1fcda64bb596793b2e4934):
better-auth@1.5.4(219be630f7f6fef2e235cef94eaddc25):
dependencies:
'@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1)
'@better-auth/drizzle-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))
@@ -12520,7 +12588,7 @@ snapshots:
transitivePeerDependencies:
- '@cloudflare/workers-types'
better-auth@1.5.4(febde88eaf587188179e6ecc47119e50):
better-auth@1.5.4(8933545d763d3f096150f97f9213a424):
dependencies:
'@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1)
'@better-auth/drizzle-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))
@@ -15935,6 +16003,12 @@ snapshots:
- '@types/react'
- debug
swr@2.4.1(react@18.2.0):
dependencies:
dequal: 2.0.3
react: 18.2.0
use-sync-external-store: 1.6.0(react@18.2.0)
symbol-observable@1.2.0: {}
tailwind-merge@2.6.1: {}
@@ -16025,6 +16099,8 @@ snapshots:
dependencies:
real-require: 0.2.0
throttleit@2.1.0: {}
tiny-invariant@1.3.3: {}
tiny-warning@1.0.3: {}
@@ -16278,13 +16354,13 @@ snapshots:
d3-time: 3.1.0
d3-timer: 3.0.1
vite-tsconfig-paths@4.3.2(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.13)(jiti@1.21.7)(tsx@4.16.2)(yaml@2.8.1)):
vite-tsconfig-paths@4.3.2(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1)):
dependencies:
debug: 4.4.3
globrex: 0.1.2
tsconfck: 3.1.6(typescript@5.9.3)
optionalDependencies:
vite: 7.3.1(@types/node@24.10.13)(jiti@1.21.7)(tsx@4.16.2)(yaml@2.8.1)
vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1)
transitivePeerDependencies:
- supports-color
- typescript
@@ -16303,6 +16379,7 @@ snapshots:
jiti: 1.21.7
tsx: 4.16.2
yaml: 2.8.1
optional: true
vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1):
dependencies:
@@ -16318,7 +16395,6 @@ snapshots:
jiti: 2.6.1
tsx: 4.16.2
yaml: 2.8.1
optional: true
vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@1.21.7)(tsx@4.16.2)(yaml@2.8.1):
dependencies:
@@ -16357,6 +16433,7 @@ snapshots:
- terser
- tsx
- yaml
optional: true
vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1):
dependencies:
@@ -16395,7 +16472,6 @@ snapshots:
- terser
- tsx
- yaml
optional: true
w3c-keyname@2.2.8: {}