mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
refactor(agents): reorganize into actions/components/schemas/hooks
This commit is contained in:
@@ -9,12 +9,12 @@ import { db } from "@/db";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {eq, isNull} from "drizzle-orm";
|
||||
import { notFound } from "next/navigation";
|
||||
import { ButtonDeleteAgent } from "@/features/agents/agent-delete-button";
|
||||
import { ButtonDeleteAgent } from "@/features/agents/components/agent-delete-button";
|
||||
import { capitalizeFirstLetter } from "@/utils/text";
|
||||
import { generateEdgeKey } from "@/utils/edge_key";
|
||||
import { getServerUrl } from "@/utils/get-server-url";
|
||||
import { AgentContentPage } from "@/features/agents/agent-content";
|
||||
import { AgentDialog } from "@/features/agents/agent-dialog";
|
||||
import { AgentContentPage } from "@/features/agents/components/agent-content";
|
||||
import { AgentDialog } from "@/features/agents/components/agent-dialog";
|
||||
|
||||
|
||||
export default async function RoutePage(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {PageParams} from "@/types/next";
|
||||
import {AgentCard} from "@/features/agents/agent-card";
|
||||
import {AgentCard} from "@/features/agents/components/agent-card";
|
||||
import {CardsWithPagination} from "@/components/common/cards-with-pagination";
|
||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||
import {notFound} from "next/navigation";
|
||||
@@ -7,7 +7,7 @@ import {db} from "@/db";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {and, desc, eq, isNull, not} from "drizzle-orm";
|
||||
import {Metadata} from "next";
|
||||
import {AgentDialog} from "@/features/agents/agent-dialog";
|
||||
import {AgentDialog} from "@/features/agents/components/agent-dialog";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Agents",
|
||||
|
||||
@@ -9,12 +9,12 @@ import {db} from "@/db";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {eq} from "drizzle-orm";
|
||||
import {notFound} from "next/navigation";
|
||||
import {ButtonDeleteAgent} from "@/features/agents/agent-delete-button";
|
||||
import {ButtonDeleteAgent} from "@/features/agents/components/agent-delete-button";
|
||||
import {capitalizeFirstLetter} from "@/utils/text";
|
||||
import {generateEdgeKey} from "@/utils/edge_key";
|
||||
import {getServerUrl} from "@/utils/get-server-url";
|
||||
import {AgentContentPage} from "@/features/agents/agent-content";
|
||||
import {AgentDialog} from "@/features/agents/agent-dialog";
|
||||
import {AgentContentPage} from "@/features/agents/components/agent-content";
|
||||
import {AgentDialog} from "@/features/agents/components/agent-dialog";
|
||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||
import {currentUser} from "@/lib/auth/current-user";
|
||||
import {computeOrganizationPermissions} from "@/lib/acl/organization-acl";
|
||||
|
||||
@@ -3,7 +3,7 @@ import {withApiKey} from "@/lib/api-v1/middleware";
|
||||
import {logger} from "@/lib/logger";
|
||||
import {ApiKeyContext} from "@/lib/api-v1/types";
|
||||
import {getAgent, resolveAgentAccess} from "@/lib/api-v1/services/agents";
|
||||
import {deleteAgentService} from "@/features/agents/agent-delete.action";
|
||||
import {deleteAgentService} from "@/features/agents/actions/agent-delete.action";
|
||||
|
||||
const log = logger.child({module: "api/v1/agents/[id]"});
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as drizzleDb from "@/db";
|
||||
import { inArray, eq, and, or, isNull } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { logger } from "@/lib/logger";
|
||||
import {createAgentService} from "@/features/agents/agents.action";
|
||||
import {createAgentService} from "@/features/agents/actions/agents.action";
|
||||
import { ActionError } from "@/lib/safe-actions/actions";
|
||||
import {getAccessibleAgentIds} from "@/lib/api-v1/services/agents";
|
||||
import {ApiKeyContext} from "@/lib/api-v1/types";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use server";
|
||||
import {action, ActionError, userAction} from "@/lib/safe-actions/actions";
|
||||
import {AgentSchema} from "@/features/agents/agents.schema";
|
||||
import {AgentSchema} from "@/features/agents/schemas/agents.schema";
|
||||
import {z} from "zod";
|
||||
import {eq, and, ne, count, desc} from "drizzle-orm";
|
||||
import {db} from "@/db";
|
||||
@@ -11,7 +11,7 @@ import {Badge} from "@/components/ui/badge";
|
||||
import {truncateWords} from "@/utils/text";
|
||||
import {useIsMobile} from "@/hooks/use-mobile";
|
||||
import {Tooltip, TooltipContent, TooltipTrigger} from "@/components/ui/tooltip";
|
||||
import {useAgentUpdateCheck} from "@/features/agents/use-agent-update-check";
|
||||
import {useAgentUpdateCheck} from "@/features/agents/hooks/use-agent-update-check";
|
||||
|
||||
export type agentCardProps = {
|
||||
data: AgentWith;
|
||||
+3
-3
@@ -3,10 +3,10 @@
|
||||
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
|
||||
import {Server} from "lucide-react";
|
||||
import {formatDateLastContact} from "@/utils/date-formatting";
|
||||
import {AgentCardKey} from "@/features/agents/agent-card-key";
|
||||
import {AgentCardKey} from "@/features/agents/components/agent-card-key";
|
||||
import {AgentWithDatabases} from "@/db/schema/08_agent";
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import {getAgentAction} from "@/features/agents/agents.action";
|
||||
import {getAgentAction} from "@/features/agents/actions/agents.action";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import {Separator} from "@/components/ui/separator";
|
||||
import {Badge} from "@/components/ui/badge";
|
||||
import {CardsWithPagination} from "@/components/common/cards-with-pagination";
|
||||
import {AgentDatabaseCard} from "@/features/agents/agent-database-card";
|
||||
import {AgentDatabaseCard} from "@/features/agents/components/agent-database-card";
|
||||
import {HealthCheckGraph} from "@/features/database/health-grid";
|
||||
import {HealthcheckLog} from "@/db/schema/15_healthcheck-log";
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import {ButtonWithConfirm} from "@/components/common/button-with-confirm";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {toast} from "sonner";
|
||||
import {deleteAgentAction} from "@/features/agents/agent-delete.action";
|
||||
import {deleteAgentAction} from "@/features/agents/actions/agent-delete.action";
|
||||
import {useIsMobile} from "@/hooks/use-mobile";
|
||||
|
||||
export type ButtonDeleteAgentProps = {
|
||||
+2
-2
@@ -7,7 +7,7 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {AgentForm} from "@/features/agents/agent-form";
|
||||
import {AgentForm} from "@/features/agents/components/agent-form";
|
||||
import {Button, buttonVariants} from "@/components/ui/button";
|
||||
import {Plus} from "lucide-react";
|
||||
import {GearIcon} from "@radix-ui/react-icons";
|
||||
@@ -16,7 +16,7 @@ import {useState} from "react";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
|
||||
import {AgentOrganisationForm} from "@/features/agents/agent-organizations.form";
|
||||
import {AgentOrganisationForm} from "@/features/agents/components/agent-organizations.form";
|
||||
import {AgentWith} from "@/db/schema/08_agent";
|
||||
|
||||
type AgentDialogProps = {
|
||||
@@ -15,9 +15,9 @@ import {Button} from "@/components/ui/button";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {useMutation, useQueryClient} from "@tanstack/react-query";
|
||||
import {TooltipProvider} from "@/components/ui/tooltip";
|
||||
import {AgentSchema, AgentType} from "@/features/agents/agents.schema";
|
||||
import {AgentSchema, AgentType} from "@/features/agents/schemas/agents.schema";
|
||||
import {toast} from "sonner";
|
||||
import {createAgentAction, updateAgentAction} from "@/features/agents/agents.action";
|
||||
import {createAgentAction, updateAgentAction} from "@/features/agents/actions/agents.action";
|
||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
|
||||
export type agentFormProps = {
|
||||
+2
-2
@@ -8,8 +8,8 @@ import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
import {MultiSelect} from "@/components/common/multi-select";
|
||||
import {toast} from "sonner";
|
||||
import {AgentWith} from "@/db/schema/08_agent";
|
||||
import {AgentOrganizationSchema, AgentOrganizationType} from "@/features/agents/agent-organizations.schema";
|
||||
import {updateAgentOrganizationsAction} from "@/features/agents/agent-organizations.action";
|
||||
import {AgentOrganizationSchema, AgentOrganizationType} from "@/features/agents/schemas/agent-organizations.schema";
|
||||
import {updateAgentOrganizationsAction} from "@/features/agents/actions/agent-organizations.action";
|
||||
|
||||
|
||||
type AgentOrganisationFormProps = {
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { userAction } from "@/lib/safe-actions/actions";
|
||||
import { z } from "zod";
|
||||
import { getAgentAction } from "@/features/agents/agents.action";
|
||||
import { getAgentAction } from "@/features/agents/actions/agents.action";
|
||||
|
||||
export const getAgentStatusAction = userAction
|
||||
.schema(z.object({ agentId: z.string() }))
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useOnboarding } from "@onboardjs/react";
|
||||
import { toast } from "sonner";
|
||||
import { createAgentAction } from "@/features/agents/agents.action";
|
||||
import { createAgentAction } from "@/features/agents/actions/agents.action";
|
||||
import type { OnboardingAgent, OnboardingDefaultsData } from "@/features/onboarding/types";
|
||||
import { generateEdgeKey } from "@/utils/edge_key";
|
||||
import { getServerUrl } from "@/utils/get-server-url";
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useOnboarding } from "@onboardjs/react";
|
||||
import { toast } from "sonner";
|
||||
import { deleteAgentAction } from "@/features/agents/agent-delete.action";
|
||||
import { deleteAgentAction } from "@/features/agents/actions/agent-delete.action";
|
||||
import type { OnboardingAgent } from "@/features/onboarding/types";
|
||||
|
||||
export const useDeleteAgent = () => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useState } from "react";
|
||||
import { useOnboarding } from "@onboardjs/react";
|
||||
import { Server } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { AgentCardKey } from "@/features/agents/agent-card-key";
|
||||
import { AgentCardKey } from "@/features/agents/components/agent-card-key";
|
||||
import type { OnboardingAgent } from "@/features/onboarding/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
import {cn} from "@/lib/utils";
|
||||
import {Agent} from "@/db/schema/08_agent";
|
||||
import {CardsWithPagination} from "@/components/common/cards-with-pagination";
|
||||
import {AgentCard} from "@/features/agents/agent-card";
|
||||
import {AgentDialog} from "@/features/agents/agent-dialog";
|
||||
import {AgentCard} from "@/features/agents/components/agent-card";
|
||||
import {AgentDialog} from "@/features/agents/components/agent-dialog";
|
||||
|
||||
export type OrganizationAgentsTabProps = {
|
||||
organization: OrganizationWithMembers;
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
} from "@/features/settings/storage.action";
|
||||
import {toast} from "sonner";
|
||||
import {Switch} from "@/components/ui/switch";
|
||||
import {downloadMasterKeyAction} from "@/features/agents/keys.action";
|
||||
import {downloadMasterKeyAction} from "@/features/agents/actions/keys.action";
|
||||
|
||||
export type SettingsStorageSectionProps = {
|
||||
settings: Setting;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from "zod";
|
||||
import { OpenAPIRegistry } from "@asteasolutions/zod-to-openapi";
|
||||
import "@/lib/api-v1/openapi/registry";
|
||||
import {AgentSchema} from "@/features/agents/agents.schema";
|
||||
import {AgentSchema} from "@/features/agents/schemas/agents.schema";
|
||||
import {agentSchema} from "@/db/schema/08_agent";
|
||||
|
||||
const UuidParam = z
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
"use server";
|
||||
import { auth } from "@/lib/auth/auth";
|
||||
import { headers } from "next/headers";
|
||||
import {User} from "@/db/schema/02_user";
|
||||
import { User } from "@/db/schema/02_user";
|
||||
|
||||
export const currentUser = async () => {
|
||||
const session = await auth.api.getSession({
|
||||
headers: await headers(),
|
||||
});
|
||||
const session = await auth.api.getSession({
|
||||
headers: await headers(),
|
||||
});
|
||||
|
||||
if (!session?.user) {
|
||||
return null;
|
||||
}
|
||||
if (!session?.user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return session.user as User;
|
||||
return session.user as User;
|
||||
};
|
||||
|
||||
@@ -2,19 +2,27 @@ import { createHmac } from "crypto";
|
||||
import { env } from "@/env.mjs";
|
||||
|
||||
export function signPasskeyContext(name: string, email: string): string {
|
||||
const payload = Buffer.from(JSON.stringify({ name, email, exp: Date.now() + 5 * 60 * 1000 })).toString("base64url");
|
||||
const sig = createHmac("sha256", env.PROJECT_SECRET).update(payload).digest("base64url");
|
||||
return `${payload}.${sig}`;
|
||||
const payload = Buffer.from(
|
||||
JSON.stringify({ name, email, exp: Date.now() + 5 * 60 * 1000 }),
|
||||
).toString("base64url");
|
||||
const sig = createHmac("sha256", env.PROJECT_SECRET)
|
||||
.update(payload)
|
||||
.digest("base64url");
|
||||
return `${payload}.${sig}`;
|
||||
}
|
||||
|
||||
export function verifyPasskeyContext(token: string): { name: string; email: string } | null {
|
||||
const dot = token.lastIndexOf(".");
|
||||
if (dot === -1) return null;
|
||||
const payload = token.slice(0, dot);
|
||||
const sig = token.slice(dot + 1);
|
||||
const expected = createHmac("sha256", env.PROJECT_SECRET).update(payload).digest("base64url");
|
||||
if (sig !== expected) return null;
|
||||
const data = JSON.parse(Buffer.from(payload, "base64url").toString());
|
||||
if (data.exp < Date.now()) return null;
|
||||
return { name: data.name, email: data.email };
|
||||
export function verifyPasskeyContext(
|
||||
token: string,
|
||||
): { name: string; email: string } | null {
|
||||
const dot = token.lastIndexOf(".");
|
||||
if (dot === -1) return null;
|
||||
const payload = token.slice(0, dot);
|
||||
const sig = token.slice(dot + 1);
|
||||
const expected = createHmac("sha256", env.PROJECT_SECRET)
|
||||
.update(payload)
|
||||
.digest("base64url");
|
||||
if (sig !== expected) return null;
|
||||
const data = JSON.parse(Buffer.from(payload, "base64url").toString());
|
||||
if (data.exp < Date.now()) return null;
|
||||
return { name: data.name, email: data.email };
|
||||
}
|
||||
|
||||
@@ -48,7 +48,6 @@ const pending = ac.newRole({
|
||||
agent: [],
|
||||
});
|
||||
|
||||
//org
|
||||
const orgMember = ac.newRole({
|
||||
project: ["list"],
|
||||
database: ["list"],
|
||||
|
||||
+13
-13
@@ -1,16 +1,16 @@
|
||||
import nodemailer from "nodemailer";
|
||||
import {Server} from "./types"
|
||||
import { Server } from "./types";
|
||||
|
||||
export const createTransporter = (server: Server) => {
|
||||
const portNumber = Number(server.port);
|
||||
return nodemailer.createTransport({
|
||||
pool: true,
|
||||
host: server.host,
|
||||
port: portNumber,
|
||||
secure: server.secure ?? portNumber === 465,
|
||||
auth: {
|
||||
user: server.user,
|
||||
pass: server.pass,
|
||||
},
|
||||
});
|
||||
};
|
||||
const portNumber = Number(server.port);
|
||||
return nodemailer.createTransport({
|
||||
pool: true,
|
||||
host: server.host,
|
||||
port: portNumber,
|
||||
secure: server.secure ?? portNumber === 465,
|
||||
auth: {
|
||||
user: server.user,
|
||||
pass: server.pass,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
+38
-33
@@ -1,42 +1,47 @@
|
||||
"use server"
|
||||
import {db} from "@/db";
|
||||
import {eq} from "drizzle-orm";
|
||||
"use server";
|
||||
import { db } from "@/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {createTransporter} from "@/lib/email/helpers";
|
||||
import {Payload} from "@/lib/email/types";
|
||||
import { createTransporter } from "@/lib/email/helpers";
|
||||
import { Payload } from "@/lib/email/types";
|
||||
|
||||
export const sendEmail = async (data: Payload) => {
|
||||
const settings = await db
|
||||
.select()
|
||||
.from(drizzleDb.schemas.setting)
|
||||
.where(eq(drizzleDb.schemas.setting.name, "system"))
|
||||
.then((res) => res[0]);
|
||||
const settings = await db
|
||||
.select()
|
||||
.from(drizzleDb.schemas.setting)
|
||||
.where(eq(drizzleDb.schemas.setting.name, "system"))
|
||||
.then((res) => res[0]);
|
||||
|
||||
if (!settings) {
|
||||
throw new Error("SMTP system settings not found.");
|
||||
}
|
||||
if (!settings) {
|
||||
throw new Error("SMTP system settings not found.");
|
||||
}
|
||||
|
||||
if (!settings.smtpHost || !settings.smtpPort || !settings.smtpUser || !settings.smtpPassword || !settings.smtpFrom) {
|
||||
console.warn("Incomplete SMTP settings. Email not sent.");
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!settings.smtpHost ||
|
||||
!settings.smtpPort ||
|
||||
!settings.smtpUser ||
|
||||
!settings.smtpPassword ||
|
||||
!settings.smtpFrom
|
||||
) {
|
||||
console.warn("Incomplete SMTP settings. Email not sent.");
|
||||
return;
|
||||
}
|
||||
|
||||
const emailsArray = data.to.split(",")
|
||||
.map(email => email.trim());
|
||||
const emailsArray = data.to.split(",").map((email) => email.trim());
|
||||
|
||||
const transporter = createTransporter({
|
||||
host: settings.smtpHost,
|
||||
port: Number(settings.smtpPort),
|
||||
user: settings.smtpUser,
|
||||
pass: settings.smtpPassword,
|
||||
from: settings.smtpFrom,
|
||||
secure: settings.smtpSecure ?? false,
|
||||
});
|
||||
await transporter.verify();
|
||||
const transporter = createTransporter({
|
||||
host: settings.smtpHost,
|
||||
port: Number(settings.smtpPort),
|
||||
user: settings.smtpUser,
|
||||
pass: settings.smtpPassword,
|
||||
from: settings.smtpFrom,
|
||||
secure: settings.smtpSecure ?? false,
|
||||
});
|
||||
await transporter.verify();
|
||||
|
||||
return await transporter.sendMail({
|
||||
...data,
|
||||
to: emailsArray,
|
||||
from: settings.smtpFrom ?? undefined,
|
||||
});
|
||||
return await transporter.sendMail({
|
||||
...data,
|
||||
to: emailsArray,
|
||||
from: settings.smtpFrom ?? undefined,
|
||||
});
|
||||
};
|
||||
|
||||
+10
-12
@@ -1,19 +1,17 @@
|
||||
"use server";
|
||||
|
||||
|
||||
export type Payload = {
|
||||
to: string;
|
||||
from?: string;
|
||||
subject: string;
|
||||
html: any;
|
||||
to: string;
|
||||
from?: string;
|
||||
subject: string;
|
||||
html: any;
|
||||
};
|
||||
|
||||
export type Server = {
|
||||
host: string;
|
||||
port: number;
|
||||
user: string;
|
||||
pass: string;
|
||||
from: string;
|
||||
secure: boolean;
|
||||
host: string;
|
||||
port: number;
|
||||
user: string;
|
||||
pass: string;
|
||||
from: string;
|
||||
secure: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -4,14 +4,10 @@ export type ApiV1Result =
|
||||
| { ok: true; status: number; data: unknown }
|
||||
| { ok: false; status: number; error: string };
|
||||
|
||||
/**
|
||||
* Thin fetch wrapper that proxies calls to the existing /api/v1 REST layer.
|
||||
* Forwards the caller's API key so the REST route runs its own auth + permission checks.
|
||||
*/
|
||||
export async function apiV1Fetch(
|
||||
path: string,
|
||||
options: RequestInit,
|
||||
apiKey: string
|
||||
apiKey: string,
|
||||
): Promise<ApiV1Result> {
|
||||
const url = `${getServerUrl()}${path}`;
|
||||
|
||||
@@ -29,7 +25,6 @@ export async function apiV1Fetch(
|
||||
return { ok: false, status: 0, error: "Failed to reach internal API" };
|
||||
}
|
||||
|
||||
// 204 No Content has no body
|
||||
if (res.status === 204) {
|
||||
return { ok: true, status: 204, data: null };
|
||||
}
|
||||
@@ -53,9 +48,7 @@ export async function apiV1Fetch(
|
||||
}
|
||||
|
||||
const data =
|
||||
typeof body === "object" &&
|
||||
body !== null &&
|
||||
"data" in body
|
||||
typeof body === "object" && body !== null && "data" in body
|
||||
? (body as { data: unknown }).data
|
||||
: body;
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import { registerBackupTools } from "./tools/backups";
|
||||
*/
|
||||
export function createPortabaseMcpServer(
|
||||
_ctx: ApiKeyContext,
|
||||
apiKey: string
|
||||
apiKey: string,
|
||||
): McpServer {
|
||||
const server = new McpServer({
|
||||
name: "portabase",
|
||||
|
||||
+28
-14
@@ -1,8 +1,7 @@
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
import {apiV1Fetch} from "@/lib/mcp/http-client";
|
||||
import {err, ok} from "@/lib/mcp/tools/response";
|
||||
|
||||
import { apiV1Fetch } from "@/lib/mcp/http-client";
|
||||
import { err, ok } from "@/lib/mcp/tools/response";
|
||||
|
||||
export function registerAgentTools(server: McpServer, apiKey: string) {
|
||||
server.tool(
|
||||
@@ -10,9 +9,13 @@ export function registerAgentTools(server: McpServer, apiKey: string) {
|
||||
"List all agents accessible to the authenticated user",
|
||||
{},
|
||||
async () => {
|
||||
const result = await apiV1Fetch("/api/v1/agents", { method: "GET" }, apiKey);
|
||||
const result = await apiV1Fetch(
|
||||
"/api/v1/agents",
|
||||
{ method: "GET" },
|
||||
apiKey,
|
||||
);
|
||||
return result.ok ? ok(result.data) : err(result.error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
@@ -20,9 +23,13 @@ export function registerAgentTools(server: McpServer, apiKey: string) {
|
||||
"Get details for a specific agent, including its associated databases",
|
||||
{ id: z.string().describe("Agent ID") },
|
||||
async ({ id }) => {
|
||||
const result = await apiV1Fetch(`/api/v1/agents/${id}`, { method: "GET" }, apiKey);
|
||||
const result = await apiV1Fetch(
|
||||
`/api/v1/agents/${id}`,
|
||||
{ method: "GET" },
|
||||
apiKey,
|
||||
);
|
||||
return result.ok ? ok(result.data) : err(result.error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
@@ -31,7 +38,6 @@ export function registerAgentTools(server: McpServer, apiKey: string) {
|
||||
{
|
||||
name: z.string().min(1).describe("Agent name"),
|
||||
organizationId: z
|
||||
.string()
|
||||
.uuid()
|
||||
.optional()
|
||||
.describe("Organization ID to scope the agent to (optional)"),
|
||||
@@ -40,10 +46,10 @@ export function registerAgentTools(server: McpServer, apiKey: string) {
|
||||
const result = await apiV1Fetch(
|
||||
"/api/v1/agents",
|
||||
{ method: "POST", body: JSON.stringify({ name, organizationId }) },
|
||||
apiKey
|
||||
apiKey,
|
||||
);
|
||||
return result.ok ? ok(result.data) : err(result.error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
@@ -51,11 +57,15 @@ export function registerAgentTools(server: McpServer, apiKey: string) {
|
||||
"Delete an agent by ID",
|
||||
{ id: z.string().describe("Agent ID") },
|
||||
async ({ id }) => {
|
||||
const result = await apiV1Fetch(`/api/v1/agents/${id}`, { method: "DELETE" }, apiKey);
|
||||
const result = await apiV1Fetch(
|
||||
`/api/v1/agents/${id}`,
|
||||
{ method: "DELETE" },
|
||||
apiKey,
|
||||
);
|
||||
return result.ok
|
||||
? ok({ message: `Agent ${id} deleted successfully` })
|
||||
: err(result.error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
@@ -63,8 +73,12 @@ export function registerAgentTools(server: McpServer, apiKey: string) {
|
||||
"Get the edge key for an agent (used by the agent to authenticate with Portabase)",
|
||||
{ id: z.string().describe("Agent ID") },
|
||||
async ({ id }) => {
|
||||
const result = await apiV1Fetch(`/api/v1/agents/${id}/key`, { method: "GET" }, apiKey);
|
||||
const result = await apiV1Fetch(
|
||||
`/api/v1/agents/${id}/key`,
|
||||
{ method: "GET" },
|
||||
apiKey,
|
||||
);
|
||||
return result.ok ? ok(result.data) : err(result.error);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
import {err, ok} from "@/lib/mcp/tools/response";
|
||||
import {apiV1Fetch} from "@/lib/mcp/http-client";
|
||||
import { err, ok } from "@/lib/mcp/tools/response";
|
||||
import { apiV1Fetch } from "@/lib/mcp/http-client";
|
||||
|
||||
export function registerBackupTools(server: McpServer, apiKey: string) {
|
||||
server.tool(
|
||||
@@ -12,10 +12,10 @@ export function registerBackupTools(server: McpServer, apiKey: string) {
|
||||
const result = await apiV1Fetch(
|
||||
`/api/v1/databases/${databaseId}/backup`,
|
||||
{ method: "GET" },
|
||||
apiKey
|
||||
apiKey,
|
||||
);
|
||||
return result.ok ? ok(result.data) : err(result.error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
@@ -29,10 +29,10 @@ export function registerBackupTools(server: McpServer, apiKey: string) {
|
||||
const result = await apiV1Fetch(
|
||||
`/api/v1/databases/${databaseId}/backup/${backupId}`,
|
||||
{ method: "GET" },
|
||||
apiKey
|
||||
apiKey,
|
||||
);
|
||||
return result.ok ? ok(result.data) : err(result.error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
@@ -43,10 +43,10 @@ export function registerBackupTools(server: McpServer, apiKey: string) {
|
||||
const result = await apiV1Fetch(
|
||||
`/api/v1/databases/${databaseId}/backup`,
|
||||
{ method: "POST", body: JSON.stringify({}) },
|
||||
apiKey
|
||||
apiKey,
|
||||
);
|
||||
return result.ok ? ok(result.data) : err(result.error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
@@ -56,7 +56,6 @@ export function registerBackupTools(server: McpServer, apiKey: string) {
|
||||
databaseId: z.string().describe("Database ID"),
|
||||
backupId: z.string().uuid().describe("Backup ID"),
|
||||
backupStorageId: z
|
||||
.string()
|
||||
.uuid()
|
||||
.describe("Backup storage ID (from get_backup storages list)"),
|
||||
},
|
||||
@@ -67,9 +66,9 @@ export function registerBackupTools(server: McpServer, apiKey: string) {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ backupId, backupStorageId }),
|
||||
},
|
||||
apiKey
|
||||
apiKey,
|
||||
);
|
||||
return result.ok ? ok(result.data) : err(result.error);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,41 +1,48 @@
|
||||
import {McpServer} from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import {z} from "zod";
|
||||
import {err, ok} from "@/lib/mcp/tools/response";
|
||||
import {apiV1Fetch} from "@/lib/mcp/http-client";
|
||||
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
import { err, ok } from "@/lib/mcp/tools/response";
|
||||
import { apiV1Fetch } from "@/lib/mcp/http-client";
|
||||
|
||||
export function registerDatabaseTools(server: McpServer, apiKey: string) {
|
||||
server.tool(
|
||||
"list_databases",
|
||||
"List all databases accessible to the authenticated user",
|
||||
{},
|
||||
async () => {
|
||||
const result = await apiV1Fetch("/api/v1/databases", {method: "GET"}, apiKey);
|
||||
return result.ok ? ok(result.data) : err(result.error);
|
||||
}
|
||||
);
|
||||
server.tool(
|
||||
"list_databases",
|
||||
"List all databases accessible to the authenticated user",
|
||||
{},
|
||||
async () => {
|
||||
const result = await apiV1Fetch(
|
||||
"/api/v1/databases",
|
||||
{ method: "GET" },
|
||||
apiKey,
|
||||
);
|
||||
return result.ok ? ok(result.data) : err(result.error);
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"get_database",
|
||||
"Get details for a specific database",
|
||||
{id: z.string().describe("Database ID")},
|
||||
async ({id}) => {
|
||||
const result = await apiV1Fetch(`/api/v1/databases/${id}`, {method: "GET"}, apiKey);
|
||||
return result.ok ? ok(result.data) : err(result.error);
|
||||
}
|
||||
);
|
||||
server.tool(
|
||||
"get_database",
|
||||
"Get details for a specific database",
|
||||
{ id: z.string().describe("Database ID") },
|
||||
async ({ id }) => {
|
||||
const result = await apiV1Fetch(
|
||||
`/api/v1/databases/${id}`,
|
||||
{ method: "GET" },
|
||||
apiKey,
|
||||
);
|
||||
return result.ok ? ok(result.data) : err(result.error);
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"get_database_status",
|
||||
"Get the current status of a database, including latest backup and restoration state",
|
||||
{id: z.string().describe("Database ID")},
|
||||
async ({id}) => {
|
||||
const result = await apiV1Fetch(
|
||||
`/api/v1/databases/${id}/status`,
|
||||
{method: "GET"},
|
||||
apiKey
|
||||
);
|
||||
return result.ok ? ok(result.data) : err(result.error);
|
||||
}
|
||||
);
|
||||
server.tool(
|
||||
"get_database_status",
|
||||
"Get the current status of a database, including latest backup and restoration state",
|
||||
{ id: z.string().describe("Database ID") },
|
||||
async ({ id }) => {
|
||||
const result = await apiV1Fetch(
|
||||
`/api/v1/databases/${id}/status`,
|
||||
{ method: "GET" },
|
||||
apiKey,
|
||||
);
|
||||
return result.ok ? ok(result.data) : err(result.error);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
export function ok(data: unknown) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
|
||||
};
|
||||
return {
|
||||
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
|
||||
};
|
||||
}
|
||||
|
||||
export function err(message: string) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: message }],
|
||||
isError: true as const,
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [{ type: "text" as const, text: message }],
|
||||
isError: true as const,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
import {createSafeActionClient} from "next-safe-action";
|
||||
import {currentUser} from "@/lib/auth/current-user";
|
||||
import { createSafeActionClient } from "next-safe-action";
|
||||
import { currentUser } from "@/lib/auth/current-user";
|
||||
|
||||
export class ActionError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "ActionError";
|
||||
}
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "ActionError";
|
||||
}
|
||||
}
|
||||
|
||||
const handleReturnedServerError = (error: Error) => {
|
||||
if (error instanceof ActionError) {
|
||||
return error.message;
|
||||
} else {
|
||||
return "An unexpected error occurred.";
|
||||
}
|
||||
if (error instanceof ActionError) {
|
||||
return error.message;
|
||||
} else {
|
||||
return "An unexpected error occurred.";
|
||||
}
|
||||
};
|
||||
|
||||
export const action = createSafeActionClient({
|
||||
handleServerError: handleReturnedServerError,
|
||||
handleServerError: handleReturnedServerError,
|
||||
});
|
||||
|
||||
export const userAction = action.use(async ({next, ctx}) => {
|
||||
const user = await currentUser();
|
||||
if (!user) {
|
||||
throw new ActionError("You must be logged in");
|
||||
}
|
||||
return next({ctx: {user}});
|
||||
export const userAction = action.use(async ({ next }) => {
|
||||
const user = await currentUser();
|
||||
if (!user) {
|
||||
throw new ActionError("You must be logged in");
|
||||
}
|
||||
return next({ ctx: { user } });
|
||||
});
|
||||
|
||||
@@ -1,45 +1,51 @@
|
||||
import {db} from "@/db";
|
||||
import {eq, lt, and, desc, isNull} from "drizzle-orm";
|
||||
import { db } from "@/db";
|
||||
import { eq, lt, and, isNull } from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {deleteBackupCronAction} from "@/lib/tasks/database/utils/delete";
|
||||
import {logger} from "@/lib/logger";
|
||||
import { deleteBackupCronAction } from "@/lib/tasks/database/utils/delete";
|
||||
import { logger } from "@/lib/logger";
|
||||
|
||||
const log = logger.child({module: "tasks/database/retention-days"});
|
||||
const log = logger.child({ module: "tasks/database/retention-days" });
|
||||
|
||||
export async function enforceRetentionDays(databaseId: string, days: number) {
|
||||
log.info({ name: "enforceRetentionDays"}, `Enforce Retention Days starting for ${databaseId}`);
|
||||
log.info(
|
||||
{ name: "enforceRetentionDays" },
|
||||
`Enforce Retention Days starting for ${databaseId}`,
|
||||
);
|
||||
|
||||
const cutoff = new Date(Date.now() - days * 86400000);
|
||||
const cutoff = new Date(Date.now() - days * 86400000);
|
||||
|
||||
const expiredBackups = await db.query.backup.findMany({
|
||||
where: and(
|
||||
eq(drizzleDb.schemas.backup.databaseId, databaseId),
|
||||
lt(drizzleDb.schemas.backup.createdAt, cutoff),
|
||||
isNull(drizzleDb.schemas.backup.deletedAt)
|
||||
),
|
||||
const expiredBackups = await db.query.backup.findMany({
|
||||
where: and(
|
||||
eq(drizzleDb.schemas.backup.databaseId, databaseId),
|
||||
lt(drizzleDb.schemas.backup.createdAt, cutoff),
|
||||
isNull(drizzleDb.schemas.backup.deletedAt),
|
||||
),
|
||||
with: {
|
||||
database: {
|
||||
with: {
|
||||
database: {
|
||||
with: {
|
||||
project: true
|
||||
}
|
||||
}
|
||||
}
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
for (const backup of expiredBackups) {
|
||||
const result = await deleteBackupCronAction({
|
||||
backupId: backup.id,
|
||||
databaseId: backup.databaseId,
|
||||
});
|
||||
|
||||
for (const backup of expiredBackups) {
|
||||
|
||||
const result = await deleteBackupCronAction({
|
||||
backupId: backup.id,
|
||||
databaseId: backup.databaseId,
|
||||
});
|
||||
|
||||
const inner = result?.data;
|
||||
if (inner?.success) {
|
||||
log.info({ name: "enforceRetentionDays"}, `(databaseId:${backup.databaseId}) - (backupId: ${backup.id}) : successfully deleted`);
|
||||
} else {
|
||||
log.info({ name: "enforceRetentionDays"}, `(databaseId:${backup.databaseId}) - (backupId: ${backup.id}) : an error occurred - ${inner?.actionError?.message}`);
|
||||
}
|
||||
const inner = result?.data;
|
||||
if (inner?.success) {
|
||||
log.info(
|
||||
{ name: "enforceRetentionDays" },
|
||||
`(databaseId:${backup.databaseId}) - (backupId: ${backup.id}) : successfully deleted`,
|
||||
);
|
||||
} else {
|
||||
log.info(
|
||||
{ name: "enforceRetentionDays" },
|
||||
`(databaseId:${backup.databaseId}) - (backupId: ${backup.id}) : an error occurred - ${inner?.actionError?.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,81 +1,106 @@
|
||||
import {db} from "@/db";
|
||||
import {subDays, subWeeks, subMonths, subYears, startOfWeek, startOfMonth, startOfYear} from "date-fns";
|
||||
import {eq, desc, isNull, and} from "drizzle-orm";
|
||||
import { db } from "@/db";
|
||||
import {
|
||||
subDays,
|
||||
subWeeks,
|
||||
subMonths,
|
||||
subYears,
|
||||
startOfWeek,
|
||||
startOfMonth,
|
||||
startOfYear,
|
||||
} from "date-fns";
|
||||
import { eq, desc, isNull, and } from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {deleteBackupCronAction} from "@/lib/tasks/database/utils/delete";
|
||||
import {logger} from "@/lib/logger";
|
||||
import { deleteBackupCronAction } from "@/lib/tasks/database/utils/delete";
|
||||
import { logger } from "@/lib/logger";
|
||||
|
||||
const log = logger.child({module: "tasks/database/retention-gsf"});
|
||||
const log = logger.child({ module: "tasks/database/retention-gsf" });
|
||||
|
||||
export async function enforceRetentionGFS(databaseId: string, gfsSettings: {
|
||||
export async function enforceRetentionGFS(
|
||||
databaseId: string,
|
||||
gfsSettings: {
|
||||
daily: number;
|
||||
weekly: number;
|
||||
monthly: number;
|
||||
yearly: number;
|
||||
}) {
|
||||
log.info({ name: "enforceRetentionGFS"}, `Retention GFS started for databaseId: ${databaseId}`);
|
||||
},
|
||||
) {
|
||||
log.info(
|
||||
{ name: "enforceRetentionGFS" },
|
||||
`Retention GFS started for databaseId: ${databaseId}`,
|
||||
);
|
||||
|
||||
const backups = await db.query.backup.findMany({
|
||||
where: and(eq(drizzleDb.schemas.backup.databaseId, databaseId), isNull(drizzleDb.schemas.backup.deletedAt)),
|
||||
orderBy: desc(drizzleDb.schemas.backup.createdAt),
|
||||
const backups = await db.query.backup.findMany({
|
||||
where: and(
|
||||
eq(drizzleDb.schemas.backup.databaseId, databaseId),
|
||||
isNull(drizzleDb.schemas.backup.deletedAt),
|
||||
),
|
||||
orderBy: desc(drizzleDb.schemas.backup.createdAt),
|
||||
with: {
|
||||
database: {
|
||||
with: {
|
||||
database: {
|
||||
with: {
|
||||
project: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const now = new Date();
|
||||
const toKeep: Set<string> = new Set();
|
||||
const now = new Date();
|
||||
const toKeep: Set<string> = new Set();
|
||||
|
||||
// DAILY
|
||||
backups.forEach((b) => {
|
||||
if (b.createdAt >= subDays(now, gfsSettings.daily)) toKeep.add(b.id);
|
||||
});
|
||||
backups.forEach((b) => {
|
||||
if (b.createdAt >= subDays(now, gfsSettings.daily)) toKeep.add(b.id);
|
||||
});
|
||||
|
||||
const weekStartDates = Array.from({length: gfsSettings.weekly}, (_, i) => startOfWeek(subWeeks(now, i), {weekStartsOn: 1}));
|
||||
weekStartDates.forEach((weekStart) => {
|
||||
const backupOfWeek = backups.find(
|
||||
(b) => b.createdAt >= weekStart && b.createdAt < subWeeks(weekStart, -1)
|
||||
const weekStartDates = Array.from({ length: gfsSettings.weekly }, (_, i) =>
|
||||
startOfWeek(subWeeks(now, i), { weekStartsOn: 1 }),
|
||||
);
|
||||
weekStartDates.forEach((weekStart) => {
|
||||
const backupOfWeek = backups.find(
|
||||
(b) => b.createdAt >= weekStart && b.createdAt < subWeeks(weekStart, -1),
|
||||
);
|
||||
if (backupOfWeek) toKeep.add(backupOfWeek.id);
|
||||
});
|
||||
|
||||
const monthStartDates = Array.from({ length: gfsSettings.monthly }, (_, i) =>
|
||||
startOfMonth(subMonths(now, i)),
|
||||
);
|
||||
monthStartDates.forEach((monthStart) => {
|
||||
const backupOfMonth = backups.find(
|
||||
(b) =>
|
||||
b.createdAt >= monthStart && b.createdAt < subMonths(monthStart, -1),
|
||||
);
|
||||
if (backupOfMonth) toKeep.add(backupOfMonth.id);
|
||||
});
|
||||
|
||||
const yearStartDates = Array.from({ length: gfsSettings.yearly }, (_, i) =>
|
||||
startOfYear(subYears(now, i)),
|
||||
);
|
||||
yearStartDates.forEach((yearStart) => {
|
||||
const backupOfYear = backups.find(
|
||||
(b) => b.createdAt >= yearStart && b.createdAt < subYears(yearStart, -1),
|
||||
);
|
||||
if (backupOfYear) toKeep.add(backupOfYear.id);
|
||||
});
|
||||
|
||||
for (const b of backups) {
|
||||
if (!toKeep.has(b.id)) {
|
||||
const result = await deleteBackupCronAction({
|
||||
backupId: b.id,
|
||||
databaseId: b.databaseId,
|
||||
});
|
||||
|
||||
const inner = result?.data;
|
||||
if (inner?.success) {
|
||||
log.info(
|
||||
{ name: "enforceRetentionGFS" },
|
||||
`(databaseId:${b.databaseId}) - (backupId: ${b.id}) : successfully deleted`,
|
||||
);
|
||||
if (backupOfWeek) toKeep.add(backupOfWeek.id);
|
||||
});
|
||||
|
||||
// MONTHLY
|
||||
const monthStartDates = Array.from({length: gfsSettings.monthly}, (_, i) => startOfMonth(subMonths(now, i)));
|
||||
monthStartDates.forEach((monthStart) => {
|
||||
const backupOfMonth = backups.find(
|
||||
(b) => b.createdAt >= monthStart && b.createdAt < subMonths(monthStart, -1)
|
||||
} else {
|
||||
log.info(
|
||||
{ name: "enforceRetentionGFS" },
|
||||
`(databaseId:${b.databaseId}) - (backupId: ${b.id}) : an error occurred - ${inner?.actionError?.message}`,
|
||||
);
|
||||
if (backupOfMonth) toKeep.add(backupOfMonth.id);
|
||||
});
|
||||
|
||||
// YEARLY
|
||||
const yearStartDates = Array.from({length: gfsSettings.yearly}, (_, i) => startOfYear(subYears(now, i)));
|
||||
yearStartDates.forEach((yearStart) => {
|
||||
const backupOfYear = backups.find(
|
||||
(b) => b.createdAt >= yearStart && b.createdAt < subYears(yearStart, -1)
|
||||
);
|
||||
if (backupOfYear) toKeep.add(backupOfYear.id);
|
||||
});
|
||||
|
||||
// Delete backups not in `toKeep`
|
||||
for (const b of backups) {
|
||||
if (!toKeep.has(b.id)) {
|
||||
|
||||
const result = await deleteBackupCronAction({
|
||||
backupId: b.id,
|
||||
databaseId: b.databaseId,
|
||||
});
|
||||
|
||||
const inner = result?.data;
|
||||
if (inner?.success) {
|
||||
log.info({ name: "enforceRetentionGFS"}, `(databaseId:${b.databaseId}) - (backupId: ${b.id}) : successfully deleted`);
|
||||
} else {
|
||||
log.info({ name: "enforceRetentionGFS"}, `(databaseId:${b.databaseId}) - (backupId: ${b.id}) : an error occurred - ${inner?.actionError?.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"use server"
|
||||
import {getMasterServerKeyContent} from "@/features/agents/keys.action";
|
||||
import {getMasterServerKeyContent} from "@/features/agents/actions/keys.action";
|
||||
|
||||
export async function generateEdgeKey(serverUrl: string, agentId: string): Promise<string> {
|
||||
const masterKey = await getMasterServerKeyContent()
|
||||
|
||||
Reference in New Issue
Block a user