diff --git a/apps/api/docker-compose.yml b/apps/api/docker-compose.yml
new file mode 100644
index 000000000..6e5df8871
--- /dev/null
+++ b/apps/api/docker-compose.yml
@@ -0,0 +1,21 @@
+version: "2"
+services:
+ zookeeper:
+ image: "confluentinc/cp-zookeeper:latest"
+ environment:
+ ZOOKEEPER_CLIENT_PORT: 2181
+ ZOOKEEPER_TICK_TIME: 2000
+ ports:
+ - "2181:2181"
+
+ kafka:
+ image: "confluentinc/cp-kafka:latest"
+ depends_on:
+ - zookeeper
+ environment:
+ KAFKA_BROKER_ID: 1
+ KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
+ KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
+ KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
+ ports:
+ - "9092:9092"
diff --git a/apps/api/package.json b/apps/api/package.json
index 5450ceab9..fc27422f7 100644
--- a/apps/api/package.json
+++ b/apps/api/package.json
@@ -1,14 +1,29 @@
{
"name": "my-app",
"scripts": {
- "dev": "tsx watch src/index.ts"
+ "dev": "tsx watch src/index.ts",
+ "tsc": "tsc --project tsconfig.json"
},
"dependencies": {
+ "react": "18.2.0",
+ "react-dom": "18.2.0",
+ "@dokploy/builders": "workspace:*",
"@hono/node-server": "^1.12.1",
"hono": "^4.5.8",
- "dotenv": "^16.3.1"
+ "dotenv": "^16.3.1",
+ "@upstash/qstash": "2.7.9",
+ "ioredis": "5.4.1",
+ "nats": "2.28.2",
+ "bullmq": "5.13.2",
+ "@nerimity/mimiqueue": "1.2.3",
+ "timers": "0.1.1",
+ "redis": "4.7.0",
+ "date-fns": "4.1.0"
},
"devDependencies": {
+ "typescript": "^5.4.2",
+ "@types/react": "^18.2.37",
+ "@types/react-dom": "^18.2.15",
"@types/node": "^20.11.17",
"tsx": "^4.7.1"
}
diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts
index 866c1f3f6..816141b3b 100644
--- a/apps/api/src/index.ts
+++ b/apps/api/src/index.ts
@@ -1,66 +1,57 @@
import { serve } from "@hono/node-server";
-import { config } from "dotenv";
import { Hono } from "hono";
-import { cors } from "hono/cors";
-import { validateLemonSqueezyLicense } from "./utils";
-
-config();
+import "dotenv/config";
+import { createClient } from "redis";
+import { Queue } from "@nerimity/mimiqueue";
+import { deployApplication } from "@dokploy/builders";
+// import { setTimeout } from "timers/promises";
const app = new Hono();
-
-app.use(
- "/*",
- cors({
- origin: ["http://localhost:3000", "http://localhost:3001"], // Ajusta esto a los orígenes de tu aplicación Next.js
- allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
- allowHeaders: ["Content-Type", "Authorization"],
- exposeHeaders: ["Content-Length", "X-Kuma-Revision"],
- maxAge: 600,
- credentials: true,
- }),
-);
-
-export const LEMON_SQUEEZY_API_KEY = process.env.LEMON_SQUEEZY_API_KEY;
-export const LEMON_SQUEEZY_STORE_ID = process.env.LEMON_SQUEEZY_STORE_ID;
-
-app.get("/v1/health", (c) => {
- return c.text("Hello Hono!");
+const redisClient = createClient({
+ socket: {
+ host: "localhost",
+ port: 6379,
+ },
+ // password: "xlfvpQ0ma2BkkkPX",
});
-app.post("/v1/validate-license", async (c) => {
- const { licenseKey } = await c.req.json();
-
- if (!licenseKey) {
- return c.json({ error: "License key is required" }, 400);
- }
-
- try {
- const licenseValidation = await validateLemonSqueezyLicense(licenseKey);
-
- if (licenseValidation.valid) {
- return c.json({
- valid: true,
- message: "License is valid",
- metadata: licenseValidation.meta,
- });
- }
- return c.json(
+app.post("/publish", async (c) => {
+ const { userId, applicationId } = await c.req.json();
+ queue
+ .add(
{
- valid: false,
- message: licenseValidation.error || "Invalid license",
+ userId,
+ applicationId,
},
- 400,
- );
- } catch (error) {
- console.error("Error during license validation:", error);
- return c.json({ error: "Internal server error" }, 500);
- }
-});
+ { groupName: userId },
+ )
+ .then((res) => {
+ console.log(res);
+ });
+ return c.json({ message: `Despliegue encolado para el usuario ${userId}` });
+});
+// await redisClient.connect();
+// await redisClient.flushAll();
+
+const queue = new Queue({
+ name: "deployments",
+ process: async (data) => {
+ // await setTimeout(8000);
+ await deployApplication({
+ applicationId: data.applicationId,
+ titleLog: "HHHHH",
+ descriptionLog: "",
+ });
+ return { done: "lol", data };
+ },
+ redisClient,
+});
const port = 4000;
-console.log(`Server is running on port ${port}`);
+(async () => {
+ await redisClient.connect();
+ await redisClient.flushAll();
+})();
-serve({
- fetch: app.fetch,
- port,
-});
+console.log("Starting Server ✅");
+serve({ fetch: app.fetch, port });
diff --git a/apps/api/src/test.ts b/apps/api/src/test.ts
new file mode 100644
index 000000000..9796eb60b
--- /dev/null
+++ b/apps/api/src/test.ts
@@ -0,0 +1,82 @@
+import { Hono } from "hono";
+import { Client } from "@upstash/qstash";
+import { serve } from "@hono/node-server";
+import dotenv from "dotenv";
+import Redis from "ioredis";
+
+dotenv.config();
+
+const redis = new Redis({
+ host: "localhost",
+ port: 7777,
+ password: "xlfvpQ0ma2BkkkPX",
+});
+
+// redis.set("test", "test");
+// console.log(await redis.get("test"));
+
+// console.log(await redis.get("user-1-processing"));
+const app = new Hono();
+console.log("QStash Token:", process.env.PUBLIC_URL);
+
+const qstash = new Client({
+ token: process.env.QSTASH_TOKEN as string,
+});
+
+const queue = qstash.queue({
+ queueName: "deployments",
+});
+
+// Endpoint que publica un mensaje en QStash
+app.post("/enqueue", async (c) => {
+ const { userId, deploymentId } = await c.req.json();
+ const response = await qstash.publishJSON({
+ url: `${process.env.PUBLIC_URL}/process`, // Endpoint para procesar la tarea
+ body: { userId, deploymentId }, // Datos del despliegue
+
+ });
+
+ return c.json({ message: "Task enqueued", id: response.messageId });
+});
+
+// Endpoint que recibe el mensaje procesado
+app.post("/process", async (c) => {
+ const { userId, deploymentId } = await c.req.json();
+
+ const isProcessing = await redis.get(`user-${userId}-processing`);
+ console.log(`isProcessing for user ${userId}:`, isProcessing);
+
+ if (isProcessing === "true") {
+ console.log(
+ `User ${userId} is already processing a deployment. Queuing the next one.`,
+ );
+ return c.json(
+ {
+ status: "User is already processing a deployment, waiting...",
+ },
+ {
+ status: 400,
+ },
+ );
+ }
+ redis.set(`user-${userId}-processing`, "true");
+
+ try {
+ await new Promise((resolve) => setTimeout(resolve, 5000));
+ } catch (error) {
+ } finally {
+ await redis.del(`user-${userId}-processing`);
+ }
+
+ return c.json({ status: "Processed", userId, deploymentId });
+});
+
+// Inicia el servidor en el puerto 3000
+const port = 3000;
+console.log(`Server is running on port http://localhost:${port}`);
+
+serve({
+ fetch: app.fetch,
+ port,
+});
+// 18
diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json
index 68a9e8f04..55e7a5e15 100644
--- a/apps/api/tsconfig.json
+++ b/apps/api/tsconfig.json
@@ -1,12 +1,14 @@
{
- "compilerOptions": {
- "target": "ESNext",
- "module": "ESNext",
- "moduleResolution": "Bundler",
- "strict": true,
- "skipLibCheck": true,
- "types": ["node"],
- "jsx": "react-jsx",
- "jsxImportSource": "hono/jsx"
- }
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "moduleResolution": "Node",
+ "strict": true,
+ "skipLibCheck": true,
+ "types": ["node"],
+ "jsx": "react-jsx",
+ "jsxImportSource": "hono/jsx",
+ "traceResolution": true,
+ "diagnostics": true
+ }
}
diff --git a/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx b/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx
index bf1a298be..8bfcf4da2 100644
--- a/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx
+++ b/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx
@@ -19,7 +19,6 @@ import {
DialogTrigger,
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
-import { Separator } from "@/components/ui/separator";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { api } from "@/utils/api";
import copy from "copy-to-clipboard";
diff --git a/apps/dokploy/next.config.mjs b/apps/dokploy/next.config.mjs
index 46b4268c3..134cf19c2 100644
--- a/apps/dokploy/next.config.mjs
+++ b/apps/dokploy/next.config.mjs
@@ -11,45 +11,46 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
/** @type {import("next").NextConfig} */
const nextConfig = {
- reactStrictMode: true,
- eslint: {
- ignoreDuringBuilds: true,
- },
- typescript: {
- ignoreBuildErrors: true,
- },
- webpack: (config) => {
- config.plugins.push(
- new CopyWebpackPlugin({
- patterns: [
- {
- from: path.resolve(__dirname, "templates/**/*.yml"),
- to: ({ context, absoluteFilename }) => {
- const relativePath = path.relative(
- path.resolve(__dirname, "templates"),
- absoluteFilename || context,
- );
- return path.join(__dirname, ".next", "templates", relativePath);
- },
- globOptions: {
- ignore: ["**/node_modules/**"],
- },
- },
- ],
- }),
- );
- return config;
- },
+ reactStrictMode: true,
+ eslint: {
+ ignoreDuringBuilds: true,
+ },
+ typescript: {
+ ignoreBuildErrors: true,
+ },
+ transpilePackages: ["@dokploy/builders"],
+ webpack: (config) => {
+ config.plugins.push(
+ new CopyWebpackPlugin({
+ patterns: [
+ {
+ from: path.resolve(__dirname, "templates/**/*.yml"),
+ to: ({ context, absoluteFilename }) => {
+ const relativePath = path.relative(
+ path.resolve(__dirname, "templates"),
+ absoluteFilename || context
+ );
+ return path.join(__dirname, ".next", "templates", relativePath);
+ },
+ globOptions: {
+ ignore: ["**/node_modules/**"],
+ },
+ },
+ ],
+ })
+ );
+ return config;
+ },
- /**
- * If you are using `appDir` then you must comment the below `i18n` config out.
- *
- * @see https://github.com/vercel/next.js/issues/41980
- */
- i18n: {
- locales: ["en"],
- defaultLocale: "en",
- },
+ /**
+ * If you are using `appDir` then you must comment the below `i18n` config out.
+ *
+ * @see https://github.com/vercel/next.js/issues/41980
+ */
+ i18n: {
+ locales: ["en"],
+ defaultLocale: "en",
+ },
};
export default nextConfig;
diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json
index 023218997..9f758cc10 100644
--- a/apps/dokploy/package.json
+++ b/apps/dokploy/package.json
@@ -34,6 +34,7 @@
"test": "vitest --config __test__/vitest.config.ts"
},
"dependencies": {
+ "@dokploy/builders": "workspace:*",
"rotating-file-stream": "3.2.3",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-yaml": "^6.1.1",
diff --git a/apps/dokploy/pages/api/[...trpc].ts b/apps/dokploy/pages/api/[...trpc].ts
index 4063d29f7..89c6616cd 100644
--- a/apps/dokploy/pages/api/[...trpc].ts
+++ b/apps/dokploy/pages/api/[...trpc].ts
@@ -18,6 +18,8 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
res.status(401).json({ message: "Unauthorized" });
return;
}
+
+ console.log(user);
// @ts-ignore
return createOpenApiNextHandler({
router: appRouter,
diff --git a/apps/dokploy/server/api/routers/admin.ts b/apps/dokploy/server/api/routers/admin.ts
index 2157073e6..acacc7769 100644
--- a/apps/dokploy/server/api/routers/admin.ts
+++ b/apps/dokploy/server/api/routers/admin.ts
@@ -6,14 +6,23 @@ import {
apiRemoveUser,
users,
} from "@/server/db/schema";
+
+// import {
+
+// } from "@dokploy/builders";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
+// import {
+// createInvitation,
+// getUserByToken,
+// removeUserByAuthId,
+// } from "../services/admin";
import {
- createInvitation,
findAdmin,
+ createInvitation,
getUserByToken,
removeUserByAuthId,
-} from "../services/admin";
+} from "@dokploy/builders";
import { adminProcedure, createTRPCRouter, publicProcedure } from "../trpc";
export const adminRouter = createTRPCRouter({
diff --git a/apps/dokploy/server/api/routers/application.ts b/apps/dokploy/server/api/routers/application.ts
index 4c43fe7ce..09e45da17 100644
--- a/apps/dokploy/server/api/routers/application.ts
+++ b/apps/dokploy/server/api/routers/application.ts
@@ -18,7 +18,7 @@ import {
apiSaveGitlabProvider,
apiUpdateApplication,
applications,
-} from "@/server/db/schema/application";
+} from "@/server/db/schema";
import {
type DeploymentJob,
cleanQueuesByApplication,
@@ -55,7 +55,7 @@ import {
getApplicationStats,
updateApplication,
updateApplicationStatus,
-} from "../services/application";
+} from "@dokploy/builders";
import { removeDeployments } from "../services/deployment";
import { addNewService, checkServiceAccess } from "../services/user";
diff --git a/apps/dokploy/server/api/routers/mariadb.ts b/apps/dokploy/server/api/routers/mariadb.ts
index e8893782b..d73215b2f 100644
--- a/apps/dokploy/server/api/routers/mariadb.ts
+++ b/apps/dokploy/server/api/routers/mariadb.ts
@@ -8,7 +8,7 @@ import {
apiSaveEnvironmentVariablesMariaDB,
apiSaveExternalPortMariaDB,
apiUpdateMariaDB,
-} from "@/server/db/schema/mariadb";
+} from "@/server/db/schema";
import {
removeService,
startService,
diff --git a/apps/dokploy/server/api/routers/mongo.ts b/apps/dokploy/server/api/routers/mongo.ts
index f51d0eb9e..89f3bc355 100644
--- a/apps/dokploy/server/api/routers/mongo.ts
+++ b/apps/dokploy/server/api/routers/mongo.ts
@@ -8,7 +8,7 @@ import {
apiSaveEnvironmentVariablesMongo,
apiSaveExternalPortMongo,
apiUpdateMongo,
-} from "@/server/db/schema/mongo";
+} from "@/server/db/schema";
import {
removeService,
startService,
diff --git a/apps/dokploy/server/api/routers/mysql.ts b/apps/dokploy/server/api/routers/mysql.ts
index 71f6d514b..0d4aaa6c6 100644
--- a/apps/dokploy/server/api/routers/mysql.ts
+++ b/apps/dokploy/server/api/routers/mysql.ts
@@ -8,7 +8,7 @@ import {
apiSaveEnvironmentVariablesMySql,
apiSaveExternalPortMySql,
apiUpdateMySql,
-} from "@/server/db/schema/mysql";
+} from "@/server/db/schema";
import {
removeService,
startService,
diff --git a/apps/dokploy/server/api/routers/port.ts b/apps/dokploy/server/api/routers/port.ts
index 36aba5de0..252977284 100644
--- a/apps/dokploy/server/api/routers/port.ts
+++ b/apps/dokploy/server/api/routers/port.ts
@@ -3,7 +3,7 @@ import {
apiCreatePort,
apiFindOnePort,
apiUpdatePort,
-} from "@/server/db/schema/port";
+} from "@/server/db/schema";
import { TRPCError } from "@trpc/server";
import {
createPort,
diff --git a/apps/dokploy/server/api/routers/postgres.ts b/apps/dokploy/server/api/routers/postgres.ts
index b902ee517..1d2779efe 100644
--- a/apps/dokploy/server/api/routers/postgres.ts
+++ b/apps/dokploy/server/api/routers/postgres.ts
@@ -8,7 +8,7 @@ import {
apiSaveEnvironmentVariablesPostgres,
apiSaveExternalPortPostgres,
apiUpdatePostgres,
-} from "@/server/db/schema/postgres";
+} from "@/server/db/schema";
import {
removeService,
startService,
diff --git a/apps/dokploy/server/api/routers/project.ts b/apps/dokploy/server/api/routers/project.ts
index 4ad9be75f..62c250ba7 100644
--- a/apps/dokploy/server/api/routers/project.ts
+++ b/apps/dokploy/server/api/routers/project.ts
@@ -15,7 +15,7 @@ import {
apiRemoveProject,
apiUpdateProject,
projects,
-} from "@/server/db/schema/project";
+} from "@/server/db/schema";
import { TRPCError } from "@trpc/server";
import { desc, eq, sql } from "drizzle-orm";
import type { AnyPgColumn } from "drizzle-orm/pg-core";
diff --git a/apps/dokploy/server/api/routers/redis.ts b/apps/dokploy/server/api/routers/redis.ts
index 19bfa7c20..2f904384d 100644
--- a/apps/dokploy/server/api/routers/redis.ts
+++ b/apps/dokploy/server/api/routers/redis.ts
@@ -8,7 +8,7 @@ import {
apiSaveEnvironmentVariablesRedis,
apiSaveExternalPortRedis,
apiUpdateRedis,
-} from "@/server/db/schema/redis";
+} from "@/server/db/schema";
import {
removeService,
startService,
diff --git a/apps/dokploy/server/api/services/application.ts b/apps/dokploy/server/api/services/application.ts
index 7f0403586..c56f39015 100644
--- a/apps/dokploy/server/api/services/application.ts
+++ b/apps/dokploy/server/api/services/application.ts
@@ -1,7 +1,7 @@
import { docker } from "@/server/constants";
import { db } from "@/server/db";
import { type apiCreateApplication, applications } from "@/server/db/schema";
-import { generateAppName } from "@/server/db/schema/utils";
+import { generateAppName } from "@/server/db/schema";
import { getAdvancedStats } from "@/server/monitoring/utilts";
import {
buildApplication,
diff --git a/apps/dokploy/server/api/services/compose.ts b/apps/dokploy/server/api/services/compose.ts
index 13c728f5f..4df97bf20 100644
--- a/apps/dokploy/server/api/services/compose.ts
+++ b/apps/dokploy/server/api/services/compose.ts
@@ -2,7 +2,7 @@ import { join } from "node:path";
import { paths } from "@/server/constants";
import { db } from "@/server/db";
import { type apiCreateCompose, compose } from "@/server/db/schema";
-import { generateAppName } from "@/server/db/schema/utils";
+import { generateAppName } from "@/server/db/schema";
import {
buildCompose,
getBuildComposeCommand,
diff --git a/apps/dokploy/server/api/services/mariadb.ts b/apps/dokploy/server/api/services/mariadb.ts
index 2fdb3a7fe..13b521d56 100644
--- a/apps/dokploy/server/api/services/mariadb.ts
+++ b/apps/dokploy/server/api/services/mariadb.ts
@@ -1,7 +1,7 @@
import { generateRandomPassword } from "@/server/auth/random-password";
import { db } from "@/server/db";
import { type apiCreateMariaDB, backups, mariadb } from "@/server/db/schema";
-import { generateAppName } from "@/server/db/schema/utils";
+import { generateAppName } from "@/server/db/schema";
import { buildMariadb } from "@/server/utils/databases/mariadb";
import { pullImage } from "@/server/utils/docker/utils";
import { generatePassword } from "@/templates/utils";
diff --git a/apps/dokploy/server/api/services/mongo.ts b/apps/dokploy/server/api/services/mongo.ts
index 9940768a7..30ed3f643 100644
--- a/apps/dokploy/server/api/services/mongo.ts
+++ b/apps/dokploy/server/api/services/mongo.ts
@@ -1,7 +1,7 @@
import { generateRandomPassword } from "@/server/auth/random-password";
import { db } from "@/server/db";
import { type apiCreateMongo, backups, mongo } from "@/server/db/schema";
-import { generateAppName } from "@/server/db/schema/utils";
+import { generateAppName } from "@/server/db/schema";
import { buildMongo } from "@/server/utils/databases/mongo";
import { pullImage } from "@/server/utils/docker/utils";
import { generatePassword } from "@/templates/utils";
diff --git a/apps/dokploy/server/api/services/mysql.ts b/apps/dokploy/server/api/services/mysql.ts
index b4dacf81f..3a5c2884d 100644
--- a/apps/dokploy/server/api/services/mysql.ts
+++ b/apps/dokploy/server/api/services/mysql.ts
@@ -1,7 +1,7 @@
import { generateRandomPassword } from "@/server/auth/random-password";
import { db } from "@/server/db";
import { type apiCreateMySql, backups, mysql } from "@/server/db/schema";
-import { generateAppName } from "@/server/db/schema/utils";
+import { generateAppName } from "@/server/db/schema";
import { buildMysql } from "@/server/utils/databases/mysql";
import { pullImage } from "@/server/utils/docker/utils";
import { generatePassword } from "@/templates/utils";
diff --git a/apps/dokploy/server/api/services/postgres.ts b/apps/dokploy/server/api/services/postgres.ts
index 56c91149c..03adaf9b1 100644
--- a/apps/dokploy/server/api/services/postgres.ts
+++ b/apps/dokploy/server/api/services/postgres.ts
@@ -1,7 +1,7 @@
import { generateRandomPassword } from "@/server/auth/random-password";
import { db } from "@/server/db";
import { type apiCreatePostgres, backups, postgres } from "@/server/db/schema";
-import { generateAppName } from "@/server/db/schema/utils";
+import { generateAppName } from "@/server/db/schema";
import { buildPostgres } from "@/server/utils/databases/postgres";
import { pullImage } from "@/server/utils/docker/utils";
import { generatePassword } from "@/templates/utils";
diff --git a/apps/dokploy/server/api/services/redis.ts b/apps/dokploy/server/api/services/redis.ts
index a4d85857c..a06e979e5 100644
--- a/apps/dokploy/server/api/services/redis.ts
+++ b/apps/dokploy/server/api/services/redis.ts
@@ -1,7 +1,7 @@
import { generateRandomPassword } from "@/server/auth/random-password";
import { db } from "@/server/db";
import { type apiCreateRedis, redis } from "@/server/db/schema";
-import { generateAppName } from "@/server/db/schema/utils";
+import { generateAppName } from "@/server/db/schema";
import { buildRedis } from "@/server/utils/databases/redis";
import { pullImage } from "@/server/utils/docker/utils";
import { generatePassword } from "@/templates/utils";
diff --git a/apps/dokploy/server/api/services/server.ts b/apps/dokploy/server/api/services/server.ts
index 4cd79cdcc..9f4c32701 100644
--- a/apps/dokploy/server/api/services/server.ts
+++ b/apps/dokploy/server/api/services/server.ts
@@ -1,4 +1,5 @@
import { db } from "@/server/db";
+
import { type apiCreateServer, server } from "@/server/db/schema";
import { TRPCError } from "@trpc/server";
import { desc, eq } from "drizzle-orm";
diff --git a/apps/dokploy/server/db/index.ts b/apps/dokploy/server/db/index.ts
index 00f68e6c8..79f2e03d2 100644
--- a/apps/dokploy/server/db/index.ts
+++ b/apps/dokploy/server/db/index.ts
@@ -1,7 +1,13 @@
import { type PostgresJsDatabase, drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
+// import { sc } from "@dokploy/schema";
+// import * as schema from "@dokploy/schema";
+// schema
import * as schema from "./schema";
+// type Schema = typeof schema;
+
+// type Schema = typeof schema;
declare global {
// eslint-disable-next-line no-var -- only var works here
var db: PostgresJsDatabase | undefined;
diff --git a/apps/dokploy/server/db/schema/index.ts b/apps/dokploy/server/db/schema/index.ts
index 3b24faff1..b93be6649 100644
--- a/apps/dokploy/server/db/schema/index.ts
+++ b/apps/dokploy/server/db/schema/index.ts
@@ -1,30 +1 @@
-export * from "./application";
-export * from "./postgres";
-export * from "./user";
-export * from "./admin";
-export * from "./auth";
-export * from "./project";
-export * from "./domain";
-export * from "./mariadb";
-export * from "./mongo";
-export * from "./mysql";
-export * from "./backups";
-export * from "./destination";
-export * from "./deployment";
-export * from "./mount";
-export * from "./certificate";
-export * from "./session";
-export * from "./redirects";
-export * from "./security";
-export * from "./port";
-export * from "./redis";
-export * from "./shared";
-export * from "./compose";
-export * from "./registry";
-export * from "./notification";
-export * from "./ssh-key";
-export * from "./git-provider";
-export * from "./bitbucket";
-export * from "./github";
-export * from "./gitlab";
-export * from "./server";
+export * from "@dokploy/builders";
diff --git a/apps/dokploy/server/utils/access-log/handler.ts b/apps/dokploy/server/utils/access-log/handler.ts
index 24baa023b..d55882592 100644
--- a/apps/dokploy/server/utils/access-log/handler.ts
+++ b/apps/dokploy/server/utils/access-log/handler.ts
@@ -23,7 +23,6 @@ class LogRotationManager {
if (isActive) {
await this.activateStream();
}
- console.log(`Log rotation initialized. Active: ${isActive}`);
}
private async getStateFromDB(): Promise {
diff --git a/apps/dokploy/server/utils/servers/connection.ts b/apps/dokploy/server/utils/servers/connection.ts
deleted file mode 100644
index 846c24554..000000000
--- a/apps/dokploy/server/utils/servers/connection.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { findServerById } from "@/server/api/services/server";
-import { Client } from "ssh2";
-import { readSSHKey } from "../filesystem/ssh";
diff --git a/packages/builders/esbuild.config.ts b/packages/builders/esbuild.config.ts
new file mode 100644
index 000000000..878031046
--- /dev/null
+++ b/packages/builders/esbuild.config.ts
@@ -0,0 +1,25 @@
+import esbuild from "esbuild";
+
+try {
+ esbuild
+ .build({
+ entryPoints: ["./src/**/*.ts"],
+ bundle: true,
+ platform: "node",
+ format: "cjs",
+ target: "node18",
+ outExtension: { ".js": ".js" },
+ minify: true,
+ outdir: "dist",
+ tsconfig: "tsconfig.server.json",
+ packages: "external",
+ alias: {
+ "@/server": "./src",
+ },
+ })
+ .catch(() => {
+ return process.exit(1);
+ });
+} catch (error) {
+ console.log(error);
+}
diff --git a/packages/builders/package.json b/packages/builders/package.json
new file mode 100644
index 000000000..bb4ade65b
--- /dev/null
+++ b/packages/builders/package.json
@@ -0,0 +1,156 @@
+{
+ "name": "@dokploy/builders",
+ "version": "1.0.0",
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "type": "module",
+ "scripts": {
+ "dev": "tsup --config ./tsup.ts --watch",
+ "build": "tsc --project tsconfig.server.json && tsc-alias -p tsconfig.server.json",
+ "tsc": "tsc --project tsconfig.server.json",
+ "build:types": "tsc --emitDeclarationOnly --experimenta-dts"
+ },
+ "dependencies": {
+ "tsc-alias": "1.8.10",
+ "esbuild": "0.20.2",
+ "esbuild-plugin-alias-path": "2.0.2",
+ "esbuild-plugin-alias": "0.2.1",
+ "tiny-glob": "^0.2.9",
+ "rotating-file-stream": "3.2.3",
+ "@codemirror/lang-json": "^6.0.1",
+ "@codemirror/lang-yaml": "^6.1.1",
+ "@codemirror/language": "^6.10.1",
+ "@codemirror/legacy-modes": "6.4.0",
+ "@codemirror/view": "6.29.0",
+ "@dokploy/trpc-openapi": "0.0.4",
+ "@faker-js/faker": "^8.4.1",
+ "@hookform/resolvers": "^3.3.4",
+ "@lucia-auth/adapter-drizzle": "1.0.7",
+ "@octokit/auth-app": "^6.0.4",
+ "@octokit/webhooks": "^13.2.7",
+ "@radix-ui/react-accordion": "1.1.2",
+ "@radix-ui/react-alert-dialog": "^1.0.5",
+ "@radix-ui/react-avatar": "^1.0.4",
+ "@radix-ui/react-checkbox": "^1.0.4",
+ "@radix-ui/react-dialog": "^1.0.5",
+ "@radix-ui/react-dropdown-menu": "^2.0.6",
+ "@radix-ui/react-label": "^2.0.2",
+ "@radix-ui/react-popover": "^1.0.7",
+ "@radix-ui/react-progress": "^1.0.3",
+ "@radix-ui/react-radio-group": "^1.1.3",
+ "@radix-ui/react-scroll-area": "^1.0.5",
+ "@radix-ui/react-select": "^2.0.0",
+ "@radix-ui/react-separator": "^1.0.3",
+ "@radix-ui/react-slot": "^1.0.2",
+ "@radix-ui/react-switch": "^1.0.3",
+ "@radix-ui/react-tabs": "^1.0.4",
+ "@radix-ui/react-toggle": "^1.0.3",
+ "@radix-ui/react-tooltip": "^1.0.7",
+ "@react-email/components": "^0.0.21",
+ "@tanstack/react-query": "^4.36.1",
+ "@tanstack/react-table": "^8.16.0",
+ "@trpc/client": "^10.43.6",
+ "@trpc/next": "^10.43.6",
+ "@trpc/react-query": "^10.43.6",
+ "@trpc/server": "^10.43.6",
+ "@uiw/codemirror-theme-github": "^4.22.1",
+ "@uiw/react-codemirror": "^4.22.1",
+ "@xterm/addon-attach": "0.10.0",
+ "@xterm/xterm": "^5.4.0",
+ "adm-zip": "^0.5.14",
+ "bcrypt": "5.1.1",
+ "bl": "6.0.11",
+ "boxen": "^7.1.1",
+ "bullmq": "5.4.2",
+ "class-variance-authority": "^0.7.0",
+ "clsx": "^2.1.0",
+ "cmdk": "^0.2.0",
+ "copy-to-clipboard": "^3.3.3",
+ "copy-webpack-plugin": "^12.0.2",
+ "date-fns": "3.6.0",
+ "dockerode": "4.0.2",
+ "dockerode-compose": "^1.4.0",
+ "dockerstats": "2.4.2",
+ "dotenv": "16.4.5",
+ "drizzle-orm": "^0.30.8",
+ "drizzle-zod": "0.5.1",
+ "hi-base32": "^0.5.1",
+ "input-otp": "^1.2.4",
+ "js-yaml": "4.1.0",
+ "k6": "^0.0.0",
+ "lodash": "4.17.21",
+ "lucia": "^3.0.1",
+ "lucide-react": "^0.312.0",
+ "nanoid": "3",
+ "next": "^14.1.3",
+ "next-themes": "^0.2.1",
+ "node-os-utils": "1.3.7",
+ "node-pty": "1.0.0",
+ "node-schedule": "2.1.1",
+ "nodemailer": "6.9.14",
+ "octokit": "3.1.2",
+ "otpauth": "^9.2.3",
+ "postgres": "3.4.4",
+ "public-ip": "6.0.2",
+ "qrcode": "^1.5.3",
+ "react": "18.2.0",
+ "react-dom": "18.2.0",
+ "react-hook-form": "^7.49.3",
+ "recharts": "^2.12.7",
+ "slugify": "^1.6.6",
+ "sonner": "^1.4.0",
+ "superjson": "^2.2.1",
+ "swagger-ui-react": "^5.17.14",
+ "tailwind-merge": "^2.2.0",
+ "tailwindcss-animate": "^1.0.7",
+ "tar-fs": "3.0.5",
+ "undici": "^6.19.2",
+ "use-resize-observer": "9.1.0",
+ "ws": "8.16.0",
+ "xterm-addon-fit": "^0.8.0",
+ "zod": "^3.23.4",
+ "zod-form-data": "^2.0.2",
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-use-controllable-state": "1.1.0",
+ "ssh2": "1.15.0"
+ },
+ "devDependencies": {
+ "@biomejs/biome": "1.8.3",
+ "@commitlint/cli": "^19.3.0",
+ "@commitlint/config-conventional": "^19.2.2",
+ "@types/adm-zip": "^0.5.5",
+ "@types/bcrypt": "5.0.2",
+ "@types/dockerode": "3.3.23",
+ "@types/js-yaml": "4.0.9",
+ "@types/lodash": "4.17.4",
+ "@types/node": "^18.17.0",
+ "@types/node-os-utils": "1.3.4",
+ "@types/node-schedule": "2.1.6",
+ "@types/nodemailer": "^6.4.15",
+ "@types/qrcode": "^1.5.5",
+ "@types/react": "^18.2.37",
+ "@types/react-dom": "^18.2.15",
+ "@types/swagger-ui-react": "^4.18.3",
+ "@types/tar-fs": "2.0.4",
+ "@types/ws": "8.5.10",
+ "autoprefixer": "^10.4.14",
+ "drizzle-kit": "^0.21.1",
+ "esbuild": "0.20.2",
+ "husky": "^9.0.11",
+ "lint-staged": "^15.2.7",
+ "localtunnel": "2.0.2",
+ "memfs": "^4.11.0",
+ "postcss": "^8.4.31",
+ "prettier": "^3.2.4",
+ "prettier-plugin-tailwindcss": "^0.5.11",
+ "tailwindcss": "^3.4.1",
+ "tsconfig-paths": "4.2.0",
+ "tsx": "^4.7.0",
+ "typescript": "^5.4.2",
+ "vite-tsconfig-paths": "4.3.2",
+ "vitest": "^1.6.0",
+ "xterm-readline": "1.1.1",
+ "@types/ssh2": "1.15.1",
+ "tsup": "6.4.0"
+ }
+}
diff --git a/packages/builders/src/auth/auth.ts b/packages/builders/src/auth/auth.ts
new file mode 100644
index 000000000..a70989de5
--- /dev/null
+++ b/packages/builders/src/auth/auth.ts
@@ -0,0 +1,113 @@
+import { webcrypto } from "node:crypto";
+import type { IncomingMessage, ServerResponse } from "node:http";
+import { DrizzlePostgreSQLAdapter } from "@lucia-auth/adapter-drizzle";
+import { TimeSpan } from "lucia";
+import { Lucia } from "lucia/dist/core.js";
+import type { Session, User } from "lucia/dist/core.js";
+import { findAdminByAuthId } from "@/server/services/admin";
+import { findUserByAuthId } from "@/server/services/user";
+import { db } from "../db";
+import { type DatabaseUser, auth, sessionTable } from "../db/schema";
+
+globalThis.crypto = webcrypto as Crypto;
+export const adapter = new DrizzlePostgreSQLAdapter(db, sessionTable, auth);
+
+export const lucia = new Lucia(adapter, {
+ sessionCookie: {
+ attributes: {
+ secure: false,
+ },
+ },
+ sessionExpiresIn: new TimeSpan(1, "d"),
+ getUserAttributes: (attributes) => {
+ return {
+ email: attributes.email,
+ rol: attributes.rol,
+ secret: attributes.secret !== null,
+ adminId: attributes.adminId,
+ };
+ },
+});
+
+declare module "lucia" {
+ interface Register {
+ Lucia: typeof lucia;
+ DatabaseUserAttributes: Omit & {
+ authId: string;
+ adminId: string;
+ };
+ }
+}
+
+export type ReturnValidateToken = Promise<{
+ user: (User & { authId: string; adminId: string }) | null;
+ session: Session | null;
+}>;
+
+export async function validateRequest(
+ req: IncomingMessage,
+ res: ServerResponse,
+): ReturnValidateToken {
+ const sessionId = lucia.readSessionCookie(req.headers.cookie ?? "");
+
+ if (!sessionId) {
+ return {
+ user: null,
+ session: null,
+ };
+ }
+ const result = await lucia.validateSession(sessionId);
+ if (result?.session?.fresh) {
+ res.appendHeader(
+ "Set-Cookie",
+ lucia.createSessionCookie(result.session.id).serialize(),
+ );
+ }
+ if (!result.session) {
+ res.appendHeader(
+ "Set-Cookie",
+ lucia.createBlankSessionCookie().serialize(),
+ );
+ }
+
+ if (result.user) {
+ if (result.user?.rol === "admin") {
+ const admin = await findAdminByAuthId(result.user.id);
+ result.user.adminId = admin.adminId;
+ } else if (result.user?.rol === "user") {
+ const userResult = await findUserByAuthId(result.user.id);
+ result.user.adminId = userResult.adminId;
+ }
+ }
+
+ return {
+ session: result.session,
+ ...((result.user && {
+ user: {
+ authId: result.user.id,
+ email: result.user.email,
+ rol: result.user.rol,
+ id: result.user.id,
+ secret: result.user.secret,
+ adminId: result.user.adminId,
+ },
+ }) || {
+ user: null,
+ }),
+ };
+}
+
+export async function validateWebSocketRequest(
+ req: IncomingMessage,
+): Promise<{ user: User; session: Session } | { user: null; session: null }> {
+ const sessionId = lucia.readSessionCookie(req.headers.cookie ?? "");
+
+ if (!sessionId) {
+ return {
+ user: null,
+ session: null,
+ };
+ }
+ const result = await lucia.validateSession(sessionId);
+ return result;
+}
diff --git a/packages/builders/src/auth/random-password.ts b/packages/builders/src/auth/random-password.ts
new file mode 100644
index 000000000..150104b94
--- /dev/null
+++ b/packages/builders/src/auth/random-password.ts
@@ -0,0 +1,20 @@
+import bcrypt from "bcrypt";
+
+export const generateRandomPassword = async () => {
+ const passwordLength = 16;
+
+ const characters =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+
+ let randomPassword = "";
+ for (let i = 0; i < passwordLength; i++) {
+ randomPassword += characters.charAt(
+ Math.floor(Math.random() * characters.length),
+ );
+ }
+
+ const saltRounds = 10;
+
+ const hashedPassword = await bcrypt.hash(randomPassword, saltRounds);
+ return { randomPassword, hashedPassword };
+};
diff --git a/packages/builders/src/auth/token.ts b/packages/builders/src/auth/token.ts
new file mode 100644
index 000000000..54162fdcb
--- /dev/null
+++ b/packages/builders/src/auth/token.ts
@@ -0,0 +1,49 @@
+import type { IncomingMessage } from "node:http";
+import { TimeSpan } from "lucia";
+import { Lucia } from "lucia/dist/core.js";
+import { type ReturnValidateToken, adapter } from "./auth";
+
+export const luciaToken = new Lucia(adapter, {
+ sessionCookie: {
+ attributes: {
+ secure: false,
+ },
+ },
+ sessionExpiresIn: new TimeSpan(365, "d"),
+ getUserAttributes: (attributes) => {
+ return {
+ email: attributes.email,
+ rol: attributes.rol,
+ secret: attributes.secret !== null,
+ };
+ },
+});
+
+export const validateBearerToken = async (
+ req: IncomingMessage,
+): ReturnValidateToken => {
+ const authorizationHeader = req.headers.authorization;
+ const sessionId = luciaToken.readBearerToken(authorizationHeader ?? "");
+ if (!sessionId) {
+ return {
+ user: null,
+ session: null,
+ };
+ }
+ const result = await luciaToken.validateSession(sessionId);
+ return {
+ session: result.session,
+ ...((result.user && {
+ user: {
+ adminId: result.user.adminId,
+ authId: result.user.id,
+ email: result.user.email,
+ rol: result.user.rol,
+ id: result.user.id,
+ secret: result.user.secret,
+ },
+ }) || {
+ user: null,
+ }),
+ };
+};
diff --git a/packages/builders/src/constants/index.ts b/packages/builders/src/constants/index.ts
new file mode 100644
index 000000000..f2f1a4d88
--- /dev/null
+++ b/packages/builders/src/constants/index.ts
@@ -0,0 +1,39 @@
+import path from "node:path";
+import Docker from "dockerode";
+
+export const IS_CLOUD = process.env.IS_CLOUD === "true";
+export const docker = new Docker();
+
+export const paths = (isServer = false) => {
+ if (isServer) {
+ const BASE_PATH = "/etc/dokploy";
+ return {
+ BASE_PATH,
+ MAIN_TRAEFIK_PATH: `${BASE_PATH}/traefik`,
+ DYNAMIC_TRAEFIK_PATH: `${BASE_PATH}/traefik/dynamic`,
+ LOGS_PATH: `${BASE_PATH}/logs`,
+ APPLICATIONS_PATH: `${BASE_PATH}/applications`,
+ COMPOSE_PATH: `${BASE_PATH}/compose`,
+ SSH_PATH: `${BASE_PATH}/ssh`,
+ CERTIFICATES_PATH: `${BASE_PATH}/certificates`,
+ MONITORING_PATH: `${BASE_PATH}/monitoring`,
+ REGISTRY_PATH: `${BASE_PATH}/registry`,
+ };
+ }
+ const BASE_PATH =
+ process.env.NODE_ENV === "production"
+ ? "/etc/dokploy"
+ : path.join(process.cwd(), ".docker");
+ return {
+ BASE_PATH,
+ MAIN_TRAEFIK_PATH: `${BASE_PATH}/traefik`,
+ DYNAMIC_TRAEFIK_PATH: `${BASE_PATH}/traefik/dynamic`,
+ LOGS_PATH: `${BASE_PATH}/logs`,
+ APPLICATIONS_PATH: `${BASE_PATH}/applications`,
+ COMPOSE_PATH: `${BASE_PATH}/compose`,
+ SSH_PATH: `${BASE_PATH}/ssh`,
+ CERTIFICATES_PATH: `${BASE_PATH}/certificates`,
+ MONITORING_PATH: `${BASE_PATH}/monitoring`,
+ REGISTRY_PATH: `${BASE_PATH}/registry`,
+ };
+};
diff --git a/packages/builders/src/db/drizzle.config.ts b/packages/builders/src/db/drizzle.config.ts
new file mode 100644
index 000000000..f556649be
--- /dev/null
+++ b/packages/builders/src/db/drizzle.config.ts
@@ -0,0 +1,14 @@
+import { defineConfig } from "drizzle-kit";
+
+export default defineConfig({
+ schema: "./server/db/schema/index.ts",
+ dialect: "postgresql",
+ dbCredentials: {
+ url: process.env.DATABASE_URL || "",
+ },
+ out: "drizzle",
+ migrations: {
+ table: "migrations",
+ schema: "public",
+ },
+});
diff --git a/packages/builders/src/db/index.ts b/packages/builders/src/db/index.ts
new file mode 100644
index 000000000..7c5f79bd2
--- /dev/null
+++ b/packages/builders/src/db/index.ts
@@ -0,0 +1,28 @@
+import { type PostgresJsDatabase, drizzle } from "drizzle-orm/postgres-js";
+import postgres from "postgres";
+// import { sc } from "@dokploy/schema";
+import * as schema from "./schema";
+// schema
+// import * as schema from "@dokploy/schema";
+
+// type Schema = typeof schema;
+
+// type Schema = typeof schema;
+declare global {
+ // eslint-disable-next-line no-var -- only var works here
+ var db: PostgresJsDatabase | undefined;
+}
+
+export let db: PostgresJsDatabase;
+if (process.env.NODE_ENV === "production") {
+ db = drizzle(postgres(process.env.DATABASE_URL || ""), {
+ schema,
+ });
+} else {
+ if (!global.db)
+ global.db = drizzle(postgres(process.env.DATABASE_URL || ""), {
+ schema,
+ });
+
+ db = global.db;
+}
diff --git a/packages/builders/src/db/migration.ts b/packages/builders/src/db/migration.ts
new file mode 100644
index 000000000..d38d39434
--- /dev/null
+++ b/packages/builders/src/db/migration.ts
@@ -0,0 +1,21 @@
+import { drizzle } from "drizzle-orm/postgres-js";
+import { migrate } from "drizzle-orm/postgres-js/migrator";
+import postgres from "postgres";
+
+const connectionString = process.env.DATABASE_URL || "";
+
+const sql = postgres(connectionString, { max: 1 });
+const db = drizzle(sql);
+
+export const migration = async () =>
+ await migrate(db, { migrationsFolder: "drizzle" })
+ .then(() => {
+ console.log("Migration complete");
+ sql.end();
+ })
+ .catch((error) => {
+ console.log("Migration failed", error);
+ })
+ .finally(() => {
+ sql.end();
+ });
diff --git a/packages/builders/src/db/reset.ts b/packages/builders/src/db/reset.ts
new file mode 100644
index 000000000..4a4dbecba
--- /dev/null
+++ b/packages/builders/src/db/reset.ts
@@ -0,0 +1,23 @@
+import { sql } from "drizzle-orm";
+// Credits to Louistiti from Drizzle Discord: https://discord.com/channels/1043890932593987624/1130802621750448160/1143083373535973406
+import { drizzle } from "drizzle-orm/postgres-js";
+import postgres from "postgres";
+
+const connectionString = process.env.DATABASE_URL || "";
+
+const pg = postgres(connectionString, { max: 1 });
+const db = drizzle(pg);
+
+const clearDb = async (): Promise => {
+ try {
+ const tablesQuery = sql`DROP SCHEMA public CASCADE; CREATE SCHEMA public; DROP schema drizzle CASCADE;`;
+ const tables = await db.execute(tablesQuery);
+ console.log(tables);
+ await pg.end();
+ } catch (error) {
+ console.error("Error to clean database", error);
+ } finally {
+ }
+};
+
+clearDb();
diff --git a/apps/dokploy/server/db/schema/admin.ts b/packages/builders/src/db/schema/admin.ts
similarity index 100%
rename from apps/dokploy/server/db/schema/admin.ts
rename to packages/builders/src/db/schema/admin.ts
diff --git a/apps/dokploy/server/db/schema/application.ts b/packages/builders/src/db/schema/application.ts
similarity index 96%
rename from apps/dokploy/server/db/schema/application.ts
rename to packages/builders/src/db/schema/application.ts
index 09175edc8..beeeece99 100644
--- a/apps/dokploy/server/db/schema/application.ts
+++ b/packages/builders/src/db/schema/application.ts
@@ -10,7 +10,6 @@ import {
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
-import { bitbucket, github, gitlab, server } from ".";
import { deployments } from "./deployment";
import { domains } from "./domain";
import { mounts } from "./mount";
@@ -22,6 +21,10 @@ import { security } from "./security";
import { applicationStatus } from "./shared";
import { sshKeys } from "./ssh-key";
import { generateAppName } from "./utils";
+import { github } from "./github";
+import { gitlab } from "./gitlab";
+import { bitbucket } from "./bitbucket";
+import { server } from "./server";
export const sourceType = pgEnum("sourceType", [
"docker",
@@ -41,7 +44,7 @@ export const buildType = pgEnum("buildType", [
]);
// TODO: refactor this types
-interface HealthCheckSwarm {
+export interface HealthCheckSwarm {
Test?: string[] | undefined;
Interval?: number | undefined;
Timeout?: number | undefined;
@@ -49,14 +52,14 @@ interface HealthCheckSwarm {
Retries?: number | undefined;
}
-interface RestartPolicySwarm {
+export interface RestartPolicySwarm {
Condition?: string | undefined;
Delay?: number | undefined;
MaxAttempts?: number | undefined;
Window?: number | undefined;
}
-interface PlacementSwarm {
+export interface PlacementSwarm {
Constraints?: string[] | undefined;
Preferences?: Array<{ Spread: { SpreadDescriptor: string } }> | undefined;
MaxReplicas?: number | undefined;
@@ -68,7 +71,7 @@ interface PlacementSwarm {
| undefined;
}
-interface UpdateConfigSwarm {
+export interface UpdateConfigSwarm {
Parallelism: number;
Delay?: number | undefined;
FailureAction?: string | undefined;
@@ -77,7 +80,7 @@ interface UpdateConfigSwarm {
Order: string;
}
-interface ServiceModeSwarm {
+export interface ServiceModeSwarm {
Replicated?: { Replicas?: number | undefined } | undefined;
Global?: {} | undefined;
ReplicatedJob?:
@@ -89,13 +92,13 @@ interface ServiceModeSwarm {
GlobalJob?: {} | undefined;
}
-interface NetworkSwarm {
+export interface NetworkSwarm {
Target?: string | undefined;
Aliases?: string[] | undefined;
DriverOpts?: { [key: string]: string } | undefined;
}
-interface LabelsSwarm {
+export interface LabelsSwarm {
[name: string]: string;
}
diff --git a/apps/dokploy/server/db/schema/auth.ts b/packages/builders/src/db/schema/auth.ts
similarity index 100%
rename from apps/dokploy/server/db/schema/auth.ts
rename to packages/builders/src/db/schema/auth.ts
diff --git a/apps/dokploy/server/db/schema/backups.ts b/packages/builders/src/db/schema/backups.ts
similarity index 100%
rename from apps/dokploy/server/db/schema/backups.ts
rename to packages/builders/src/db/schema/backups.ts
diff --git a/apps/dokploy/server/db/schema/bitbucket.ts b/packages/builders/src/db/schema/bitbucket.ts
similarity index 100%
rename from apps/dokploy/server/db/schema/bitbucket.ts
rename to packages/builders/src/db/schema/bitbucket.ts
diff --git a/apps/dokploy/server/db/schema/certificate.ts b/packages/builders/src/db/schema/certificate.ts
similarity index 100%
rename from apps/dokploy/server/db/schema/certificate.ts
rename to packages/builders/src/db/schema/certificate.ts
diff --git a/apps/dokploy/server/db/schema/compose.ts b/packages/builders/src/db/schema/compose.ts
similarity index 96%
rename from apps/dokploy/server/db/schema/compose.ts
rename to packages/builders/src/db/schema/compose.ts
index b15171619..29d90edcd 100644
--- a/apps/dokploy/server/db/schema/compose.ts
+++ b/packages/builders/src/db/schema/compose.ts
@@ -1,16 +1,19 @@
-import { sshKeys } from "@/server/db/schema/ssh-key";
+import { sshKeys } from "./ssh-key";
import { relations } from "drizzle-orm";
import { boolean, integer, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
-import { bitbucket, github, gitlab, server } from ".";
import { deployments } from "./deployment";
import { domains } from "./domain";
import { mounts } from "./mount";
import { projects } from "./project";
import { applicationStatus } from "./shared";
import { generateAppName } from "./utils";
+import { github } from "./github";
+import { gitlab } from "./gitlab";
+import { bitbucket } from "./bitbucket";
+import { server } from "./server";
export const sourceTypeCompose = pgEnum("sourceTypeCompose", [
"git",
diff --git a/apps/dokploy/server/db/schema/deployment.ts b/packages/builders/src/db/schema/deployment.ts
similarity index 100%
rename from apps/dokploy/server/db/schema/deployment.ts
rename to packages/builders/src/db/schema/deployment.ts
diff --git a/apps/dokploy/server/db/schema/destination.ts b/packages/builders/src/db/schema/destination.ts
similarity index 100%
rename from apps/dokploy/server/db/schema/destination.ts
rename to packages/builders/src/db/schema/destination.ts
diff --git a/apps/dokploy/server/db/schema/domain.ts b/packages/builders/src/db/schema/domain.ts
similarity index 97%
rename from apps/dokploy/server/db/schema/domain.ts
rename to packages/builders/src/db/schema/domain.ts
index 301175fc5..eee9a8198 100644
--- a/apps/dokploy/server/db/schema/domain.ts
+++ b/packages/builders/src/db/schema/domain.ts
@@ -1,4 +1,4 @@
-import { domain } from "@/server/db/validations/domain";
+import { domain } from "../validations/domain";
import { relations } from "drizzle-orm";
import {
boolean,
diff --git a/apps/dokploy/server/db/schema/git-provider.ts b/packages/builders/src/db/schema/git-provider.ts
similarity index 100%
rename from apps/dokploy/server/db/schema/git-provider.ts
rename to packages/builders/src/db/schema/git-provider.ts
diff --git a/apps/dokploy/server/db/schema/github.ts b/packages/builders/src/db/schema/github.ts
similarity index 100%
rename from apps/dokploy/server/db/schema/github.ts
rename to packages/builders/src/db/schema/github.ts
diff --git a/apps/dokploy/server/db/schema/gitlab.ts b/packages/builders/src/db/schema/gitlab.ts
similarity index 100%
rename from apps/dokploy/server/db/schema/gitlab.ts
rename to packages/builders/src/db/schema/gitlab.ts
diff --git a/packages/builders/src/db/schema/index.ts b/packages/builders/src/db/schema/index.ts
new file mode 100644
index 000000000..4a6103688
--- /dev/null
+++ b/packages/builders/src/db/schema/index.ts
@@ -0,0 +1,31 @@
+export * from "./application";
+export * from "./postgres";
+export * from "./user";
+export * from "./admin";
+export * from "./auth";
+export * from "./project";
+export * from "./domain";
+export * from "./mariadb";
+export * from "./mongo";
+export * from "./mysql";
+export * from "./backups";
+export * from "./destination";
+export * from "./deployment";
+export * from "./mount";
+export * from "./certificate";
+export * from "./session";
+export * from "./redirects";
+export * from "./security";
+export * from "./port";
+export * from "./redis";
+export * from "./shared";
+export * from "./compose";
+export * from "./registry";
+export * from "./notification";
+export * from "./ssh-key";
+export * from "./git-provider";
+export * from "./bitbucket";
+export * from "./github";
+export * from "./gitlab";
+export * from "./server";
+export * from "./utils";
diff --git a/apps/dokploy/server/db/schema/mariadb.ts b/packages/builders/src/db/schema/mariadb.ts
similarity index 98%
rename from apps/dokploy/server/db/schema/mariadb.ts
rename to packages/builders/src/db/schema/mariadb.ts
index b5e13284f..dcae297b0 100644
--- a/apps/dokploy/server/db/schema/mariadb.ts
+++ b/packages/builders/src/db/schema/mariadb.ts
@@ -1,4 +1,3 @@
-import { generatePassword } from "@/templates/utils";
import { relations } from "drizzle-orm";
import { integer, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
diff --git a/apps/dokploy/server/db/schema/mongo.ts b/packages/builders/src/db/schema/mongo.ts
similarity index 100%
rename from apps/dokploy/server/db/schema/mongo.ts
rename to packages/builders/src/db/schema/mongo.ts
diff --git a/apps/dokploy/server/db/schema/mount.ts b/packages/builders/src/db/schema/mount.ts
similarity index 100%
rename from apps/dokploy/server/db/schema/mount.ts
rename to packages/builders/src/db/schema/mount.ts
diff --git a/apps/dokploy/server/db/schema/mysql.ts b/packages/builders/src/db/schema/mysql.ts
similarity index 100%
rename from apps/dokploy/server/db/schema/mysql.ts
rename to packages/builders/src/db/schema/mysql.ts
diff --git a/apps/dokploy/server/db/schema/notification.ts b/packages/builders/src/db/schema/notification.ts
similarity index 100%
rename from apps/dokploy/server/db/schema/notification.ts
rename to packages/builders/src/db/schema/notification.ts
diff --git a/apps/dokploy/server/db/schema/port.ts b/packages/builders/src/db/schema/port.ts
similarity index 100%
rename from apps/dokploy/server/db/schema/port.ts
rename to packages/builders/src/db/schema/port.ts
diff --git a/apps/dokploy/server/db/schema/postgres.ts b/packages/builders/src/db/schema/postgres.ts
similarity index 100%
rename from apps/dokploy/server/db/schema/postgres.ts
rename to packages/builders/src/db/schema/postgres.ts
diff --git a/apps/dokploy/server/db/schema/project.ts b/packages/builders/src/db/schema/project.ts
similarity index 100%
rename from apps/dokploy/server/db/schema/project.ts
rename to packages/builders/src/db/schema/project.ts
diff --git a/apps/dokploy/server/db/schema/redirects.ts b/packages/builders/src/db/schema/redirects.ts
similarity index 100%
rename from apps/dokploy/server/db/schema/redirects.ts
rename to packages/builders/src/db/schema/redirects.ts
diff --git a/apps/dokploy/server/db/schema/redis.ts b/packages/builders/src/db/schema/redis.ts
similarity index 100%
rename from apps/dokploy/server/db/schema/redis.ts
rename to packages/builders/src/db/schema/redis.ts
diff --git a/apps/dokploy/server/db/schema/registry.ts b/packages/builders/src/db/schema/registry.ts
similarity index 100%
rename from apps/dokploy/server/db/schema/registry.ts
rename to packages/builders/src/db/schema/registry.ts
diff --git a/apps/dokploy/server/db/schema/security.ts b/packages/builders/src/db/schema/security.ts
similarity index 100%
rename from apps/dokploy/server/db/schema/security.ts
rename to packages/builders/src/db/schema/security.ts
diff --git a/apps/dokploy/server/db/schema/server.ts b/packages/builders/src/db/schema/server.ts
similarity index 89%
rename from apps/dokploy/server/db/schema/server.ts
rename to packages/builders/src/db/schema/server.ts
index 02fa87cb7..6d435d88c 100644
--- a/apps/dokploy/server/db/schema/server.ts
+++ b/packages/builders/src/db/schema/server.ts
@@ -3,19 +3,18 @@ import { boolean, integer, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
-import {
- applications,
- compose,
- mariadb,
- mongo,
- mysql,
- postgres,
- redis,
-} from ".";
+
import { admins } from "./admin";
import { deployments } from "./deployment";
import { sshKeys } from "./ssh-key";
import { generateAppName } from "./utils";
+import { applications } from "./application";
+import { compose } from "./compose";
+import { mariadb } from "./mariadb";
+import { mongo } from "./mongo";
+import { mysql } from "./mysql";
+import { postgres } from "./postgres";
+import { redis } from "./redis";
export const server = pgTable("server", {
serverId: text("serverId")
diff --git a/apps/dokploy/server/db/schema/session.ts b/packages/builders/src/db/schema/session.ts
similarity index 100%
rename from apps/dokploy/server/db/schema/session.ts
rename to packages/builders/src/db/schema/session.ts
diff --git a/apps/dokploy/server/db/schema/shared.ts b/packages/builders/src/db/schema/shared.ts
similarity index 100%
rename from apps/dokploy/server/db/schema/shared.ts
rename to packages/builders/src/db/schema/shared.ts
diff --git a/apps/dokploy/server/db/schema/source.ts b/packages/builders/src/db/schema/source.ts
similarity index 100%
rename from apps/dokploy/server/db/schema/source.ts
rename to packages/builders/src/db/schema/source.ts
diff --git a/apps/dokploy/server/db/schema/ssh-key.ts b/packages/builders/src/db/schema/ssh-key.ts
similarity index 88%
rename from apps/dokploy/server/db/schema/ssh-key.ts
rename to packages/builders/src/db/schema/ssh-key.ts
index 8ff6b0e21..34ce188de 100644
--- a/apps/dokploy/server/db/schema/ssh-key.ts
+++ b/packages/builders/src/db/schema/ssh-key.ts
@@ -1,6 +1,6 @@
-import { applications } from "@/server/db/schema/application";
-import { compose } from "@/server/db/schema/compose";
-import { sshKeyCreate, sshKeyType } from "@/server/db/validations";
+import { applications } from "./application";
+import { compose } from "./compose";
+import { sshKeyCreate, sshKeyType } from "../validations";
import { relations } from "drizzle-orm";
import { pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
diff --git a/apps/dokploy/server/db/schema/user.ts b/packages/builders/src/db/schema/user.ts
similarity index 100%
rename from apps/dokploy/server/db/schema/user.ts
rename to packages/builders/src/db/schema/user.ts
diff --git a/apps/dokploy/server/db/schema/utils.ts b/packages/builders/src/db/schema/utils.ts
similarity index 100%
rename from apps/dokploy/server/db/schema/utils.ts
rename to packages/builders/src/db/schema/utils.ts
diff --git a/packages/builders/src/db/seed.ts b/packages/builders/src/db/seed.ts
new file mode 100644
index 000000000..bbb3c3f27
--- /dev/null
+++ b/packages/builders/src/db/seed.ts
@@ -0,0 +1,35 @@
+import bc from "bcrypt";
+import { drizzle } from "drizzle-orm/postgres-js";
+import postgres from "postgres";
+import { users } from "./schema";
+
+const connectionString = process.env.DATABASE_URL || "";
+
+const pg = postgres(connectionString, { max: 1 });
+const db = drizzle(pg);
+
+function password(txt: string) {
+ return bc.hashSync(txt, 10);
+}
+
+async function seed() {
+ console.log("> Seed:", process.env.DATABASE_PATH, "\n");
+
+ // const authenticationR = await db
+ // .insert(users)
+ // .values([
+ // {
+ // email: "user1@hotmail.com",
+ // password: password("12345671"),
+ // },
+ // ])
+ // .onConflictDoNothing()
+ // .returning();
+
+ // console.log("\nSemillas Update:", authenticationR.length);
+}
+
+seed().catch((e) => {
+ console.error(e);
+ process.exit(1);
+});
diff --git a/packages/builders/src/db/validations/domain.ts b/packages/builders/src/db/validations/domain.ts
new file mode 100644
index 000000000..b09b57b6c
--- /dev/null
+++ b/packages/builders/src/db/validations/domain.ts
@@ -0,0 +1,46 @@
+import { z } from "zod";
+
+export const domain = z
+ .object({
+ host: z.string().min(1, { message: "Add a hostname" }),
+ path: z.string().min(1).optional(),
+ port: z
+ .number()
+ .min(1, { message: "Port must be at least 1" })
+ .max(65535, { message: "Port must be 65535 or below" })
+ .optional(),
+ https: z.boolean().optional(),
+ certificateType: z.enum(["letsencrypt", "none"]).optional(),
+ })
+ .superRefine((input, ctx) => {
+ if (input.https && !input.certificateType) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ path: ["certificateType"],
+ message: "Required",
+ });
+ }
+ });
+
+export const domainCompose = z
+ .object({
+ host: z.string().min(1, { message: "Host is required" }),
+ path: z.string().min(1).optional(),
+ port: z
+ .number()
+ .min(1, { message: "Port must be at least 1" })
+ .max(65535, { message: "Port must be 65535 or below" })
+ .optional(),
+ https: z.boolean().optional(),
+ certificateType: z.enum(["letsencrypt", "none"]).optional(),
+ serviceName: z.string().min(1, { message: "Service name is required" }),
+ })
+ .superRefine((input, ctx) => {
+ if (input.https && !input.certificateType) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ path: ["certificateType"],
+ message: "Required",
+ });
+ }
+ });
diff --git a/packages/builders/src/db/validations/index.ts b/packages/builders/src/db/validations/index.ts
new file mode 100644
index 000000000..b3cb57852
--- /dev/null
+++ b/packages/builders/src/db/validations/index.ts
@@ -0,0 +1,37 @@
+import { z } from "zod";
+
+export const sshKeyCreate = z.object({
+ name: z.string().min(1),
+ description: z.string().optional(),
+ publicKey: z.string().refine(
+ (key) => {
+ const rsaPubPattern = /^ssh-rsa\s+([A-Za-z0-9+/=]+)\s*(.*)?\s*$/;
+ const ed25519PubPattern = /^ssh-ed25519\s+([A-Za-z0-9+/=]+)\s*(.*)?\s*$/;
+ return rsaPubPattern.test(key) || ed25519PubPattern.test(key);
+ },
+ {
+ message: "Invalid public key format",
+ },
+ ),
+ privateKey: z.string().refine(
+ (key) => {
+ const rsaPrivPattern =
+ /^-----BEGIN RSA PRIVATE KEY-----\n([A-Za-z0-9+/=\n]+)-----END RSA PRIVATE KEY-----\s*$/;
+ const ed25519PrivPattern =
+ /^-----BEGIN OPENSSH PRIVATE KEY-----\n([A-Za-z0-9+/=\n]+)-----END OPENSSH PRIVATE KEY-----\s*$/;
+ return rsaPrivPattern.test(key) || ed25519PrivPattern.test(key);
+ },
+ {
+ message: "Invalid private key format",
+ },
+ ),
+});
+
+export const sshKeyUpdate = sshKeyCreate.pick({
+ name: true,
+ description: true,
+});
+
+export const sshKeyType = z.object({
+ type: z.enum(["rsa", "ed25519"]).optional(),
+});
diff --git a/packages/builders/src/emails/.gitignore b/packages/builders/src/emails/.gitignore
new file mode 100644
index 000000000..b2d59d1f7
--- /dev/null
+++ b/packages/builders/src/emails/.gitignore
@@ -0,0 +1,2 @@
+/node_modules
+/dist
\ No newline at end of file
diff --git a/packages/builders/src/emails/emails/build-failed.tsx b/packages/builders/src/emails/emails/build-failed.tsx
new file mode 100644
index 000000000..87cd02a7c
--- /dev/null
+++ b/packages/builders/src/emails/emails/build-failed.tsx
@@ -0,0 +1,113 @@
+import {
+ Body,
+ Button,
+ Container,
+ Head,
+ Heading,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Section,
+ Tailwind,
+ Text,
+} from "@react-email/components";
+import * as React from "react";
+
+export type TemplateProps = {
+ projectName: string;
+ applicationName: string;
+ applicationType: string;
+ errorMessage: string;
+ buildLink: string;
+ date: string;
+};
+
+export const BuildFailedEmail = ({
+ projectName = "dokploy",
+ applicationName = "frontend",
+ applicationType = "application",
+ errorMessage = "Error array.length is not a function",
+ buildLink = "https://dokploy.com/projects/dokploy-test/applications/dokploy-test",
+ date = "2023-05-01T00:00:00.000Z",
+}: TemplateProps) => {
+ const previewText = `Build failed for ${applicationName}`;
+ return (
+
+
+ {previewText}
+
+
+
+
+
+
+
+ Build failed for {applicationName}
+
+
+ Hello,
+
+
+ Your build for {applicationName} failed. Please
+ check the error message below.
+
+
+ Details:
+
+ Project Name: {projectName}
+
+
+ Application Name: {applicationName}
+
+
+ Application Type: {applicationType}
+
+
+ Date: {date}
+
+
+
+ Reason:
+ {errorMessage}
+
+
+
+ or copy and paste this URL into your browser:{" "}
+
+ {buildLink}
+
+
+
+
+
+
+ );
+};
+
+export default BuildFailedEmail;
diff --git a/packages/builders/src/emails/emails/build-success.tsx b/packages/builders/src/emails/emails/build-success.tsx
new file mode 100644
index 000000000..321a65c92
--- /dev/null
+++ b/packages/builders/src/emails/emails/build-success.tsx
@@ -0,0 +1,106 @@
+import {
+ Body,
+ Button,
+ Container,
+ Head,
+ Heading,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Section,
+ Tailwind,
+ Text,
+} from "@react-email/components";
+import * as React from "react";
+
+export type TemplateProps = {
+ projectName: string;
+ applicationName: string;
+ applicationType: string;
+ buildLink: string;
+ date: string;
+};
+
+export const BuildSuccessEmail = ({
+ projectName = "dokploy",
+ applicationName = "frontend",
+ applicationType = "application",
+ buildLink = "https://dokploy.com/projects/dokploy-test/applications/dokploy-test",
+ date = "2023-05-01T00:00:00.000Z",
+}: TemplateProps) => {
+ const previewText = `Build success for ${applicationName}`;
+ return (
+
+
+ {previewText}
+
+
+
+
+
+
+
+ Build success for {applicationName}
+
+
+ Hello,
+
+
+ Your build for {applicationName} was successful
+
+
+ Details:
+
+ Project Name: {projectName}
+
+
+ Application Name: {applicationName}
+
+
+ Application Type: {applicationType}
+
+
+ Date: {date}
+
+
+
+
+ or copy and paste this URL into your browser:{" "}
+
+ {buildLink}
+
+
+
+
+
+
+ );
+};
+
+export default BuildSuccessEmail;
diff --git a/packages/builders/src/emails/emails/database-backup.tsx b/packages/builders/src/emails/emails/database-backup.tsx
new file mode 100644
index 000000000..a539246e4
--- /dev/null
+++ b/packages/builders/src/emails/emails/database-backup.tsx
@@ -0,0 +1,105 @@
+import {
+ Body,
+ Container,
+ Head,
+ Heading,
+ Html,
+ Img,
+ Preview,
+ Section,
+ Tailwind,
+ Text,
+} from "@react-email/components";
+import * as React from "react";
+
+export type TemplateProps = {
+ projectName: string;
+ applicationName: string;
+ databaseType: "postgres" | "mysql" | "mongodb" | "mariadb";
+ type: "error" | "success";
+ errorMessage?: string;
+ date: string;
+};
+
+export const DatabaseBackupEmail = ({
+ projectName = "dokploy",
+ applicationName = "frontend",
+ databaseType = "postgres",
+ type = "success",
+ errorMessage,
+ date = "2023-05-01T00:00:00.000Z",
+}: TemplateProps) => {
+ const previewText = `Database backup for ${applicationName} was ${type === "success" ? "successful ✅" : "failed ❌"}`;
+ return (
+
+ {previewText}
+
+
+
+
+
+
+
+
+
+ Database backup for {applicationName}
+
+
+ Hello,
+
+
+ Your database backup for {applicationName} was{" "}
+ {type === "success"
+ ? "successful ✅"
+ : "failed Please check the error message below. ❌"}
+ .
+
+
+ Details:
+
+ Project Name: {projectName}
+
+
+ Application Name: {applicationName}
+
+
+ Database Type: {databaseType}
+
+
+ Date: {date}
+
+
+ {type === "error" && errorMessage ? (
+
+ Reason:
+
+ {errorMessage || "Error message not provided"}
+
+
+ ) : null}
+
+
+
+
+ );
+};
+
+export default DatabaseBackupEmail;
diff --git a/packages/builders/src/emails/emails/docker-cleanup.tsx b/packages/builders/src/emails/emails/docker-cleanup.tsx
new file mode 100644
index 000000000..7ff5212cd
--- /dev/null
+++ b/packages/builders/src/emails/emails/docker-cleanup.tsx
@@ -0,0 +1,81 @@
+import {
+ Body,
+ Button,
+ Container,
+ Head,
+ Heading,
+ Html,
+ Img,
+ Preview,
+ Section,
+ Tailwind,
+ Text,
+} from "@react-email/components";
+import * as React from "react";
+
+export type TemplateProps = {
+ message: string;
+ date: string;
+};
+
+export const DockerCleanupEmail = ({
+ message = "Docker cleanup for dokploy",
+ date = "2023-05-01T00:00:00.000Z",
+}: TemplateProps) => {
+ const previewText = "Docker cleanup for dokploy";
+ return (
+
+ {previewText}
+
+
+
+
+
+
+
+
+
+ Docker cleanup for dokploy
+
+
+ Hello,
+
+
+ The docker cleanup for dokploy was successful ✅
+
+
+
+ Details:
+
+ Message: {message}
+
+
+ Date: {date}
+
+
+
+
+
+
+ );
+};
+
+export default DockerCleanupEmail;
diff --git a/packages/builders/src/emails/emails/dokploy-restart.tsx b/packages/builders/src/emails/emails/dokploy-restart.tsx
new file mode 100644
index 000000000..c8ed665a7
--- /dev/null
+++ b/packages/builders/src/emails/emails/dokploy-restart.tsx
@@ -0,0 +1,75 @@
+import {
+ Body,
+ Container,
+ Head,
+ Heading,
+ Html,
+ Img,
+ Preview,
+ Section,
+ Tailwind,
+ Text,
+} from "@react-email/components";
+import * as React from "react";
+
+export type TemplateProps = {
+ date: string;
+};
+
+export const DokployRestartEmail = ({
+ date = "2023-05-01T00:00:00.000Z",
+}: TemplateProps) => {
+ const previewText = "Your dokploy server was restarted";
+ return (
+
+ {previewText}
+
+
+
+
+
+
+
+
+
+ Dokploy Server Restart
+
+
+ Hello,
+
+
+ Your dokploy server was restarted ✅
+
+
+
+ Details:
+
+ Date: {date}
+
+
+
+
+
+
+ );
+};
+
+export default DokployRestartEmail;
diff --git a/packages/builders/src/emails/emails/invitation.tsx b/packages/builders/src/emails/emails/invitation.tsx
new file mode 100644
index 000000000..170f0e4b0
--- /dev/null
+++ b/packages/builders/src/emails/emails/invitation.tsx
@@ -0,0 +1,98 @@
+import {
+ Body,
+ Button,
+ Container,
+ Head,
+ Heading,
+ Hr,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Section,
+ Tailwind,
+ Text,
+} from "@react-email/components";
+
+export type TemplateProps = {
+ email: string;
+ name: string;
+};
+
+interface VercelInviteUserEmailProps {
+ inviteLink: string;
+ toEmail: string;
+}
+
+export const InvitationEmail = ({
+ inviteLink,
+ toEmail,
+}: VercelInviteUserEmailProps) => {
+ const previewText = "Join to Dokploy";
+ return (
+
+
+ {previewText}
+
+
+
+
+
+
+
+ Join to Dokploy
+
+
+ Hello,
+
+
+ You have been invited to join Dokploy, a platform
+ that helps for deploying your apps to the cloud.
+
+
+
+ or copy and paste this URL into your browser:{" "}
+
+ https://dokploy.com
+
+
+
+
+ This invitation was intended for {toEmail}. This invite was sent
+ from dokploy.com. If you
+ were not expecting this invitation, you can ignore this email. If
+ you are concerned about your account's safety, please reply to
+
+
+
+
+
+ );
+};
+
+export default InvitationEmail;
diff --git a/packages/builders/src/emails/emails/notion-magic-link.tsx b/packages/builders/src/emails/emails/notion-magic-link.tsx
new file mode 100644
index 000000000..89cc3444c
--- /dev/null
+++ b/packages/builders/src/emails/emails/notion-magic-link.tsx
@@ -0,0 +1,150 @@
+import {
+ Body,
+ Container,
+ Head,
+ Heading,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Text,
+} from "@react-email/components";
+import * as React from "react";
+
+interface NotionMagicLinkEmailProps {
+ loginCode?: string;
+}
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : "";
+
+export const NotionMagicLinkEmail = ({
+ loginCode,
+}: NotionMagicLinkEmailProps) => (
+
+
+ Log in with this magic link
+
+
+ Login
+
+ Click here to log in with this magic link
+
+
+ Or, copy and paste this temporary login code:
+
+ {loginCode}
+
+ If you didn't try to login, you can safely ignore this email.
+
+
+ Hint: You can set a permanent password in Settings & members → My
+ account.
+
+
+
+
+ Notion.so
+
+ , the all-in-one-workspace
+
+ for your notes, tasks, wikis, and databases.
+
+
+
+
+);
+
+NotionMagicLinkEmail.PreviewProps = {
+ loginCode: "sparo-ndigo-amurt-secan",
+} as NotionMagicLinkEmailProps;
+
+export default NotionMagicLinkEmail;
+
+const main = {
+ backgroundColor: "#ffffff",
+};
+
+const container = {
+ paddingLeft: "12px",
+ paddingRight: "12px",
+ margin: "0 auto",
+};
+
+const h1 = {
+ color: "#333",
+ fontFamily:
+ "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
+ fontSize: "24px",
+ fontWeight: "bold",
+ margin: "40px 0",
+ padding: "0",
+};
+
+const link = {
+ color: "#2754C5",
+ fontFamily:
+ "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
+ fontSize: "14px",
+ textDecoration: "underline",
+};
+
+const text = {
+ color: "#333",
+ fontFamily:
+ "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
+ fontSize: "14px",
+ margin: "24px 0",
+};
+
+const footer = {
+ color: "#898989",
+ fontFamily:
+ "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
+ fontSize: "12px",
+ lineHeight: "22px",
+ marginTop: "12px",
+ marginBottom: "24px",
+};
+
+const code = {
+ display: "inline-block",
+ padding: "16px 4.5%",
+ width: "90.5%",
+ backgroundColor: "#f4f4f4",
+ borderRadius: "5px",
+ border: "1px solid #eee",
+ color: "#333",
+};
diff --git a/packages/builders/src/emails/emails/plaid-verify-identity.tsx b/packages/builders/src/emails/emails/plaid-verify-identity.tsx
new file mode 100644
index 000000000..2fce846a8
--- /dev/null
+++ b/packages/builders/src/emails/emails/plaid-verify-identity.tsx
@@ -0,0 +1,158 @@
+import {
+ Body,
+ Container,
+ Head,
+ Heading,
+ Html,
+ Img,
+ Link,
+ Section,
+ Text,
+} from "@react-email/components";
+import * as React from "react";
+
+interface PlaidVerifyIdentityEmailProps {
+ validationCode?: string;
+}
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : "";
+
+export const PlaidVerifyIdentityEmail = ({
+ validationCode,
+}: PlaidVerifyIdentityEmailProps) => (
+
+
+
+
+
+ Verify Your Identity
+
+ Enter the following code to finish linking Venmo.
+
+
+ Not expecting this email?
+
+ Contact{" "}
+
+ login@plaid.com
+ {" "}
+ if you did not request this code.
+
+
+ Securely powered by Plaid.
+
+
+);
+
+PlaidVerifyIdentityEmail.PreviewProps = {
+ validationCode: "144833",
+} as PlaidVerifyIdentityEmailProps;
+
+export default PlaidVerifyIdentityEmail;
+
+const main = {
+ backgroundColor: "#ffffff",
+ fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif",
+};
+
+const container = {
+ backgroundColor: "#ffffff",
+ border: "1px solid #eee",
+ borderRadius: "5px",
+ boxShadow: "0 5px 10px rgba(20,50,70,.2)",
+ marginTop: "20px",
+ maxWidth: "360px",
+ margin: "0 auto",
+ padding: "68px 0 130px",
+};
+
+const logo = {
+ margin: "0 auto",
+};
+
+const tertiary = {
+ color: "#0a85ea",
+ fontSize: "11px",
+ fontWeight: 700,
+ fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif",
+ height: "16px",
+ letterSpacing: "0",
+ lineHeight: "16px",
+ margin: "16px 8px 8px 8px",
+ textTransform: "uppercase" as const,
+ textAlign: "center" as const,
+};
+
+const secondary = {
+ color: "#000",
+ display: "inline-block",
+ fontFamily: "HelveticaNeue-Medium,Helvetica,Arial,sans-serif",
+ fontSize: "20px",
+ fontWeight: 500,
+ lineHeight: "24px",
+ marginBottom: "0",
+ marginTop: "0",
+ textAlign: "center" as const,
+};
+
+const codeContainer = {
+ background: "rgba(0,0,0,.05)",
+ borderRadius: "4px",
+ margin: "16px auto 14px",
+ verticalAlign: "middle",
+ width: "280px",
+};
+
+const code = {
+ color: "#000",
+ display: "inline-block",
+ fontFamily: "HelveticaNeue-Bold",
+ fontSize: "32px",
+ fontWeight: 700,
+ letterSpacing: "6px",
+ lineHeight: "40px",
+ paddingBottom: "8px",
+ paddingTop: "8px",
+ margin: "0 auto",
+ width: "100%",
+ textAlign: "center" as const,
+};
+
+const paragraph = {
+ color: "#444",
+ fontSize: "15px",
+ fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif",
+ letterSpacing: "0",
+ lineHeight: "23px",
+ padding: "0 40px",
+ margin: "0",
+ textAlign: "center" as const,
+};
+
+const link = {
+ color: "#444",
+ textDecoration: "underline",
+};
+
+const footer = {
+ color: "#000",
+ fontSize: "12px",
+ fontWeight: 800,
+ letterSpacing: "0",
+ lineHeight: "23px",
+ margin: "0",
+ marginTop: "20px",
+ fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif",
+ textAlign: "center" as const,
+ textTransform: "uppercase" as const,
+};
diff --git a/packages/builders/src/emails/emails/static/logo.png b/packages/builders/src/emails/emails/static/logo.png
new file mode 100644
index 000000000..e2e1413df
Binary files /dev/null and b/packages/builders/src/emails/emails/static/logo.png differ
diff --git a/packages/builders/src/emails/emails/static/notion-logo.png b/packages/builders/src/emails/emails/static/notion-logo.png
new file mode 100644
index 000000000..57cddee9b
Binary files /dev/null and b/packages/builders/src/emails/emails/static/notion-logo.png differ
diff --git a/packages/builders/src/emails/emails/static/plaid-logo.png b/packages/builders/src/emails/emails/static/plaid-logo.png
new file mode 100644
index 000000000..ce9d08cb2
Binary files /dev/null and b/packages/builders/src/emails/emails/static/plaid-logo.png differ
diff --git a/packages/builders/src/emails/emails/static/plaid.png b/packages/builders/src/emails/emails/static/plaid.png
new file mode 100644
index 000000000..ce9d08cb2
Binary files /dev/null and b/packages/builders/src/emails/emails/static/plaid.png differ
diff --git a/packages/builders/src/emails/emails/static/stripe-logo.png b/packages/builders/src/emails/emails/static/stripe-logo.png
new file mode 100644
index 000000000..af4c7719a
Binary files /dev/null and b/packages/builders/src/emails/emails/static/stripe-logo.png differ
diff --git a/packages/builders/src/emails/emails/static/vercel-arrow.png b/packages/builders/src/emails/emails/static/vercel-arrow.png
new file mode 100644
index 000000000..018f64d2c
Binary files /dev/null and b/packages/builders/src/emails/emails/static/vercel-arrow.png differ
diff --git a/packages/builders/src/emails/emails/static/vercel-logo.png b/packages/builders/src/emails/emails/static/vercel-logo.png
new file mode 100644
index 000000000..5b970948d
Binary files /dev/null and b/packages/builders/src/emails/emails/static/vercel-logo.png differ
diff --git a/packages/builders/src/emails/emails/static/vercel-team.png b/packages/builders/src/emails/emails/static/vercel-team.png
new file mode 100644
index 000000000..d3de7d938
Binary files /dev/null and b/packages/builders/src/emails/emails/static/vercel-team.png differ
diff --git a/packages/builders/src/emails/emails/static/vercel-user.png b/packages/builders/src/emails/emails/static/vercel-user.png
new file mode 100644
index 000000000..81beac694
Binary files /dev/null and b/packages/builders/src/emails/emails/static/vercel-user.png differ
diff --git a/packages/builders/src/emails/emails/stripe-welcome.tsx b/packages/builders/src/emails/emails/stripe-welcome.tsx
new file mode 100644
index 000000000..232f4a2ce
--- /dev/null
+++ b/packages/builders/src/emails/emails/stripe-welcome.tsx
@@ -0,0 +1,152 @@
+import {
+ Body,
+ Button,
+ Container,
+ Head,
+ Hr,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Section,
+ Text,
+} from "@react-email/components";
+import * as React from "react";
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : "";
+
+export const StripeWelcomeEmail = () => (
+
+
+ You're now ready to make live transactions with Stripe!
+
+
+
+
+
+
+ Thanks for submitting your account information. You're now ready to
+ make live transactions with Stripe!
+
+
+ You can view your payments and a variety of other information about
+ your account right from your dashboard.
+
+
+
+
+ If you haven't finished your integration, you might find our{" "}
+
+ docs
+ {" "}
+ handy.
+
+
+ Once you're ready to start accepting payments, you'll just need to
+ use your live{" "}
+
+ API keys
+ {" "}
+ instead of your test API keys. Your account can simultaneously be
+ used for both test and live requests, so you can continue testing
+ while accepting live payments. Check out our{" "}
+
+ tutorial about account basics
+
+ .
+
+
+ Finally, we've put together a{" "}
+
+ quick checklist
+ {" "}
+ to ensure your website conforms to card network standards.
+
+
+ We'll be here to help you with any step along the way. You can find
+ answers to most questions and get in touch with us on our{" "}
+
+ support site
+
+ .
+
+ — The Stripe team
+
+
+ Stripe, 354 Oyster Point Blvd, South San Francisco, CA 94080
+
+
+
+
+
+);
+
+export default StripeWelcomeEmail;
+
+const main = {
+ backgroundColor: "#f6f9fc",
+ fontFamily:
+ '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
+};
+
+const container = {
+ backgroundColor: "#ffffff",
+ margin: "0 auto",
+ padding: "20px 0 48px",
+ marginBottom: "64px",
+};
+
+const box = {
+ padding: "0 48px",
+};
+
+const hr = {
+ borderColor: "#e6ebf1",
+ margin: "20px 0",
+};
+
+const paragraph = {
+ color: "#525f7f",
+
+ fontSize: "16px",
+ lineHeight: "24px",
+ textAlign: "left" as const,
+};
+
+const anchor = {
+ color: "#556cd6",
+};
+
+const button = {
+ backgroundColor: "#656ee8",
+ borderRadius: "5px",
+ color: "#fff",
+ fontSize: "16px",
+ fontWeight: "bold",
+ textDecoration: "none",
+ textAlign: "center" as const,
+ display: "block",
+ width: "100%",
+ padding: "10px",
+};
+
+const footer = {
+ color: "#8898aa",
+ fontSize: "12px",
+ lineHeight: "16px",
+};
diff --git a/packages/builders/src/emails/emails/vercel-invite-user.tsx b/packages/builders/src/emails/emails/vercel-invite-user.tsx
new file mode 100644
index 000000000..bd7404ab9
--- /dev/null
+++ b/packages/builders/src/emails/emails/vercel-invite-user.tsx
@@ -0,0 +1,154 @@
+import {
+ Body,
+ Button,
+ Column,
+ Container,
+ Head,
+ Heading,
+ Hr,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from "@react-email/components";
+import * as React from "react";
+
+interface VercelInviteUserEmailProps {
+ username?: string;
+ userImage?: string;
+ invitedByUsername?: string;
+ invitedByEmail?: string;
+ teamName?: string;
+ teamImage?: string;
+ inviteLink?: string;
+ inviteFromIp?: string;
+ inviteFromLocation?: string;
+}
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : "";
+
+export const VercelInviteUserEmail = ({
+ username,
+ userImage,
+ invitedByUsername,
+ invitedByEmail,
+ teamName,
+ teamImage,
+ inviteLink,
+ inviteFromIp,
+ inviteFromLocation,
+}: VercelInviteUserEmailProps) => {
+ const previewText = `Join ${invitedByUsername} on Vercel`;
+
+ return (
+
+
+ {previewText}
+
+
+
+
+
+
+
+ Join {teamName} on Vercel
+
+
+ Hello {username},
+
+
+ {invitedByUsername} (
+
+ {invitedByEmail}
+
+ ) has invited you to the {teamName} team on{" "}
+ Vercel.
+
+
+
+
+ or copy and paste this URL into your browser:{" "}
+
+ {inviteLink}
+
+
+
+
+ This invitation was intended for{" "}
+ {username}. This invite was
+ sent from {inviteFromIp}{" "}
+ located in{" "}
+ {inviteFromLocation}. If you
+ were not expecting this invitation, you can ignore this email. If
+ you are concerned about your account's safety, please reply to
+ this email to get in touch with us.
+
+
+
+
+
+ );
+};
+
+VercelInviteUserEmail.PreviewProps = {
+ username: "alanturing",
+ userImage: `${baseUrl}/static/vercel-user.png`,
+ invitedByUsername: "Alan",
+ invitedByEmail: "alan.turing@example.com",
+ teamName: "Enigma",
+ teamImage: `${baseUrl}/static/vercel-team.png`,
+ inviteLink: "https://vercel.com/teams/invite/foo",
+ inviteFromIp: "204.13.186.218",
+ inviteFromLocation: "São Paulo, Brazil",
+} as VercelInviteUserEmailProps;
+
+export default VercelInviteUserEmail;
diff --git a/packages/builders/src/emails/package.json b/packages/builders/src/emails/package.json
new file mode 100644
index 000000000..c7aad5b58
--- /dev/null
+++ b/packages/builders/src/emails/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "emails",
+ "version": "0.0.19",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "build": "email build",
+ "dev": "email dev",
+ "export": "email export"
+ },
+ "dependencies": {
+ "@react-email/components": "0.0.21",
+ "react-email": "2.1.5",
+ "react": "^18.2.0"
+ },
+ "devDependencies": {
+ "@types/react": "18.2.33",
+ "@types/react-dom": "18.2.14"
+ }
+}
diff --git a/packages/builders/src/emails/pnpm-lock.yaml b/packages/builders/src/emails/pnpm-lock.yaml
new file mode 100644
index 000000000..270dd6cb5
--- /dev/null
+++ b/packages/builders/src/emails/pnpm-lock.yaml
@@ -0,0 +1,4209 @@
+lockfileVersion: '6.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+dependencies:
+ '@react-email/components':
+ specifier: 0.0.21
+ version: 0.0.21(@types/react@18.2.33)(react-dom@18.3.1)(react@18.3.1)
+ react:
+ specifier: ^18.2.0
+ version: 18.3.1
+ react-email:
+ specifier: 2.1.5
+ version: 2.1.5(eslint@9.7.0)
+
+devDependencies:
+ '@types/react':
+ specifier: 18.2.33
+ version: 18.2.33
+ '@types/react-dom':
+ specifier: 18.2.14
+ version: 18.2.14
+
+packages:
+
+ /@alloc/quick-lru@5.2.0:
+ resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
+ engines: {node: '>=10'}
+ dev: false
+
+ /@ampproject/remapping@2.3.0:
+ resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
+ engines: {node: '>=6.0.0'}
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.5
+ '@jridgewell/trace-mapping': 0.3.25
+ dev: false
+
+ /@babel/code-frame@7.24.7:
+ resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/highlight': 7.24.7
+ picocolors: 1.0.1
+ dev: false
+
+ /@babel/compat-data@7.24.8:
+ resolution: {integrity: sha512-c4IM7OTg6k1Q+AJ153e2mc2QVTezTwnb4VzquwcyiEzGnW0Kedv4do/TrkU98qPeC5LNiMt/QXwIjzYXLBpyZg==}
+ engines: {node: '>=6.9.0'}
+ dev: false
+
+ /@babel/core@7.24.5:
+ resolution: {integrity: sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@ampproject/remapping': 2.3.0
+ '@babel/code-frame': 7.24.7
+ '@babel/generator': 7.24.8
+ '@babel/helper-compilation-targets': 7.24.8
+ '@babel/helper-module-transforms': 7.24.8(@babel/core@7.24.5)
+ '@babel/helpers': 7.24.8
+ '@babel/parser': 7.24.5
+ '@babel/template': 7.24.7
+ '@babel/traverse': 7.24.8
+ '@babel/types': 7.24.8
+ convert-source-map: 2.0.0
+ debug: 4.3.5
+ gensync: 1.0.0-beta.2
+ json5: 2.2.3
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
+ /@babel/generator@7.24.8:
+ resolution: {integrity: sha512-47DG+6F5SzOi0uEvK4wMShmn5yY0mVjVJoWTphdY2B4Rx9wHgjK7Yhtr0ru6nE+sn0v38mzrWOlah0p/YlHHOQ==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/types': 7.24.8
+ '@jridgewell/gen-mapping': 0.3.5
+ '@jridgewell/trace-mapping': 0.3.25
+ jsesc: 2.5.2
+ dev: false
+
+ /@babel/helper-compilation-targets@7.24.8:
+ resolution: {integrity: sha512-oU+UoqCHdp+nWVDkpldqIQL/i/bvAv53tRqLG/s+cOXxe66zOYLU7ar/Xs3LdmBihrUMEUhwu6dMZwbNOYDwvw==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/compat-data': 7.24.8
+ '@babel/helper-validator-option': 7.24.8
+ browserslist: 4.23.2
+ lru-cache: 5.1.1
+ semver: 6.3.1
+ dev: false
+
+ /@babel/helper-environment-visitor@7.24.7:
+ resolution: {integrity: sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/types': 7.24.8
+ dev: false
+
+ /@babel/helper-function-name@7.24.7:
+ resolution: {integrity: sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/template': 7.24.7
+ '@babel/types': 7.24.8
+ dev: false
+
+ /@babel/helper-hoist-variables@7.24.7:
+ resolution: {integrity: sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/types': 7.24.8
+ dev: false
+
+ /@babel/helper-module-imports@7.24.7:
+ resolution: {integrity: sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/traverse': 7.24.8
+ '@babel/types': 7.24.8
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
+ /@babel/helper-module-transforms@7.24.8(@babel/core@7.24.5):
+ resolution: {integrity: sha512-m4vWKVqvkVAWLXfHCCfff2luJj86U+J0/x+0N3ArG/tP0Fq7zky2dYwMbtPmkc/oulkkbjdL3uWzuoBwQ8R00Q==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ dependencies:
+ '@babel/core': 7.24.5
+ '@babel/helper-environment-visitor': 7.24.7
+ '@babel/helper-module-imports': 7.24.7
+ '@babel/helper-simple-access': 7.24.7
+ '@babel/helper-split-export-declaration': 7.24.7
+ '@babel/helper-validator-identifier': 7.24.7
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
+ /@babel/helper-simple-access@7.24.7:
+ resolution: {integrity: sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/traverse': 7.24.8
+ '@babel/types': 7.24.8
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
+ /@babel/helper-split-export-declaration@7.24.7:
+ resolution: {integrity: sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/types': 7.24.8
+ dev: false
+
+ /@babel/helper-string-parser@7.24.8:
+ resolution: {integrity: sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==}
+ engines: {node: '>=6.9.0'}
+ dev: false
+
+ /@babel/helper-validator-identifier@7.24.7:
+ resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==}
+ engines: {node: '>=6.9.0'}
+ dev: false
+
+ /@babel/helper-validator-option@7.24.8:
+ resolution: {integrity: sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==}
+ engines: {node: '>=6.9.0'}
+ dev: false
+
+ /@babel/helpers@7.24.8:
+ resolution: {integrity: sha512-gV2265Nkcz7weJJfvDoAEVzC1e2OTDpkGbEsebse8koXUJUXPsCMi7sRo/+SPMuMZ9MtUPnGwITTnQnU5YjyaQ==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/template': 7.24.7
+ '@babel/types': 7.24.8
+ dev: false
+
+ /@babel/highlight@7.24.7:
+ resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/helper-validator-identifier': 7.24.7
+ chalk: 2.4.2
+ js-tokens: 4.0.0
+ picocolors: 1.0.1
+ dev: false
+
+ /@babel/parser@7.24.5:
+ resolution: {integrity: sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+ dependencies:
+ '@babel/types': 7.24.8
+ dev: false
+
+ /@babel/parser@7.24.8:
+ resolution: {integrity: sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+ dependencies:
+ '@babel/types': 7.24.8
+ dev: false
+
+ /@babel/runtime@7.24.8:
+ resolution: {integrity: sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ regenerator-runtime: 0.14.1
+ dev: false
+
+ /@babel/template@7.24.7:
+ resolution: {integrity: sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/code-frame': 7.24.7
+ '@babel/parser': 7.24.8
+ '@babel/types': 7.24.8
+ dev: false
+
+ /@babel/traverse@7.24.8:
+ resolution: {integrity: sha512-t0P1xxAPzEDcEPmjprAQq19NWum4K0EQPjMwZQZbHt+GiZqvjCHjj755Weq1YRPVzBI+3zSfvScfpnuIecVFJQ==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/code-frame': 7.24.7
+ '@babel/generator': 7.24.8
+ '@babel/helper-environment-visitor': 7.24.7
+ '@babel/helper-function-name': 7.24.7
+ '@babel/helper-hoist-variables': 7.24.7
+ '@babel/helper-split-export-declaration': 7.24.7
+ '@babel/parser': 7.24.8
+ '@babel/types': 7.24.8
+ debug: 4.3.5
+ globals: 11.12.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
+ /@babel/types@7.24.8:
+ resolution: {integrity: sha512-SkSBEHwwJRU52QEVZBmMBnE5Ux2/6WU1grdYyOhpbCNxbmJrDuDCphBzKZSO3taf0zztp+qkWlymE5tVL5l0TA==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/helper-string-parser': 7.24.8
+ '@babel/helper-validator-identifier': 7.24.7
+ to-fast-properties: 2.0.0
+ dev: false
+
+ /@emotion/is-prop-valid@0.8.8:
+ resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==}
+ requiresBuild: true
+ dependencies:
+ '@emotion/memoize': 0.7.4
+ dev: false
+ optional: true
+
+ /@emotion/memoize@0.7.4:
+ resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@esbuild/aix-ppc64@0.19.11:
+ resolution: {integrity: sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==}
+ engines: {node: '>=12'}
+ cpu: [ppc64]
+ os: [aix]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@esbuild/android-arm64@0.19.11:
+ resolution: {integrity: sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [android]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@esbuild/android-arm@0.19.11:
+ resolution: {integrity: sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==}
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [android]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@esbuild/android-x64@0.19.11:
+ resolution: {integrity: sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [android]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@esbuild/darwin-arm64@0.19.11:
+ resolution: {integrity: sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [darwin]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@esbuild/darwin-x64@0.19.11:
+ resolution: {integrity: sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [darwin]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@esbuild/freebsd-arm64@0.19.11:
+ resolution: {integrity: sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [freebsd]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@esbuild/freebsd-x64@0.19.11:
+ resolution: {integrity: sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [freebsd]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@esbuild/linux-arm64@0.19.11:
+ resolution: {integrity: sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [linux]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@esbuild/linux-arm@0.19.11:
+ resolution: {integrity: sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==}
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [linux]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@esbuild/linux-ia32@0.19.11:
+ resolution: {integrity: sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [linux]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@esbuild/linux-loong64@0.19.11:
+ resolution: {integrity: sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==}
+ engines: {node: '>=12'}
+ cpu: [loong64]
+ os: [linux]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@esbuild/linux-mips64el@0.19.11:
+ resolution: {integrity: sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==}
+ engines: {node: '>=12'}
+ cpu: [mips64el]
+ os: [linux]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@esbuild/linux-ppc64@0.19.11:
+ resolution: {integrity: sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==}
+ engines: {node: '>=12'}
+ cpu: [ppc64]
+ os: [linux]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@esbuild/linux-riscv64@0.19.11:
+ resolution: {integrity: sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==}
+ engines: {node: '>=12'}
+ cpu: [riscv64]
+ os: [linux]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@esbuild/linux-s390x@0.19.11:
+ resolution: {integrity: sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==}
+ engines: {node: '>=12'}
+ cpu: [s390x]
+ os: [linux]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@esbuild/linux-x64@0.19.11:
+ resolution: {integrity: sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [linux]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@esbuild/netbsd-x64@0.19.11:
+ resolution: {integrity: sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [netbsd]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@esbuild/openbsd-x64@0.19.11:
+ resolution: {integrity: sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [openbsd]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@esbuild/sunos-x64@0.19.11:
+ resolution: {integrity: sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [sunos]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@esbuild/win32-arm64@0.19.11:
+ resolution: {integrity: sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [win32]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@esbuild/win32-ia32@0.19.11:
+ resolution: {integrity: sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [win32]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@esbuild/win32-x64@0.19.11:
+ resolution: {integrity: sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [win32]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@eslint-community/eslint-utils@4.4.0(eslint@9.7.0):
+ resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ peerDependencies:
+ eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+ dependencies:
+ eslint: 9.7.0
+ eslint-visitor-keys: 3.4.3
+ dev: false
+
+ /@eslint-community/regexpp@4.11.0:
+ resolution: {integrity: sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==}
+ engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
+ dev: false
+
+ /@eslint/config-array@0.17.0:
+ resolution: {integrity: sha512-A68TBu6/1mHHuc5YJL0U0VVeGNiklLAL6rRmhTCP2B5XjWLMnrX+HkO+IAXyHvks5cyyY1jjK5ITPQ1HGS2EVA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ dependencies:
+ '@eslint/object-schema': 2.1.4
+ debug: 4.3.5
+ minimatch: 3.1.2
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
+ /@eslint/eslintrc@3.1.0:
+ resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ dependencies:
+ ajv: 6.12.6
+ debug: 4.3.5
+ espree: 10.1.0
+ globals: 14.0.0
+ ignore: 5.3.1
+ import-fresh: 3.3.0
+ js-yaml: 4.1.0
+ minimatch: 3.1.2
+ strip-json-comments: 3.1.1
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
+ /@eslint/js@9.7.0:
+ resolution: {integrity: sha512-ChuWDQenef8OSFnvuxv0TCVxEwmu3+hPNKvM9B34qpM0rDRbjL8t5QkQeHHeAfsKQjuH9wS82WeCi1J/owatng==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ dev: false
+
+ /@eslint/object-schema@2.1.4:
+ resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ dev: false
+
+ /@floating-ui/core@1.6.4:
+ resolution: {integrity: sha512-a4IowK4QkXl4SCWTGUR0INAfEOX3wtsYw3rKK5InQEHMGObkR8Xk44qYQD9P4r6HHw0iIfK6GUKECmY8sTkqRA==}
+ dependencies:
+ '@floating-ui/utils': 0.2.4
+ dev: false
+
+ /@floating-ui/dom@1.6.7:
+ resolution: {integrity: sha512-wmVfPG5o2xnKDU4jx/m4w5qva9FWHcnZ8BvzEe90D/RpwsJaTAVYPEPdQ8sbr/N8zZTAHlZUTQdqg8ZUbzHmng==}
+ dependencies:
+ '@floating-ui/core': 1.6.4
+ '@floating-ui/utils': 0.2.4
+ dev: false
+
+ /@floating-ui/react-dom@2.1.1(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg==}
+ peerDependencies:
+ react: '>=16.8.0'
+ react-dom: '>=16.8.0'
+ dependencies:
+ '@floating-ui/dom': 1.6.7
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ dev: false
+
+ /@floating-ui/utils@0.2.4:
+ resolution: {integrity: sha512-dWO2pw8hhi+WrXq1YJy2yCuWoL20PddgGaqTgVe4cOS9Q6qklXCiA1tJEqX6BEwRNSCP84/afac9hd4MS+zEUA==}
+ dev: false
+
+ /@humanwhocodes/module-importer@1.0.1:
+ resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
+ engines: {node: '>=12.22'}
+ dev: false
+
+ /@humanwhocodes/retry@0.3.0:
+ resolution: {integrity: sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==}
+ engines: {node: '>=18.18'}
+ dev: false
+
+ /@isaacs/cliui@8.0.2:
+ resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
+ engines: {node: '>=12'}
+ dependencies:
+ string-width: 5.1.2
+ string-width-cjs: /string-width@4.2.3
+ strip-ansi: 7.1.0
+ strip-ansi-cjs: /strip-ansi@6.0.1
+ wrap-ansi: 8.1.0
+ wrap-ansi-cjs: /wrap-ansi@7.0.0
+ dev: false
+
+ /@jridgewell/gen-mapping@0.3.5:
+ resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==}
+ engines: {node: '>=6.0.0'}
+ dependencies:
+ '@jridgewell/set-array': 1.2.1
+ '@jridgewell/sourcemap-codec': 1.5.0
+ '@jridgewell/trace-mapping': 0.3.25
+ dev: false
+
+ /@jridgewell/resolve-uri@3.1.2:
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+ engines: {node: '>=6.0.0'}
+ dev: false
+
+ /@jridgewell/set-array@1.2.1:
+ resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
+ engines: {node: '>=6.0.0'}
+ dev: false
+
+ /@jridgewell/source-map@0.3.6:
+ resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==}
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.5
+ '@jridgewell/trace-mapping': 0.3.25
+ dev: false
+
+ /@jridgewell/sourcemap-codec@1.5.0:
+ resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
+ dev: false
+
+ /@jridgewell/trace-mapping@0.3.25:
+ resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.5.0
+ dev: false
+
+ /@next/env@14.1.4:
+ resolution: {integrity: sha512-e7X7bbn3Z6DWnDi75UWn+REgAbLEqxI8Tq2pkFOFAMpWAWApz/YCUhtWMWn410h8Q2fYiYL7Yg5OlxMOCfFjJQ==}
+ dev: false
+
+ /@next/swc-darwin-arm64@14.1.4:
+ resolution: {integrity: sha512-ubmUkbmW65nIAOmoxT1IROZdmmJMmdYvXIe8211send9ZYJu+SqxSnJM4TrPj9wmL6g9Atvj0S/2cFmMSS99jg==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [darwin]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@next/swc-darwin-x64@14.1.4:
+ resolution: {integrity: sha512-b0Xo1ELj3u7IkZWAKcJPJEhBop117U78l70nfoQGo4xUSvv0PJSTaV4U9xQBLvZlnjsYkc8RwQN1HoH/oQmLlQ==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [darwin]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@next/swc-linux-arm64-gnu@14.1.4:
+ resolution: {integrity: sha512-457G0hcLrdYA/u1O2XkRMsDKId5VKe3uKPvrKVOyuARa6nXrdhJOOYU9hkKKyQTMru1B8qEP78IAhf/1XnVqKA==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@next/swc-linux-arm64-musl@14.1.4:
+ resolution: {integrity: sha512-l/kMG+z6MB+fKA9KdtyprkTQ1ihlJcBh66cf0HvqGP+rXBbOXX0dpJatjZbHeunvEHoBBS69GYQG5ry78JMy3g==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@next/swc-linux-x64-gnu@14.1.4:
+ resolution: {integrity: sha512-BapIFZ3ZRnvQ1uWbmqEGJuPT9cgLwvKtxhK/L2t4QYO7l+/DxXuIGjvp1x8rvfa/x1FFSsipERZK70pewbtJtw==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@next/swc-linux-x64-musl@14.1.4:
+ resolution: {integrity: sha512-mqVxTwk4XuBl49qn2A5UmzFImoL1iLm0KQQwtdRJRKl21ylQwwGCxJtIYo2rbfkZHoSKlh/YgztY0qH3wG1xIg==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@next/swc-win32-arm64-msvc@14.1.4:
+ resolution: {integrity: sha512-xzxF4ErcumXjO2Pvg/wVGrtr9QQJLk3IyQX1ddAC/fi6/5jZCZ9xpuL9Tzc4KPWMFq8GGWFVDMshZOdHGdkvag==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [win32]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@next/swc-win32-ia32-msvc@14.1.4:
+ resolution: {integrity: sha512-WZiz8OdbkpRw6/IU/lredZWKKZopUMhcI2F+XiMAcPja0uZYdMTZQRoQ0WZcvinn9xZAidimE7tN9W5v9Yyfyw==}
+ engines: {node: '>= 10'}
+ cpu: [ia32]
+ os: [win32]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@next/swc-win32-x64-msvc@14.1.4:
+ resolution: {integrity: sha512-4Rto21sPfw555sZ/XNLqfxDUNeLhNYGO2dlPqsnuCg8N8a2a9u1ltqBOPQ4vj1Gf7eJC0W2hHG2eYUHuiXgY2w==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [win32]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@nodelib/fs.scandir@2.1.5:
+ resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
+ engines: {node: '>= 8'}
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ run-parallel: 1.2.0
+ dev: false
+
+ /@nodelib/fs.stat@2.0.5:
+ resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
+ engines: {node: '>= 8'}
+ dev: false
+
+ /@nodelib/fs.walk@1.2.8:
+ resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
+ engines: {node: '>= 8'}
+ dependencies:
+ '@nodelib/fs.scandir': 2.1.5
+ fastq: 1.17.1
+ dev: false
+
+ /@one-ini/wasm@0.1.1:
+ resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==}
+ dev: false
+
+ /@pkgjs/parseargs@0.11.0:
+ resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
+ engines: {node: '>=14'}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@radix-ui/colors@1.0.1:
+ resolution: {integrity: sha512-xySw8f0ZVsAEP+e7iLl3EvcBXX7gsIlC1Zso/sPBW9gIWerBTgz6axrjU+MZ39wD+WFi5h5zdWpsg3+hwt2Qsg==}
+ dev: false
+
+ /@radix-ui/primitive@1.1.0:
+ resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==}
+ dev: false
+
+ /@radix-ui/react-arrow@1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1)
+ '@types/react': 18.2.47
+ '@types/react-dom': 18.2.14
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ dev: false
+
+ /@radix-ui/react-collapsible@1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-zQY7Epa8sTL0mq4ajSJpjgn2YmCgyrG7RsQgLp3C0LQVkG7+Tf6Pv1CeNWZLyqMjhdPkBa5Lx7wYBeSu7uCSTA==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@radix-ui/primitive': 1.1.0
+ '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@radix-ui/react-id': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@types/react': 18.2.47
+ '@types/react-dom': 18.2.14
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ dev: false
+
+ /@radix-ui/react-collection@1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-slot': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@types/react': 18.2.47
+ '@types/react-dom': 18.2.14
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ dev: false
+
+ /@radix-ui/react-compose-refs@1.0.1(@types/react@18.2.33)(react@18.3.1):
+ resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.24.8
+ '@types/react': 18.2.33
+ react: 18.3.1
+ dev: false
+
+ /@radix-ui/react-compose-refs@1.1.0(@types/react@18.2.47)(react@18.3.1):
+ resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@types/react': 18.2.47
+ react: 18.3.1
+ dev: false
+
+ /@radix-ui/react-context@1.1.0(@types/react@18.2.47)(react@18.3.1):
+ resolution: {integrity: sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@types/react': 18.2.47
+ react: 18.3.1
+ dev: false
+
+ /@radix-ui/react-direction@1.1.0(@types/react@18.2.47)(react@18.3.1):
+ resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@types/react': 18.2.47
+ react: 18.3.1
+ dev: false
+
+ /@radix-ui/react-dismissable-layer@1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-/UovfmmXGptwGcBQawLzvn2jOfM0t4z3/uKffoBlj724+n3FvBbZ7M0aaBOmkp6pqFYpO4yx8tSVJjx3Fl2jig==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@radix-ui/primitive': 1.1.0
+ '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@types/react': 18.2.47
+ '@types/react-dom': 18.2.14
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ dev: false
+
+ /@radix-ui/react-focus-guards@1.1.0(@types/react@18.2.47)(react@18.3.1):
+ resolution: {integrity: sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@types/react': 18.2.47
+ react: 18.3.1
+ dev: false
+
+ /@radix-ui/react-focus-scope@1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@types/react': 18.2.47
+ '@types/react-dom': 18.2.14
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ dev: false
+
+ /@radix-ui/react-id@1.1.0(@types/react@18.2.47)(react@18.3.1):
+ resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@types/react': 18.2.47
+ react: 18.3.1
+ dev: false
+
+ /@radix-ui/react-popover@1.1.1(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-3y1A3isulwnWhvTTwmIreiB8CF4L+qRjZnK1wYLO7pplddzXKby/GnZ2M7OZY3qgnl6p9AodUIHRYGXNah8Y7g==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@radix-ui/primitive': 1.1.0
+ '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-focus-guards': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-id': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-portal': 1.1.1(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-slot': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@types/react': 18.2.47
+ '@types/react-dom': 18.2.14
+ aria-hidden: 1.2.4
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ react-remove-scroll: 2.5.7(@types/react@18.2.47)(react@18.3.1)
+ dev: false
+
+ /@radix-ui/react-popper@1.2.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@floating-ui/react-dom': 2.1.1(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-arrow': 1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@radix-ui/react-use-rect': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@radix-ui/react-use-size': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@radix-ui/rect': 1.1.0
+ '@types/react': 18.2.47
+ '@types/react-dom': 18.2.14
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ dev: false
+
+ /@radix-ui/react-portal@1.1.1(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-A3UtLk85UtqhzFqtoC8Q0KvR2GbXF3mtPgACSazajqq6A41mEQgo53iPzY4i6BwDxlIFqWIhiQ2G729n+2aw/g==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@types/react': 18.2.47
+ '@types/react-dom': 18.2.14
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ dev: false
+
+ /@radix-ui/react-presence@1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@types/react': 18.2.47
+ '@types/react-dom': 18.2.14
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ dev: false
+
+ /@radix-ui/react-primitive@2.0.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@radix-ui/react-slot': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@types/react': 18.2.47
+ '@types/react-dom': 18.2.14
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ dev: false
+
+ /@radix-ui/react-roving-focus@1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@radix-ui/primitive': 1.1.0
+ '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@radix-ui/react-direction': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@radix-ui/react-id': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@types/react': 18.2.47
+ '@types/react-dom': 18.2.14
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ dev: false
+
+ /@radix-ui/react-slot@1.0.2(@types/react@18.2.33)(react@18.3.1):
+ resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.24.8
+ '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.33)(react@18.3.1)
+ '@types/react': 18.2.33
+ react: 18.3.1
+ dev: false
+
+ /@radix-ui/react-slot@1.1.0(@types/react@18.2.47)(react@18.3.1):
+ resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@types/react': 18.2.47
+ react: 18.3.1
+ dev: false
+
+ /@radix-ui/react-toggle-group@1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-PpTJV68dZU2oqqgq75Uzto5o/XfOVgkrJ9rulVmfTKxWp3HfUjHE6CP/WLRR4AzPX9HWxw7vFow2me85Yu+Naw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@radix-ui/primitive': 1.1.0
+ '@radix-ui/react-context': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@radix-ui/react-direction': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-roving-focus': 1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-toggle': 1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@types/react': 18.2.47
+ '@types/react-dom': 18.2.14
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ dev: false
+
+ /@radix-ui/react-toggle@1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-gwoxaKZ0oJ4vIgzsfESBuSgJNdc0rv12VhHgcqN0TEJmmZixXG/2XpsLK8kzNWYcnaoRIEEQc0bEi3dIvdUpjw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@radix-ui/primitive': 1.1.0
+ '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@types/react': 18.2.47
+ '@types/react-dom': 18.2.14
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ dev: false
+
+ /@radix-ui/react-tooltip@1.1.1(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-LLE8nzNE4MzPMw3O2zlVlkLFid3y9hMUs7uCbSHyKSo+tCN4yMCf+ZCCcfrYgsOC0TiHBPQ1mtpJ2liY3ZT3SQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@radix-ui/primitive': 1.1.0
+ '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-id': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-portal': 1.1.1(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-slot': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@radix-ui/react-visually-hidden': 1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1)
+ '@types/react': 18.2.47
+ '@types/react-dom': 18.2.14
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ dev: false
+
+ /@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.2.47)(react@18.3.1):
+ resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@types/react': 18.2.47
+ react: 18.3.1
+ dev: false
+
+ /@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.2.47)(react@18.3.1):
+ resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@types/react': 18.2.47
+ react: 18.3.1
+ dev: false
+
+ /@radix-ui/react-use-escape-keydown@1.1.0(@types/react@18.2.47)(react@18.3.1):
+ resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@types/react': 18.2.47
+ react: 18.3.1
+ dev: false
+
+ /@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.2.47)(react@18.3.1):
+ resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@types/react': 18.2.47
+ react: 18.3.1
+ dev: false
+
+ /@radix-ui/react-use-rect@1.1.0(@types/react@18.2.47)(react@18.3.1):
+ resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@radix-ui/rect': 1.1.0
+ '@types/react': 18.2.47
+ react: 18.3.1
+ dev: false
+
+ /@radix-ui/react-use-size@1.1.0(@types/react@18.2.47)(react@18.3.1):
+ resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@types/react': 18.2.47
+ react: 18.3.1
+ dev: false
+
+ /@radix-ui/react-visually-hidden@1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1)
+ '@types/react': 18.2.47
+ '@types/react-dom': 18.2.14
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ dev: false
+
+ /@radix-ui/rect@1.1.0:
+ resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==}
+ dev: false
+
+ /@react-email/body@0.0.8(react@18.3.1):
+ resolution: {integrity: sha512-gqdkNYlIaIw0OdpWu8KjIcQSIFvx7t2bZpXVxMMvBS859Ia1+1X3b5RNbjI3S1ZqLddUf7owOHkO4MiXGE+nxg==}
+ peerDependencies:
+ react: ^18.2.0
+ dependencies:
+ react: 18.3.1
+ dev: false
+
+ /@react-email/button@0.0.15(react@18.3.1):
+ resolution: {integrity: sha512-9Zi6SO3E8PoHYDfcJTecImiHLyitYWmIRs0HE3Ogra60ZzlWP2EXu+AZqwQnhXuq+9pbgwBWNWxB5YPetNPTNA==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: ^18.2.0
+ dependencies:
+ react: 18.3.1
+ dev: false
+
+ /@react-email/code-block@0.0.5(react@18.3.1):
+ resolution: {integrity: sha512-mmInpZsSIkNaYC1y40/S0XXrIqbTzrpllP6J1JMJuDOBG8l5T7pNl4V+gwfsSTvy9hVsuzQFmhHK8kVb1UXv3A==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: ^18.2.0
+ dependencies:
+ prismjs: 1.29.0
+ react: 18.3.1
+ dev: false
+
+ /@react-email/code-inline@0.0.2(react@18.3.1):
+ resolution: {integrity: sha512-0cmgbbibFeOJl0q04K9jJlPDuJ+SEiX/OG6m3Ko7UOkG3TqjRD8Dtvkij6jNDVfUh/zESpqJCP2CxrCLLMUjdA==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: ^18.2.0
+ dependencies:
+ react: 18.3.1
+ dev: false
+
+ /@react-email/column@0.0.10(react@18.3.1):
+ resolution: {integrity: sha512-MnP8Mnwipr0X3XtdD6jMLckb0sI5/IlS6Kl/2F6/rsSWBJy5Gg6nizlekTdkwDmy0kNSe3/1nGU0Zqo98pl63Q==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: ^18.2.0
+ dependencies:
+ react: 18.3.1
+ dev: false
+
+ /@react-email/components@0.0.21(@types/react@18.2.33)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-fwGfH7FF+iuq+IdPcbEO5HoF0Pakk9big+fFW9+3kiyvbSNuo8Io1rhPTMLd8q41XomN4g7mgWovdAeS/8PHrA==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: ^18.2.0
+ dependencies:
+ '@react-email/body': 0.0.8(react@18.3.1)
+ '@react-email/button': 0.0.15(react@18.3.1)
+ '@react-email/code-block': 0.0.5(react@18.3.1)
+ '@react-email/code-inline': 0.0.2(react@18.3.1)
+ '@react-email/column': 0.0.10(react@18.3.1)
+ '@react-email/container': 0.0.12(react@18.3.1)
+ '@react-email/font': 0.0.6(react@18.3.1)
+ '@react-email/head': 0.0.9(react@18.3.1)
+ '@react-email/heading': 0.0.12(@types/react@18.2.33)(react@18.3.1)
+ '@react-email/hr': 0.0.8(react@18.3.1)
+ '@react-email/html': 0.0.8(react@18.3.1)
+ '@react-email/img': 0.0.8(react@18.3.1)
+ '@react-email/link': 0.0.8(react@18.3.1)
+ '@react-email/markdown': 0.0.10(react@18.3.1)
+ '@react-email/preview': 0.0.9(react@18.3.1)
+ '@react-email/render': 0.0.16(react-dom@18.3.1)(react@18.3.1)
+ '@react-email/row': 0.0.8(react@18.3.1)
+ '@react-email/section': 0.0.12(react@18.3.1)
+ '@react-email/tailwind': 0.0.18(react@18.3.1)
+ '@react-email/text': 0.0.8(react@18.3.1)
+ react: 18.3.1
+ transitivePeerDependencies:
+ - '@types/react'
+ - react-dom
+ dev: false
+
+ /@react-email/container@0.0.12(react@18.3.1):
+ resolution: {integrity: sha512-HFu8Pu5COPFfeZxSL+wKv/TV5uO/sp4zQ0XkRCdnGkj/xoq0lqOHVDL4yC2Pu6fxXF/9C3PHDA++5uEYV5WVJw==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: ^18.2.0
+ dependencies:
+ react: 18.3.1
+ dev: false
+
+ /@react-email/font@0.0.6(react@18.3.1):
+ resolution: {integrity: sha512-sZZFvEZ4U3vNCAZ8wXqIO3DuGJR2qE/8m2fEH+tdqwa532zGO3zW+UlCTg0b9455wkJSzEBeaWik0IkNvjXzxw==}
+ peerDependencies:
+ react: ^18.2.0
+ dependencies:
+ react: 18.3.1
+ dev: false
+
+ /@react-email/head@0.0.9(react@18.3.1):
+ resolution: {integrity: sha512-dF3Uv1qy3oh+IU2atXdv5Xk0hk2udOlMb1A/MNGngC0eHyoEV9ThA0XvhN7mm5x9dDLkVamoWUKXDtmkiuSRqQ==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: ^18.2.0
+ dependencies:
+ react: 18.3.1
+ dev: false
+
+ /@react-email/heading@0.0.12(@types/react@18.2.33)(react@18.3.1):
+ resolution: {integrity: sha512-eB7mpnAvDmwvQLoPuwEiPRH4fPXWe6ltz6Ptbry2BlI88F0a2k11Ghb4+sZHBqg7vVw/MKbqEgtLqr3QJ/KfCQ==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: ^18.2.0
+ dependencies:
+ '@radix-ui/react-slot': 1.0.2(@types/react@18.2.33)(react@18.3.1)
+ react: 18.3.1
+ transitivePeerDependencies:
+ - '@types/react'
+ dev: false
+
+ /@react-email/hr@0.0.8(react@18.3.1):
+ resolution: {integrity: sha512-JLVvpCg2wYKEB+n/PGCggWG9fRU5e4lxsGdpK5SDLsCL0ic3OLKSpHMfeE+ZSuw0GixAVVQN7F64PVJHQkd4MQ==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: ^18.2.0
+ dependencies:
+ react: 18.3.1
+ dev: false
+
+ /@react-email/html@0.0.8(react@18.3.1):
+ resolution: {integrity: sha512-arII3wBNLpeJtwyIJXPaILm5BPKhA+nvdC1F9QkuKcOBJv2zXctn8XzPqyGqDfdplV692ulNJP7XY55YqbKp6w==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: ^18.2.0
+ dependencies:
+ react: 18.3.1
+ dev: false
+
+ /@react-email/img@0.0.8(react@18.3.1):
+ resolution: {integrity: sha512-jx/rPuKo31tV18fu7P5rRqelaH5wkhg83Dq7uLwJpfqhbi4KFBGeBfD0Y3PiLPPoh+WvYf+Adv9W2ghNW8nOMQ==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: ^18.2.0
+ dependencies:
+ react: 18.3.1
+ dev: false
+
+ /@react-email/link@0.0.8(react@18.3.1):
+ resolution: {integrity: sha512-nVikuTi8WJHa6Baad4VuRUbUCa/7EtZ1Qy73TRejaCHn+vhetc39XGqHzKLNh+Z/JFL8Hv9g+4AgG16o2R0ogQ==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: ^18.2.0
+ dependencies:
+ react: 18.3.1
+ dev: false
+
+ /@react-email/markdown@0.0.10(react@18.3.1):
+ resolution: {integrity: sha512-MH0xO+NJ4IuJcx9nyxbgGKAMXyudFjCZ0A2GQvuWajemW9qy2hgnJ3mW3/z5lwcenG+JPn7JyO/iZpizQ7u1tA==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: ^18.2.0
+ dependencies:
+ md-to-react-email: 5.0.2(react@18.3.1)
+ react: 18.3.1
+ dev: false
+
+ /@react-email/preview@0.0.9(react@18.3.1):
+ resolution: {integrity: sha512-2fyAA/zzZYfYmxfyn3p2YOIU30klyA6Dq4ytyWq4nfzQWWglt5hNDE0cMhObvRtfjM9ghMSVtoELAb0MWiF/kw==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: ^18.2.0
+ dependencies:
+ react: 18.3.1
+ dev: false
+
+ /@react-email/render@0.0.16(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-wDaMy27xAq1cJHtSFptp0DTKPuV2GYhloqia95ub/DH9Dea1aWYsbdM918MOc/b/HvVS3w1z8DWzfAk13bGStQ==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: ^18.2.0
+ react-dom: ^18.2.0
+ dependencies:
+ html-to-text: 9.0.5
+ js-beautify: 1.15.1
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ react-promise-suspense: 0.3.4
+ dev: false
+
+ /@react-email/row@0.0.8(react@18.3.1):
+ resolution: {integrity: sha512-JsB6pxs/ZyjYpEML3nbwJRGAerjcN/Pa/QG48XUwnT/MioDWrUuyQuefw+CwCrSUZ2P1IDrv2tUD3/E3xzcoKw==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: ^18.2.0
+ dependencies:
+ react: 18.3.1
+ dev: false
+
+ /@react-email/section@0.0.12(react@18.3.1):
+ resolution: {integrity: sha512-UCD/N/BeOTN4h3VZBUaFdiSem6HnpuxD1Q51TdBFnqeNqS5hBomp8LWJJ9s4gzwHWk1XPdNfLA3I/fJwulJshg==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: ^18.2.0
+ dependencies:
+ react: 18.3.1
+ dev: false
+
+ /@react-email/tailwind@0.0.18(react@18.3.1):
+ resolution: {integrity: sha512-ob8CXX/Pqq1U8YfL5OJTL48WJkixizyoXMMRYTiDLDN9LVLU7lSLtcK9kOD9CgFbO2yUPQr7/5+7gnQJ+cXa8Q==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: ^18.2.0
+ dependencies:
+ react: 18.3.1
+ dev: false
+
+ /@react-email/text@0.0.8(react@18.3.1):
+ resolution: {integrity: sha512-uvN2TNWMrfC9wv/LLmMLbbEN1GrMWZb9dBK14eYxHHAEHCeyvGb5ePZZ2MPyzO7Y5yTC+vFEnCEr76V+hWMxCQ==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: ^18.2.0
+ dependencies:
+ react: 18.3.1
+ dev: false
+
+ /@selderee/plugin-htmlparser2@0.11.0:
+ resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
+ dependencies:
+ domhandler: 5.0.3
+ selderee: 0.11.0
+ dev: false
+
+ /@socket.io/component-emitter@3.1.2:
+ resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==}
+ dev: false
+
+ /@swc/core-darwin-arm64@1.3.101:
+ resolution: {integrity: sha512-mNFK+uHNPRXSnfTOG34zJOeMl2waM4hF4a2NY7dkMXrPqw9CoJn4MwTXJcyMiSz1/BnNjjTCHF3Yhj0jPxmkzQ==}
+ engines: {node: '>=10'}
+ cpu: [arm64]
+ os: [darwin]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@swc/core-darwin-x64@1.3.101:
+ resolution: {integrity: sha512-B085j8XOx73Fg15KsHvzYWG262bRweGr3JooO1aW5ec5pYbz5Ew9VS5JKYS03w2UBSxf2maWdbPz2UFAxg0whw==}
+ engines: {node: '>=10'}
+ cpu: [x64]
+ os: [darwin]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@swc/core-linux-arm-gnueabihf@1.3.101:
+ resolution: {integrity: sha512-9xLKRb6zSzRGPqdz52Hy5GuB1lSjmLqa0lST6MTFads3apmx4Vgs8Y5NuGhx/h2I8QM4jXdLbpqQlifpzTlSSw==}
+ engines: {node: '>=10'}
+ cpu: [arm]
+ os: [linux]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@swc/core-linux-arm64-gnu@1.3.101:
+ resolution: {integrity: sha512-oE+r1lo7g/vs96Weh2R5l971dt+ZLuhaUX+n3BfDdPxNHfObXgKMjO7E+QS5RbGjv/AwiPCxQmbdCp/xN5ICJA==}
+ engines: {node: '>=10'}
+ cpu: [arm64]
+ os: [linux]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@swc/core-linux-arm64-musl@1.3.101:
+ resolution: {integrity: sha512-OGjYG3H4BMOTnJWJyBIovCez6KiHF30zMIu4+lGJTCrxRI2fAjGLml3PEXj8tC3FMcud7U2WUn6TdG0/te2k6g==}
+ engines: {node: '>=10'}
+ cpu: [arm64]
+ os: [linux]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@swc/core-linux-x64-gnu@1.3.101:
+ resolution: {integrity: sha512-/kBMcoF12PRO/lwa8Z7w4YyiKDcXQEiLvM+S3G9EvkoKYGgkkz4Q6PSNhF5rwg/E3+Hq5/9D2R+6nrkF287ihg==}
+ engines: {node: '>=10'}
+ cpu: [x64]
+ os: [linux]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@swc/core-linux-x64-musl@1.3.101:
+ resolution: {integrity: sha512-kDN8lm4Eew0u1p+h1l3JzoeGgZPQ05qDE0czngnjmfpsH2sOZxVj1hdiCwS5lArpy7ktaLu5JdRnx70MkUzhXw==}
+ engines: {node: '>=10'}
+ cpu: [x64]
+ os: [linux]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@swc/core-win32-arm64-msvc@1.3.101:
+ resolution: {integrity: sha512-9Wn8TTLWwJKw63K/S+jjrZb9yoJfJwCE2RV5vPCCWmlMf3U1AXj5XuWOLUX+Rp2sGKau7wZKsvywhheWm+qndQ==}
+ engines: {node: '>=10'}
+ cpu: [arm64]
+ os: [win32]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@swc/core-win32-ia32-msvc@1.3.101:
+ resolution: {integrity: sha512-onO5KvICRVlu2xmr4//V2je9O2XgS1SGKpbX206KmmjcJhXN5EYLSxW9qgg+kgV5mip+sKTHTAu7IkzkAtElYA==}
+ engines: {node: '>=10'}
+ cpu: [ia32]
+ os: [win32]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@swc/core-win32-x64-msvc@1.3.101:
+ resolution: {integrity: sha512-T3GeJtNQV00YmiVw/88/nxJ/H43CJvFnpvBHCVn17xbahiVUOPOduh3rc9LgAkKiNt/aV8vU3OJR+6PhfMR7UQ==}
+ engines: {node: '>=10'}
+ cpu: [x64]
+ os: [win32]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@swc/core@1.3.101:
+ resolution: {integrity: sha512-w5aQ9qYsd/IYmXADAnkXPGDMTqkQalIi+kfFf/MHRKTpaOL7DHjMXwPp/n8hJ0qNjRvchzmPtOqtPBiER50d8A==}
+ engines: {node: '>=10'}
+ requiresBuild: true
+ peerDependencies:
+ '@swc/helpers': ^0.5.0
+ peerDependenciesMeta:
+ '@swc/helpers':
+ optional: true
+ dependencies:
+ '@swc/counter': 0.1.3
+ '@swc/types': 0.1.9
+ optionalDependencies:
+ '@swc/core-darwin-arm64': 1.3.101
+ '@swc/core-darwin-x64': 1.3.101
+ '@swc/core-linux-arm-gnueabihf': 1.3.101
+ '@swc/core-linux-arm64-gnu': 1.3.101
+ '@swc/core-linux-arm64-musl': 1.3.101
+ '@swc/core-linux-x64-gnu': 1.3.101
+ '@swc/core-linux-x64-musl': 1.3.101
+ '@swc/core-win32-arm64-msvc': 1.3.101
+ '@swc/core-win32-ia32-msvc': 1.3.101
+ '@swc/core-win32-x64-msvc': 1.3.101
+ dev: false
+
+ /@swc/counter@0.1.3:
+ resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
+ dev: false
+
+ /@swc/helpers@0.5.2:
+ resolution: {integrity: sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==}
+ dependencies:
+ tslib: 2.6.3
+ dev: false
+
+ /@swc/types@0.1.9:
+ resolution: {integrity: sha512-qKnCno++jzcJ4lM4NTfYifm1EFSCeIfKiAHAfkENZAV5Kl9PjJIyd2yeeVv6c/2CckuLyv2NmRC5pv6pm2WQBg==}
+ dependencies:
+ '@swc/counter': 0.1.3
+ dev: false
+
+ /@types/cookie@0.4.1:
+ resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==}
+ dev: false
+
+ /@types/cors@2.8.17:
+ resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==}
+ dependencies:
+ '@types/node': 20.14.10
+ dev: false
+
+ /@types/eslint-scope@3.7.7:
+ resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==}
+ dependencies:
+ '@types/eslint': 8.56.10
+ '@types/estree': 1.0.5
+ dev: false
+
+ /@types/eslint@8.56.10:
+ resolution: {integrity: sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==}
+ dependencies:
+ '@types/estree': 1.0.5
+ '@types/json-schema': 7.0.15
+ dev: false
+
+ /@types/estree@1.0.5:
+ resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
+ dev: false
+
+ /@types/json-schema@7.0.15:
+ resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
+ dev: false
+
+ /@types/node@20.14.10:
+ resolution: {integrity: sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==}
+ dependencies:
+ undici-types: 5.26.5
+ dev: false
+
+ /@types/prismjs@1.26.4:
+ resolution: {integrity: sha512-rlAnzkW2sZOjbqZ743IHUhFcvzaGbqijwOu8QZnZCjfQzBqFE3s4lOTJEsxikImav9uzz/42I+O7YUs1mWgMlg==}
+ dev: false
+
+ /@types/prop-types@15.7.12:
+ resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==}
+
+ /@types/react-dom@18.2.14:
+ resolution: {integrity: sha512-V835xgdSVmyQmI1KLV2BEIUgqEuinxp9O4G6g3FqO/SqLac049E53aysv0oEFD2kHfejeKU+ZqL2bcFWj9gLAQ==}
+ dependencies:
+ '@types/react': 18.2.33
+
+ /@types/react@18.2.33:
+ resolution: {integrity: sha512-v+I7S+hu3PIBoVkKGpSYYpiBT1ijqEzWpzQD62/jm4K74hPpSP7FF9BnKG6+fg2+62weJYkkBWDJlZt5JO/9hg==}
+ dependencies:
+ '@types/prop-types': 15.7.12
+ '@types/scheduler': 0.23.0
+ csstype: 3.1.3
+
+ /@types/react@18.2.47:
+ resolution: {integrity: sha512-xquNkkOirwyCgoClNk85BjP+aqnIS+ckAJ8i37gAbDs14jfW/J23f2GItAf33oiUPQnqNMALiFeoM9Y5mbjpVQ==}
+ dependencies:
+ '@types/prop-types': 15.7.12
+ '@types/scheduler': 0.23.0
+ csstype: 3.1.3
+ dev: false
+
+ /@types/scheduler@0.23.0:
+ resolution: {integrity: sha512-YIoDCTH3Af6XM5VuwGG/QL/CJqga1Zm3NkU3HZ4ZHK2fRMPYP1VczsTUqtsf43PH/iJNVlPHAo2oWX7BSdB2Hw==}
+
+ /@types/webpack@5.28.5(@swc/core@1.3.101)(esbuild@0.19.11):
+ resolution: {integrity: sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==}
+ dependencies:
+ '@types/node': 20.14.10
+ tapable: 2.2.1
+ webpack: 5.93.0(@swc/core@1.3.101)(esbuild@0.19.11)
+ transitivePeerDependencies:
+ - '@swc/core'
+ - esbuild
+ - uglify-js
+ - webpack-cli
+ dev: false
+
+ /@webassemblyjs/ast@1.12.1:
+ resolution: {integrity: sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==}
+ dependencies:
+ '@webassemblyjs/helper-numbers': 1.11.6
+ '@webassemblyjs/helper-wasm-bytecode': 1.11.6
+ dev: false
+
+ /@webassemblyjs/floating-point-hex-parser@1.11.6:
+ resolution: {integrity: sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==}
+ dev: false
+
+ /@webassemblyjs/helper-api-error@1.11.6:
+ resolution: {integrity: sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==}
+ dev: false
+
+ /@webassemblyjs/helper-buffer@1.12.1:
+ resolution: {integrity: sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==}
+ dev: false
+
+ /@webassemblyjs/helper-numbers@1.11.6:
+ resolution: {integrity: sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==}
+ dependencies:
+ '@webassemblyjs/floating-point-hex-parser': 1.11.6
+ '@webassemblyjs/helper-api-error': 1.11.6
+ '@xtuc/long': 4.2.2
+ dev: false
+
+ /@webassemblyjs/helper-wasm-bytecode@1.11.6:
+ resolution: {integrity: sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==}
+ dev: false
+
+ /@webassemblyjs/helper-wasm-section@1.12.1:
+ resolution: {integrity: sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==}
+ dependencies:
+ '@webassemblyjs/ast': 1.12.1
+ '@webassemblyjs/helper-buffer': 1.12.1
+ '@webassemblyjs/helper-wasm-bytecode': 1.11.6
+ '@webassemblyjs/wasm-gen': 1.12.1
+ dev: false
+
+ /@webassemblyjs/ieee754@1.11.6:
+ resolution: {integrity: sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==}
+ dependencies:
+ '@xtuc/ieee754': 1.2.0
+ dev: false
+
+ /@webassemblyjs/leb128@1.11.6:
+ resolution: {integrity: sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==}
+ dependencies:
+ '@xtuc/long': 4.2.2
+ dev: false
+
+ /@webassemblyjs/utf8@1.11.6:
+ resolution: {integrity: sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==}
+ dev: false
+
+ /@webassemblyjs/wasm-edit@1.12.1:
+ resolution: {integrity: sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==}
+ dependencies:
+ '@webassemblyjs/ast': 1.12.1
+ '@webassemblyjs/helper-buffer': 1.12.1
+ '@webassemblyjs/helper-wasm-bytecode': 1.11.6
+ '@webassemblyjs/helper-wasm-section': 1.12.1
+ '@webassemblyjs/wasm-gen': 1.12.1
+ '@webassemblyjs/wasm-opt': 1.12.1
+ '@webassemblyjs/wasm-parser': 1.12.1
+ '@webassemblyjs/wast-printer': 1.12.1
+ dev: false
+
+ /@webassemblyjs/wasm-gen@1.12.1:
+ resolution: {integrity: sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==}
+ dependencies:
+ '@webassemblyjs/ast': 1.12.1
+ '@webassemblyjs/helper-wasm-bytecode': 1.11.6
+ '@webassemblyjs/ieee754': 1.11.6
+ '@webassemblyjs/leb128': 1.11.6
+ '@webassemblyjs/utf8': 1.11.6
+ dev: false
+
+ /@webassemblyjs/wasm-opt@1.12.1:
+ resolution: {integrity: sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==}
+ dependencies:
+ '@webassemblyjs/ast': 1.12.1
+ '@webassemblyjs/helper-buffer': 1.12.1
+ '@webassemblyjs/wasm-gen': 1.12.1
+ '@webassemblyjs/wasm-parser': 1.12.1
+ dev: false
+
+ /@webassemblyjs/wasm-parser@1.12.1:
+ resolution: {integrity: sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==}
+ dependencies:
+ '@webassemblyjs/ast': 1.12.1
+ '@webassemblyjs/helper-api-error': 1.11.6
+ '@webassemblyjs/helper-wasm-bytecode': 1.11.6
+ '@webassemblyjs/ieee754': 1.11.6
+ '@webassemblyjs/leb128': 1.11.6
+ '@webassemblyjs/utf8': 1.11.6
+ dev: false
+
+ /@webassemblyjs/wast-printer@1.12.1:
+ resolution: {integrity: sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==}
+ dependencies:
+ '@webassemblyjs/ast': 1.12.1
+ '@xtuc/long': 4.2.2
+ dev: false
+
+ /@xtuc/ieee754@1.2.0:
+ resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==}
+ dev: false
+
+ /@xtuc/long@4.2.2:
+ resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==}
+ dev: false
+
+ /abbrev@2.0.0:
+ resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==}
+ engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+ dev: false
+
+ /accepts@1.3.8:
+ resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
+ engines: {node: '>= 0.6'}
+ dependencies:
+ mime-types: 2.1.35
+ negotiator: 0.6.3
+ dev: false
+
+ /acorn-import-attributes@1.9.5(acorn@8.12.1):
+ resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==}
+ peerDependencies:
+ acorn: ^8
+ dependencies:
+ acorn: 8.12.1
+ dev: false
+
+ /acorn-jsx@5.3.2(acorn@8.12.1):
+ resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
+ peerDependencies:
+ acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+ dependencies:
+ acorn: 8.12.1
+ dev: false
+
+ /acorn@8.12.1:
+ resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+ dev: false
+
+ /ajv-keywords@3.5.2(ajv@6.12.6):
+ resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==}
+ peerDependencies:
+ ajv: ^6.9.1
+ dependencies:
+ ajv: 6.12.6
+ dev: false
+
+ /ajv@6.12.6:
+ resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-json-stable-stringify: 2.1.0
+ json-schema-traverse: 0.4.1
+ uri-js: 4.4.1
+ dev: false
+
+ /ansi-regex@5.0.1:
+ resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
+ engines: {node: '>=8'}
+ dev: false
+
+ /ansi-regex@6.0.1:
+ resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==}
+ engines: {node: '>=12'}
+ dev: false
+
+ /ansi-styles@3.2.1:
+ resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
+ engines: {node: '>=4'}
+ dependencies:
+ color-convert: 1.9.3
+ dev: false
+
+ /ansi-styles@4.3.0:
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+ engines: {node: '>=8'}
+ dependencies:
+ color-convert: 2.0.1
+ dev: false
+
+ /ansi-styles@6.2.1:
+ resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
+ engines: {node: '>=12'}
+ dev: false
+
+ /any-promise@1.3.0:
+ resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
+ dev: false
+
+ /anymatch@3.1.3:
+ resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
+ engines: {node: '>= 8'}
+ dependencies:
+ normalize-path: 3.0.0
+ picomatch: 2.3.1
+ dev: false
+
+ /arg@5.0.2:
+ resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
+ dev: false
+
+ /argparse@2.0.1:
+ resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+ dev: false
+
+ /aria-hidden@1.2.4:
+ resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==}
+ engines: {node: '>=10'}
+ dependencies:
+ tslib: 2.6.3
+ dev: false
+
+ /autoprefixer@10.4.14(postcss@8.4.38):
+ resolution: {integrity: sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==}
+ engines: {node: ^10 || ^12 || >=14}
+ hasBin: true
+ peerDependencies:
+ postcss: ^8.1.0
+ dependencies:
+ browserslist: 4.23.2
+ caniuse-lite: 1.0.30001642
+ fraction.js: 4.3.7
+ normalize-range: 0.1.2
+ picocolors: 1.0.1
+ postcss: 8.4.38
+ postcss-value-parser: 4.2.0
+ dev: false
+
+ /balanced-match@1.0.2:
+ resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+ dev: false
+
+ /base64-js@1.5.1:
+ resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
+ dev: false
+
+ /base64id@2.0.0:
+ resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==}
+ engines: {node: ^4.5.0 || >= 5.9}
+ dev: false
+
+ /binary-extensions@2.3.0:
+ resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
+ engines: {node: '>=8'}
+ dev: false
+
+ /bl@4.1.0:
+ resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
+ dependencies:
+ buffer: 5.7.1
+ inherits: 2.0.4
+ readable-stream: 3.6.2
+ dev: false
+
+ /brace-expansion@1.1.11:
+ resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
+ dependencies:
+ balanced-match: 1.0.2
+ concat-map: 0.0.1
+ dev: false
+
+ /brace-expansion@2.0.1:
+ resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
+ dependencies:
+ balanced-match: 1.0.2
+ dev: false
+
+ /braces@3.0.3:
+ resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
+ engines: {node: '>=8'}
+ dependencies:
+ fill-range: 7.1.1
+ dev: false
+
+ /browserslist@4.23.2:
+ resolution: {integrity: sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+ dependencies:
+ caniuse-lite: 1.0.30001642
+ electron-to-chromium: 1.4.827
+ node-releases: 2.0.14
+ update-browserslist-db: 1.1.0(browserslist@4.23.2)
+ dev: false
+
+ /buffer-from@1.1.2:
+ resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
+ dev: false
+
+ /buffer@5.7.1:
+ resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
+ dependencies:
+ base64-js: 1.5.1
+ ieee754: 1.2.1
+ dev: false
+
+ /busboy@1.6.0:
+ resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
+ engines: {node: '>=10.16.0'}
+ dependencies:
+ streamsearch: 1.1.0
+ dev: false
+
+ /callsites@3.1.0:
+ resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
+ engines: {node: '>=6'}
+ dev: false
+
+ /camelcase-css@2.0.1:
+ resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
+ engines: {node: '>= 6'}
+ dev: false
+
+ /caniuse-lite@1.0.30001642:
+ resolution: {integrity: sha512-3XQ0DoRgLijXJErLSl+bLnJ+Et4KqV1PY6JJBGAFlsNsz31zeAIncyeZfLCabHK/jtSh+671RM9YMldxjUPZtA==}
+ dev: false
+
+ /chalk@2.4.2:
+ resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
+ engines: {node: '>=4'}
+ dependencies:
+ ansi-styles: 3.2.1
+ escape-string-regexp: 1.0.5
+ supports-color: 5.5.0
+ dev: false
+
+ /chalk@4.1.2:
+ resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
+ engines: {node: '>=10'}
+ dependencies:
+ ansi-styles: 4.3.0
+ supports-color: 7.2.0
+ dev: false
+
+ /chokidar@3.5.3:
+ resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
+ engines: {node: '>= 8.10.0'}
+ dependencies:
+ anymatch: 3.1.3
+ braces: 3.0.3
+ glob-parent: 5.1.2
+ is-binary-path: 2.1.0
+ is-glob: 4.0.3
+ normalize-path: 3.0.0
+ readdirp: 3.6.0
+ optionalDependencies:
+ fsevents: 2.3.3
+ dev: false
+
+ /chrome-trace-event@1.0.4:
+ resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==}
+ engines: {node: '>=6.0'}
+ dev: false
+
+ /cli-cursor@3.1.0:
+ resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
+ engines: {node: '>=8'}
+ dependencies:
+ restore-cursor: 3.1.0
+ dev: false
+
+ /cli-spinners@2.9.2:
+ resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==}
+ engines: {node: '>=6'}
+ dev: false
+
+ /client-only@0.0.1:
+ resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
+ dev: false
+
+ /clone@1.0.4:
+ resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
+ engines: {node: '>=0.8'}
+ dev: false
+
+ /clsx@1.2.1:
+ resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==}
+ engines: {node: '>=6'}
+ dev: false
+
+ /clsx@2.1.0:
+ resolution: {integrity: sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==}
+ engines: {node: '>=6'}
+ dev: false
+
+ /color-convert@1.9.3:
+ resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
+ dependencies:
+ color-name: 1.1.3
+ dev: false
+
+ /color-convert@2.0.1:
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+ engines: {node: '>=7.0.0'}
+ dependencies:
+ color-name: 1.1.4
+ dev: false
+
+ /color-name@1.1.3:
+ resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
+ dev: false
+
+ /color-name@1.1.4:
+ resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+ dev: false
+
+ /commander@10.0.1:
+ resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==}
+ engines: {node: '>=14'}
+ dev: false
+
+ /commander@11.1.0:
+ resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==}
+ engines: {node: '>=16'}
+ dev: false
+
+ /commander@2.20.3:
+ resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
+ dev: false
+
+ /commander@4.1.1:
+ resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
+ engines: {node: '>= 6'}
+ dev: false
+
+ /concat-map@0.0.1:
+ resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+ dev: false
+
+ /config-chain@1.1.13:
+ resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==}
+ dependencies:
+ ini: 1.3.8
+ proto-list: 1.2.4
+ dev: false
+
+ /convert-source-map@2.0.0:
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+ dev: false
+
+ /cookie@0.4.2:
+ resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==}
+ engines: {node: '>= 0.6'}
+ dev: false
+
+ /cors@2.8.5:
+ resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
+ engines: {node: '>= 0.10'}
+ dependencies:
+ object-assign: 4.1.1
+ vary: 1.1.2
+ dev: false
+
+ /cross-spawn@7.0.3:
+ resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
+ engines: {node: '>= 8'}
+ dependencies:
+ path-key: 3.1.1
+ shebang-command: 2.0.0
+ which: 2.0.2
+ dev: false
+
+ /cssesc@3.0.0:
+ resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
+ engines: {node: '>=4'}
+ hasBin: true
+ dev: false
+
+ /csstype@3.1.3:
+ resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
+
+ /debounce@2.0.0:
+ resolution: {integrity: sha512-xRetU6gL1VJbs85Mc4FoEGSjQxzpdxRyFhe3lmWFyy2EzydIcD4xzUvRJMD+NPDfMwKNhxa3PvsIOU32luIWeA==}
+ engines: {node: '>=18'}
+ dev: false
+
+ /debug@4.3.5:
+ resolution: {integrity: sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+ dependencies:
+ ms: 2.1.2
+ dev: false
+
+ /deep-is@0.1.4:
+ resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+ dev: false
+
+ /deepmerge@4.3.1:
+ resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
+ engines: {node: '>=0.10.0'}
+ dev: false
+
+ /defaults@1.0.4:
+ resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==}
+ dependencies:
+ clone: 1.0.4
+ dev: false
+
+ /detect-node-es@1.1.0:
+ resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
+ dev: false
+
+ /didyoumean@1.2.2:
+ resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
+ dev: false
+
+ /dlv@1.1.3:
+ resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
+ dev: false
+
+ /dom-serializer@2.0.0:
+ resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
+ dependencies:
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+ entities: 4.5.0
+ dev: false
+
+ /domelementtype@2.3.0:
+ resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
+ dev: false
+
+ /domhandler@5.0.3:
+ resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
+ engines: {node: '>= 4'}
+ dependencies:
+ domelementtype: 2.3.0
+ dev: false
+
+ /domutils@3.1.0:
+ resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==}
+ dependencies:
+ dom-serializer: 2.0.0
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+ dev: false
+
+ /dotenv@16.0.3:
+ resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==}
+ engines: {node: '>=12'}
+ dev: false
+
+ /eastasianwidth@0.2.0:
+ resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
+ dev: false
+
+ /editorconfig@1.0.4:
+ resolution: {integrity: sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==}
+ engines: {node: '>=14'}
+ hasBin: true
+ dependencies:
+ '@one-ini/wasm': 0.1.1
+ commander: 10.0.1
+ minimatch: 9.0.1
+ semver: 7.6.2
+ dev: false
+
+ /electron-to-chromium@1.4.827:
+ resolution: {integrity: sha512-VY+J0e4SFcNfQy19MEoMdaIcZLmDCprqvBtkii1WTCTQHpRvf5N8+3kTYCgL/PcntvwQvmMJWTuDPsq+IlhWKQ==}
+ dev: false
+
+ /emoji-regex@8.0.0:
+ resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
+ dev: false
+
+ /emoji-regex@9.2.2:
+ resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
+ dev: false
+
+ /engine.io-client@6.5.4:
+ resolution: {integrity: sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==}
+ dependencies:
+ '@socket.io/component-emitter': 3.1.2
+ debug: 4.3.5
+ engine.io-parser: 5.2.3
+ ws: 8.17.1
+ xmlhttprequest-ssl: 2.0.0
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+ dev: false
+
+ /engine.io-parser@5.2.3:
+ resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==}
+ engines: {node: '>=10.0.0'}
+ dev: false
+
+ /engine.io@6.5.5:
+ resolution: {integrity: sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==}
+ engines: {node: '>=10.2.0'}
+ dependencies:
+ '@types/cookie': 0.4.1
+ '@types/cors': 2.8.17
+ '@types/node': 20.14.10
+ accepts: 1.3.8
+ base64id: 2.0.0
+ cookie: 0.4.2
+ cors: 2.8.5
+ debug: 4.3.5
+ engine.io-parser: 5.2.3
+ ws: 8.17.1
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+ dev: false
+
+ /enhanced-resolve@5.17.0:
+ resolution: {integrity: sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==}
+ engines: {node: '>=10.13.0'}
+ dependencies:
+ graceful-fs: 4.2.11
+ tapable: 2.2.1
+ dev: false
+
+ /entities@4.5.0:
+ resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
+ engines: {node: '>=0.12'}
+ dev: false
+
+ /es-module-lexer@1.5.4:
+ resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==}
+ dev: false
+
+ /esbuild@0.19.11:
+ resolution: {integrity: sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==}
+ engines: {node: '>=12'}
+ hasBin: true
+ requiresBuild: true
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.19.11
+ '@esbuild/android-arm': 0.19.11
+ '@esbuild/android-arm64': 0.19.11
+ '@esbuild/android-x64': 0.19.11
+ '@esbuild/darwin-arm64': 0.19.11
+ '@esbuild/darwin-x64': 0.19.11
+ '@esbuild/freebsd-arm64': 0.19.11
+ '@esbuild/freebsd-x64': 0.19.11
+ '@esbuild/linux-arm': 0.19.11
+ '@esbuild/linux-arm64': 0.19.11
+ '@esbuild/linux-ia32': 0.19.11
+ '@esbuild/linux-loong64': 0.19.11
+ '@esbuild/linux-mips64el': 0.19.11
+ '@esbuild/linux-ppc64': 0.19.11
+ '@esbuild/linux-riscv64': 0.19.11
+ '@esbuild/linux-s390x': 0.19.11
+ '@esbuild/linux-x64': 0.19.11
+ '@esbuild/netbsd-x64': 0.19.11
+ '@esbuild/openbsd-x64': 0.19.11
+ '@esbuild/sunos-x64': 0.19.11
+ '@esbuild/win32-arm64': 0.19.11
+ '@esbuild/win32-ia32': 0.19.11
+ '@esbuild/win32-x64': 0.19.11
+ dev: false
+
+ /escalade@3.1.2:
+ resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==}
+ engines: {node: '>=6'}
+ dev: false
+
+ /escape-string-regexp@1.0.5:
+ resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
+ engines: {node: '>=0.8.0'}
+ dev: false
+
+ /escape-string-regexp@4.0.0:
+ resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
+ engines: {node: '>=10'}
+ dev: false
+
+ /eslint-config-prettier@9.0.0(eslint@9.7.0):
+ resolution: {integrity: sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==}
+ hasBin: true
+ peerDependencies:
+ eslint: '>=7.0.0'
+ dependencies:
+ eslint: 9.7.0
+ dev: false
+
+ /eslint-config-turbo@1.10.12(eslint@9.7.0):
+ resolution: {integrity: sha512-z3jfh+D7UGYlzMWGh+Kqz++hf8LOE96q3o5R8X4HTjmxaBWlLAWG+0Ounr38h+JLR2TJno0hU9zfzoPNkR9BdA==}
+ peerDependencies:
+ eslint: '>6.6.0'
+ dependencies:
+ eslint: 9.7.0
+ eslint-plugin-turbo: 1.10.12(eslint@9.7.0)
+ dev: false
+
+ /eslint-plugin-turbo@1.10.12(eslint@9.7.0):
+ resolution: {integrity: sha512-uNbdj+ohZaYo4tFJ6dStRXu2FZigwulR1b3URPXe0Q8YaE7thuekKNP+54CHtZPH9Zey9dmDx5btAQl9mfzGOw==}
+ peerDependencies:
+ eslint: '>6.6.0'
+ dependencies:
+ dotenv: 16.0.3
+ eslint: 9.7.0
+ dev: false
+
+ /eslint-scope@5.1.1:
+ resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==}
+ engines: {node: '>=8.0.0'}
+ dependencies:
+ esrecurse: 4.3.0
+ estraverse: 4.3.0
+ dev: false
+
+ /eslint-scope@8.0.2:
+ resolution: {integrity: sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ dependencies:
+ esrecurse: 4.3.0
+ estraverse: 5.3.0
+ dev: false
+
+ /eslint-visitor-keys@3.4.3:
+ resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ dev: false
+
+ /eslint-visitor-keys@4.0.0:
+ resolution: {integrity: sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ dev: false
+
+ /eslint@9.7.0:
+ resolution: {integrity: sha512-FzJ9D/0nGiCGBf8UXO/IGLTgLVzIxze1zpfA8Ton2mjLovXdAPlYDv+MQDcqj3TmrhAGYfOpz9RfR+ent0AgAw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ hasBin: true
+ dependencies:
+ '@eslint-community/eslint-utils': 4.4.0(eslint@9.7.0)
+ '@eslint-community/regexpp': 4.11.0
+ '@eslint/config-array': 0.17.0
+ '@eslint/eslintrc': 3.1.0
+ '@eslint/js': 9.7.0
+ '@humanwhocodes/module-importer': 1.0.1
+ '@humanwhocodes/retry': 0.3.0
+ '@nodelib/fs.walk': 1.2.8
+ ajv: 6.12.6
+ chalk: 4.1.2
+ cross-spawn: 7.0.3
+ debug: 4.3.5
+ escape-string-regexp: 4.0.0
+ eslint-scope: 8.0.2
+ eslint-visitor-keys: 4.0.0
+ espree: 10.1.0
+ esquery: 1.6.0
+ esutils: 2.0.3
+ fast-deep-equal: 3.1.3
+ file-entry-cache: 8.0.0
+ find-up: 5.0.0
+ glob-parent: 6.0.2
+ ignore: 5.3.1
+ imurmurhash: 0.1.4
+ is-glob: 4.0.3
+ is-path-inside: 3.0.3
+ json-stable-stringify-without-jsonify: 1.0.1
+ levn: 0.4.1
+ lodash.merge: 4.6.2
+ minimatch: 3.1.2
+ natural-compare: 1.4.0
+ optionator: 0.9.4
+ strip-ansi: 6.0.1
+ text-table: 0.2.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
+ /espree@10.1.0:
+ resolution: {integrity: sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ dependencies:
+ acorn: 8.12.1
+ acorn-jsx: 5.3.2(acorn@8.12.1)
+ eslint-visitor-keys: 4.0.0
+ dev: false
+
+ /esquery@1.6.0:
+ resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==}
+ engines: {node: '>=0.10'}
+ dependencies:
+ estraverse: 5.3.0
+ dev: false
+
+ /esrecurse@4.3.0:
+ resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
+ engines: {node: '>=4.0'}
+ dependencies:
+ estraverse: 5.3.0
+ dev: false
+
+ /estraverse@4.3.0:
+ resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==}
+ engines: {node: '>=4.0'}
+ dev: false
+
+ /estraverse@5.3.0:
+ resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
+ engines: {node: '>=4.0'}
+ dev: false
+
+ /esutils@2.0.3:
+ resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
+ engines: {node: '>=0.10.0'}
+ dev: false
+
+ /events@3.3.0:
+ resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
+ engines: {node: '>=0.8.x'}
+ dev: false
+
+ /fast-deep-equal@2.0.1:
+ resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==}
+ dev: false
+
+ /fast-deep-equal@3.1.3:
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+ dev: false
+
+ /fast-glob@3.3.2:
+ resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
+ engines: {node: '>=8.6.0'}
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ '@nodelib/fs.walk': 1.2.8
+ glob-parent: 5.1.2
+ merge2: 1.4.1
+ micromatch: 4.0.7
+ dev: false
+
+ /fast-json-stable-stringify@2.1.0:
+ resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
+ dev: false
+
+ /fast-levenshtein@2.0.6:
+ resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+ dev: false
+
+ /fastq@1.17.1:
+ resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
+ dependencies:
+ reusify: 1.0.4
+ dev: false
+
+ /file-entry-cache@8.0.0:
+ resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
+ engines: {node: '>=16.0.0'}
+ dependencies:
+ flat-cache: 4.0.1
+ dev: false
+
+ /fill-range@7.1.1:
+ resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
+ engines: {node: '>=8'}
+ dependencies:
+ to-regex-range: 5.0.1
+ dev: false
+
+ /find-up@5.0.0:
+ resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
+ engines: {node: '>=10'}
+ dependencies:
+ locate-path: 6.0.0
+ path-exists: 4.0.0
+ dev: false
+
+ /flat-cache@4.0.1:
+ resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
+ engines: {node: '>=16'}
+ dependencies:
+ flatted: 3.3.1
+ keyv: 4.5.4
+ dev: false
+
+ /flatted@3.3.1:
+ resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==}
+ dev: false
+
+ /foreground-child@3.2.1:
+ resolution: {integrity: sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==}
+ engines: {node: '>=14'}
+ dependencies:
+ cross-spawn: 7.0.3
+ signal-exit: 4.1.0
+ dev: false
+
+ /fraction.js@4.3.7:
+ resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
+ dev: false
+
+ /framer-motion@10.17.4(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-CYBSs6cWfzcasAX8aofgKFZootmkQtR4qxbfTOksBLny/lbUfkGbQAFOS3qnl6Uau1N9y8tUpI7mVIrHgkFjLQ==}
+ peerDependencies:
+ react: ^18.0.0
+ react-dom: ^18.0.0
+ peerDependenciesMeta:
+ react:
+ optional: true
+ react-dom:
+ optional: true
+ dependencies:
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ tslib: 2.6.3
+ optionalDependencies:
+ '@emotion/is-prop-valid': 0.8.8
+ dev: false
+
+ /fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /function-bind@1.1.2:
+ resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+ dev: false
+
+ /gensync@1.0.0-beta.2:
+ resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
+ engines: {node: '>=6.9.0'}
+ dev: false
+
+ /get-nonce@1.0.1:
+ resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
+ engines: {node: '>=6'}
+ dev: false
+
+ /glob-parent@5.1.2:
+ resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
+ engines: {node: '>= 6'}
+ dependencies:
+ is-glob: 4.0.3
+ dev: false
+
+ /glob-parent@6.0.2:
+ resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
+ engines: {node: '>=10.13.0'}
+ dependencies:
+ is-glob: 4.0.3
+ dev: false
+
+ /glob-to-regexp@0.4.1:
+ resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
+ dev: false
+
+ /glob@10.3.4:
+ resolution: {integrity: sha512-6LFElP3A+i/Q8XQKEvZjkEWEOTgAIALR9AO2rwT8bgPhDd1anmqDJDZ6lLddI4ehxxxR1S5RIqKe1uapMQfYaQ==}
+ engines: {node: '>=16 || 14 >=14.17'}
+ hasBin: true
+ dependencies:
+ foreground-child: 3.2.1
+ jackspeak: 2.3.6
+ minimatch: 9.0.5
+ minipass: 7.1.2
+ path-scurry: 1.11.1
+ dev: false
+
+ /glob@10.4.5:
+ resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
+ hasBin: true
+ dependencies:
+ foreground-child: 3.2.1
+ jackspeak: 3.4.3
+ minimatch: 9.0.5
+ minipass: 7.1.2
+ package-json-from-dist: 1.0.0
+ path-scurry: 1.11.1
+ dev: false
+
+ /globals@11.12.0:
+ resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
+ engines: {node: '>=4'}
+ dev: false
+
+ /globals@14.0.0:
+ resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
+ engines: {node: '>=18'}
+ dev: false
+
+ /graceful-fs@4.2.11:
+ resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+ dev: false
+
+ /has-flag@3.0.0:
+ resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
+ engines: {node: '>=4'}
+ dev: false
+
+ /has-flag@4.0.0:
+ resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+ engines: {node: '>=8'}
+ dev: false
+
+ /hasown@2.0.2:
+ resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ function-bind: 1.1.2
+ dev: false
+
+ /html-to-text@9.0.5:
+ resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==}
+ engines: {node: '>=14'}
+ dependencies:
+ '@selderee/plugin-htmlparser2': 0.11.0
+ deepmerge: 4.3.1
+ dom-serializer: 2.0.0
+ htmlparser2: 8.0.2
+ selderee: 0.11.0
+ dev: false
+
+ /htmlparser2@8.0.2:
+ resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
+ dependencies:
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+ domutils: 3.1.0
+ entities: 4.5.0
+ dev: false
+
+ /ieee754@1.2.1:
+ resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
+ dev: false
+
+ /ignore@5.3.1:
+ resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==}
+ engines: {node: '>= 4'}
+ dev: false
+
+ /import-fresh@3.3.0:
+ resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
+ engines: {node: '>=6'}
+ dependencies:
+ parent-module: 1.0.1
+ resolve-from: 4.0.0
+ dev: false
+
+ /imurmurhash@0.1.4:
+ resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
+ engines: {node: '>=0.8.19'}
+ dev: false
+
+ /inherits@2.0.4:
+ resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+ dev: false
+
+ /ini@1.3.8:
+ resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
+ dev: false
+
+ /invariant@2.2.4:
+ resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
+ dependencies:
+ loose-envify: 1.4.0
+ dev: false
+
+ /is-binary-path@2.1.0:
+ resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
+ engines: {node: '>=8'}
+ dependencies:
+ binary-extensions: 2.3.0
+ dev: false
+
+ /is-core-module@2.14.0:
+ resolution: {integrity: sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ hasown: 2.0.2
+ dev: false
+
+ /is-extglob@2.1.1:
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+ engines: {node: '>=0.10.0'}
+ dev: false
+
+ /is-fullwidth-code-point@3.0.0:
+ resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
+ engines: {node: '>=8'}
+ dev: false
+
+ /is-glob@4.0.3:
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+ engines: {node: '>=0.10.0'}
+ dependencies:
+ is-extglob: 2.1.1
+ dev: false
+
+ /is-interactive@1.0.0:
+ resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==}
+ engines: {node: '>=8'}
+ dev: false
+
+ /is-number@7.0.0:
+ resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+ engines: {node: '>=0.12.0'}
+ dev: false
+
+ /is-path-inside@3.0.3:
+ resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==}
+ engines: {node: '>=8'}
+ dev: false
+
+ /is-unicode-supported@0.1.0:
+ resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==}
+ engines: {node: '>=10'}
+ dev: false
+
+ /isexe@2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+ dev: false
+
+ /jackspeak@2.3.6:
+ resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==}
+ engines: {node: '>=14'}
+ dependencies:
+ '@isaacs/cliui': 8.0.2
+ optionalDependencies:
+ '@pkgjs/parseargs': 0.11.0
+ dev: false
+
+ /jackspeak@3.4.3:
+ resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
+ dependencies:
+ '@isaacs/cliui': 8.0.2
+ optionalDependencies:
+ '@pkgjs/parseargs': 0.11.0
+ dev: false
+
+ /jest-worker@27.5.1:
+ resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==}
+ engines: {node: '>= 10.13.0'}
+ dependencies:
+ '@types/node': 20.14.10
+ merge-stream: 2.0.0
+ supports-color: 8.1.1
+ dev: false
+
+ /jiti@1.21.6:
+ resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==}
+ hasBin: true
+ dev: false
+
+ /js-beautify@1.15.1:
+ resolution: {integrity: sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==}
+ engines: {node: '>=14'}
+ hasBin: true
+ dependencies:
+ config-chain: 1.1.13
+ editorconfig: 1.0.4
+ glob: 10.4.5
+ js-cookie: 3.0.5
+ nopt: 7.2.1
+ dev: false
+
+ /js-cookie@3.0.5:
+ resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
+ engines: {node: '>=14'}
+ dev: false
+
+ /js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+ dev: false
+
+ /js-yaml@4.1.0:
+ resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
+ hasBin: true
+ dependencies:
+ argparse: 2.0.1
+ dev: false
+
+ /jsesc@2.5.2:
+ resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==}
+ engines: {node: '>=4'}
+ hasBin: true
+ dev: false
+
+ /json-buffer@3.0.1:
+ resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
+ dev: false
+
+ /json-parse-even-better-errors@2.3.1:
+ resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
+ dev: false
+
+ /json-schema-traverse@0.4.1:
+ resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
+ dev: false
+
+ /json-stable-stringify-without-jsonify@1.0.1:
+ resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
+ dev: false
+
+ /json5@2.2.3:
+ resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
+ engines: {node: '>=6'}
+ hasBin: true
+ dev: false
+
+ /keyv@4.5.4:
+ resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
+ dependencies:
+ json-buffer: 3.0.1
+ dev: false
+
+ /leac@0.6.0:
+ resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==}
+ dev: false
+
+ /levn@0.4.1:
+ resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
+ engines: {node: '>= 0.8.0'}
+ dependencies:
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+ dev: false
+
+ /lilconfig@2.1.0:
+ resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
+ engines: {node: '>=10'}
+ dev: false
+
+ /lilconfig@3.1.2:
+ resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==}
+ engines: {node: '>=14'}
+ dev: false
+
+ /lines-and-columns@1.2.4:
+ resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
+ dev: false
+
+ /loader-runner@4.3.0:
+ resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==}
+ engines: {node: '>=6.11.5'}
+ dev: false
+
+ /locate-path@6.0.0:
+ resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
+ engines: {node: '>=10'}
+ dependencies:
+ p-locate: 5.0.0
+ dev: false
+
+ /lodash.merge@4.6.2:
+ resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
+ dev: false
+
+ /log-symbols@4.1.0:
+ resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
+ engines: {node: '>=10'}
+ dependencies:
+ chalk: 4.1.2
+ is-unicode-supported: 0.1.0
+ dev: false
+
+ /loose-envify@1.4.0:
+ resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
+ hasBin: true
+ dependencies:
+ js-tokens: 4.0.0
+ dev: false
+
+ /lru-cache@10.4.3:
+ resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
+ dev: false
+
+ /lru-cache@5.1.1:
+ resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+ dependencies:
+ yallist: 3.1.1
+ dev: false
+
+ /marked@7.0.4:
+ resolution: {integrity: sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ==}
+ engines: {node: '>= 16'}
+ hasBin: true
+ dev: false
+
+ /md-to-react-email@5.0.2(react@18.3.1):
+ resolution: {integrity: sha512-x6kkpdzIzUhecda/yahltfEl53mH26QdWu4abUF9+S0Jgam8P//Ciro8cdhyMHnT5MQUJYrIbO6ORM2UxPiNNA==}
+ peerDependencies:
+ react: 18.x
+ dependencies:
+ marked: 7.0.4
+ react: 18.3.1
+ dev: false
+
+ /merge-stream@2.0.0:
+ resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
+ dev: false
+
+ /merge2@1.4.1:
+ resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
+ engines: {node: '>= 8'}
+ dev: false
+
+ /micromatch@4.0.7:
+ resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==}
+ engines: {node: '>=8.6'}
+ dependencies:
+ braces: 3.0.3
+ picomatch: 2.3.1
+ dev: false
+
+ /mime-db@1.52.0:
+ resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
+ engines: {node: '>= 0.6'}
+ dev: false
+
+ /mime-types@2.1.35:
+ resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
+ engines: {node: '>= 0.6'}
+ dependencies:
+ mime-db: 1.52.0
+ dev: false
+
+ /mimic-fn@2.1.0:
+ resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
+ engines: {node: '>=6'}
+ dev: false
+
+ /minimatch@3.1.2:
+ resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
+ dependencies:
+ brace-expansion: 1.1.11
+ dev: false
+
+ /minimatch@9.0.1:
+ resolution: {integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==}
+ engines: {node: '>=16 || 14 >=14.17'}
+ dependencies:
+ brace-expansion: 2.0.1
+ dev: false
+
+ /minimatch@9.0.5:
+ resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
+ engines: {node: '>=16 || 14 >=14.17'}
+ dependencies:
+ brace-expansion: 2.0.1
+ dev: false
+
+ /minipass@7.1.2:
+ resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
+ engines: {node: '>=16 || 14 >=14.17'}
+ dev: false
+
+ /ms@2.1.2:
+ resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
+ dev: false
+
+ /mz@2.7.0:
+ resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
+ dependencies:
+ any-promise: 1.3.0
+ object-assign: 4.1.1
+ thenify-all: 1.6.0
+ dev: false
+
+ /nanoid@3.3.7:
+ resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+ dev: false
+
+ /natural-compare@1.4.0:
+ resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+ dev: false
+
+ /negotiator@0.6.3:
+ resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
+ engines: {node: '>= 0.6'}
+ dev: false
+
+ /neo-async@2.6.2:
+ resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
+ dev: false
+
+ /next@14.1.4(@babel/core@7.24.5)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-1WTaXeSrUwlz/XcnhGTY7+8eiaFvdet5z9u3V2jb+Ek1vFo0VhHKSAIJvDWfQpttWjnyw14kBeq28TPq7bTeEQ==}
+ engines: {node: '>=18.17.0'}
+ hasBin: true
+ peerDependencies:
+ '@opentelemetry/api': ^1.1.0
+ react: ^18.2.0
+ react-dom: ^18.2.0
+ sass: ^1.3.0
+ peerDependenciesMeta:
+ '@opentelemetry/api':
+ optional: true
+ sass:
+ optional: true
+ dependencies:
+ '@next/env': 14.1.4
+ '@swc/helpers': 0.5.2
+ busboy: 1.6.0
+ caniuse-lite: 1.0.30001642
+ graceful-fs: 4.2.11
+ postcss: 8.4.31
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ styled-jsx: 5.1.1(@babel/core@7.24.5)(react@18.3.1)
+ optionalDependencies:
+ '@next/swc-darwin-arm64': 14.1.4
+ '@next/swc-darwin-x64': 14.1.4
+ '@next/swc-linux-arm64-gnu': 14.1.4
+ '@next/swc-linux-arm64-musl': 14.1.4
+ '@next/swc-linux-x64-gnu': 14.1.4
+ '@next/swc-linux-x64-musl': 14.1.4
+ '@next/swc-win32-arm64-msvc': 14.1.4
+ '@next/swc-win32-ia32-msvc': 14.1.4
+ '@next/swc-win32-x64-msvc': 14.1.4
+ transitivePeerDependencies:
+ - '@babel/core'
+ - babel-plugin-macros
+ dev: false
+
+ /node-releases@2.0.14:
+ resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==}
+ dev: false
+
+ /nopt@7.2.1:
+ resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==}
+ engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+ hasBin: true
+ dependencies:
+ abbrev: 2.0.0
+ dev: false
+
+ /normalize-path@3.0.0:
+ resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
+ engines: {node: '>=0.10.0'}
+ dev: false
+
+ /normalize-range@0.1.2:
+ resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
+ engines: {node: '>=0.10.0'}
+ dev: false
+
+ /object-assign@4.1.1:
+ resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
+ engines: {node: '>=0.10.0'}
+ dev: false
+
+ /object-hash@3.0.0:
+ resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
+ engines: {node: '>= 6'}
+ dev: false
+
+ /onetime@5.1.2:
+ resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
+ engines: {node: '>=6'}
+ dependencies:
+ mimic-fn: 2.1.0
+ dev: false
+
+ /optionator@0.9.4:
+ resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
+ engines: {node: '>= 0.8.0'}
+ dependencies:
+ deep-is: 0.1.4
+ fast-levenshtein: 2.0.6
+ levn: 0.4.1
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+ word-wrap: 1.2.5
+ dev: false
+
+ /ora@5.4.1:
+ resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==}
+ engines: {node: '>=10'}
+ dependencies:
+ bl: 4.1.0
+ chalk: 4.1.2
+ cli-cursor: 3.1.0
+ cli-spinners: 2.9.2
+ is-interactive: 1.0.0
+ is-unicode-supported: 0.1.0
+ log-symbols: 4.1.0
+ strip-ansi: 6.0.1
+ wcwidth: 1.0.1
+ dev: false
+
+ /p-limit@3.1.0:
+ resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
+ engines: {node: '>=10'}
+ dependencies:
+ yocto-queue: 0.1.0
+ dev: false
+
+ /p-locate@5.0.0:
+ resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
+ engines: {node: '>=10'}
+ dependencies:
+ p-limit: 3.1.0
+ dev: false
+
+ /package-json-from-dist@1.0.0:
+ resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==}
+ dev: false
+
+ /parent-module@1.0.1:
+ resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
+ engines: {node: '>=6'}
+ dependencies:
+ callsites: 3.1.0
+ dev: false
+
+ /parseley@0.12.1:
+ resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==}
+ dependencies:
+ leac: 0.6.0
+ peberminta: 0.9.0
+ dev: false
+
+ /path-exists@4.0.0:
+ resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
+ engines: {node: '>=8'}
+ dev: false
+
+ /path-key@3.1.1:
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+ engines: {node: '>=8'}
+ dev: false
+
+ /path-parse@1.0.7:
+ resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
+ dev: false
+
+ /path-scurry@1.11.1:
+ resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
+ engines: {node: '>=16 || 14 >=14.18'}
+ dependencies:
+ lru-cache: 10.4.3
+ minipass: 7.1.2
+ dev: false
+
+ /peberminta@0.9.0:
+ resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==}
+ dev: false
+
+ /picocolors@1.0.1:
+ resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==}
+ dev: false
+
+ /picomatch@2.3.1:
+ resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
+ engines: {node: '>=8.6'}
+ dev: false
+
+ /pify@2.3.0:
+ resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
+ engines: {node: '>=0.10.0'}
+ dev: false
+
+ /pirates@4.0.6:
+ resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
+ engines: {node: '>= 6'}
+ dev: false
+
+ /postcss-import@15.1.0(postcss@8.4.38):
+ resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
+ engines: {node: '>=14.0.0'}
+ peerDependencies:
+ postcss: ^8.0.0
+ dependencies:
+ postcss: 8.4.38
+ postcss-value-parser: 4.2.0
+ read-cache: 1.0.0
+ resolve: 1.22.8
+ dev: false
+
+ /postcss-js@4.0.1(postcss@8.4.38):
+ resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==}
+ engines: {node: ^12 || ^14 || >= 16}
+ peerDependencies:
+ postcss: ^8.4.21
+ dependencies:
+ camelcase-css: 2.0.1
+ postcss: 8.4.38
+ dev: false
+
+ /postcss-load-config@4.0.2(postcss@8.4.38):
+ resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==}
+ engines: {node: '>= 14'}
+ peerDependencies:
+ postcss: '>=8.0.9'
+ ts-node: '>=9.0.0'
+ peerDependenciesMeta:
+ postcss:
+ optional: true
+ ts-node:
+ optional: true
+ dependencies:
+ lilconfig: 3.1.2
+ postcss: 8.4.38
+ yaml: 2.4.5
+ dev: false
+
+ /postcss-nested@6.0.1(postcss@8.4.38):
+ resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==}
+ engines: {node: '>=12.0'}
+ peerDependencies:
+ postcss: ^8.2.14
+ dependencies:
+ postcss: 8.4.38
+ postcss-selector-parser: 6.1.1
+ dev: false
+
+ /postcss-selector-parser@6.1.1:
+ resolution: {integrity: sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==}
+ engines: {node: '>=4'}
+ dependencies:
+ cssesc: 3.0.0
+ util-deprecate: 1.0.2
+ dev: false
+
+ /postcss-value-parser@4.2.0:
+ resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
+ dev: false
+
+ /postcss@8.4.31:
+ resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
+ engines: {node: ^10 || ^12 || >=14}
+ dependencies:
+ nanoid: 3.3.7
+ picocolors: 1.0.1
+ source-map-js: 1.0.2
+ dev: false
+
+ /postcss@8.4.38:
+ resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==}
+ engines: {node: ^10 || ^12 || >=14}
+ dependencies:
+ nanoid: 3.3.7
+ picocolors: 1.0.1
+ source-map-js: 1.2.0
+ dev: false
+
+ /prelude-ls@1.2.1:
+ resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
+ engines: {node: '>= 0.8.0'}
+ dev: false
+
+ /prism-react-renderer@2.1.0(react@18.3.1):
+ resolution: {integrity: sha512-I5cvXHjA1PVGbGm1MsWCpvBCRrYyxEri0MC7/JbfIfYfcXAxHyO5PaUjs3A8H5GW6kJcLhTHxxMaOZZpRZD2iQ==}
+ peerDependencies:
+ react: '>=16.0.0'
+ dependencies:
+ '@types/prismjs': 1.26.4
+ clsx: 1.2.1
+ react: 18.3.1
+ dev: false
+
+ /prismjs@1.29.0:
+ resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==}
+ engines: {node: '>=6'}
+ dev: false
+
+ /proto-list@1.2.4:
+ resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==}
+ dev: false
+
+ /punycode@2.3.1:
+ resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+ engines: {node: '>=6'}
+ dev: false
+
+ /queue-microtask@1.2.3:
+ resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+ dev: false
+
+ /randombytes@2.1.0:
+ resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
+ dependencies:
+ safe-buffer: 5.2.1
+ dev: false
+
+ /react-dom@18.3.1(react@18.3.1):
+ resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
+ peerDependencies:
+ react: ^18.3.1
+ dependencies:
+ loose-envify: 1.4.0
+ react: 18.3.1
+ scheduler: 0.23.2
+ dev: false
+
+ /react-email@2.1.5(eslint@9.7.0):
+ resolution: {integrity: sha512-SjGt5XiqNwrC6FT0rAxERj0MC9binUOVZDzspAxcRHpxjZavvePAHvV29uROWNQ1Ha7ssg1sfy4dTQi7bjCXrg==}
+ engines: {node: '>=18.0.0'}
+ hasBin: true
+ dependencies:
+ '@babel/core': 7.24.5
+ '@babel/parser': 7.24.5
+ '@radix-ui/colors': 1.0.1
+ '@radix-ui/react-collapsible': 1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-popover': 1.1.1(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-slot': 1.1.0(@types/react@18.2.47)(react@18.3.1)
+ '@radix-ui/react-toggle-group': 1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-tooltip': 1.1.1(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1)
+ '@swc/core': 1.3.101
+ '@types/react': 18.2.47
+ '@types/react-dom': 18.2.14
+ '@types/webpack': 5.28.5(@swc/core@1.3.101)(esbuild@0.19.11)
+ autoprefixer: 10.4.14(postcss@8.4.38)
+ chalk: 4.1.2
+ chokidar: 3.5.3
+ clsx: 2.1.0
+ commander: 11.1.0
+ debounce: 2.0.0
+ esbuild: 0.19.11
+ eslint-config-prettier: 9.0.0(eslint@9.7.0)
+ eslint-config-turbo: 1.10.12(eslint@9.7.0)
+ framer-motion: 10.17.4(react-dom@18.3.1)(react@18.3.1)
+ glob: 10.3.4
+ log-symbols: 4.1.0
+ mime-types: 2.1.35
+ next: 14.1.4(@babel/core@7.24.5)(react-dom@18.3.1)(react@18.3.1)
+ normalize-path: 3.0.0
+ ora: 5.4.1
+ postcss: 8.4.38
+ prism-react-renderer: 2.1.0(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ socket.io: 4.7.3
+ socket.io-client: 4.7.3
+ sonner: 1.3.1(react-dom@18.3.1)(react@18.3.1)
+ source-map-js: 1.0.2
+ stacktrace-parser: 0.1.10
+ tailwind-merge: 2.2.0
+ tailwindcss: 3.4.0
+ typescript: 5.1.6
+ transitivePeerDependencies:
+ - '@opentelemetry/api'
+ - '@swc/helpers'
+ - babel-plugin-macros
+ - bufferutil
+ - eslint
+ - sass
+ - supports-color
+ - ts-node
+ - uglify-js
+ - utf-8-validate
+ - webpack-cli
+ dev: false
+
+ /react-promise-suspense@0.3.4:
+ resolution: {integrity: sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==}
+ dependencies:
+ fast-deep-equal: 2.0.1
+ dev: false
+
+ /react-remove-scroll-bar@2.3.6(@types/react@18.2.47)(react@18.3.1):
+ resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@types/react': 18.2.47
+ react: 18.3.1
+ react-style-singleton: 2.2.1(@types/react@18.2.47)(react@18.3.1)
+ tslib: 2.6.3
+ dev: false
+
+ /react-remove-scroll@2.5.7(@types/react@18.2.47)(react@18.3.1):
+ resolution: {integrity: sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@types/react': 18.2.47
+ react: 18.3.1
+ react-remove-scroll-bar: 2.3.6(@types/react@18.2.47)(react@18.3.1)
+ react-style-singleton: 2.2.1(@types/react@18.2.47)(react@18.3.1)
+ tslib: 2.6.3
+ use-callback-ref: 1.3.2(@types/react@18.2.47)(react@18.3.1)
+ use-sidecar: 1.1.2(@types/react@18.2.47)(react@18.3.1)
+ dev: false
+
+ /react-style-singleton@2.2.1(@types/react@18.2.47)(react@18.3.1):
+ resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@types/react': 18.2.47
+ get-nonce: 1.0.1
+ invariant: 2.2.4
+ react: 18.3.1
+ tslib: 2.6.3
+ dev: false
+
+ /react@18.3.1:
+ resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
+ engines: {node: '>=0.10.0'}
+ dependencies:
+ loose-envify: 1.4.0
+ dev: false
+
+ /read-cache@1.0.0:
+ resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
+ dependencies:
+ pify: 2.3.0
+ dev: false
+
+ /readable-stream@3.6.2:
+ resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
+ engines: {node: '>= 6'}
+ dependencies:
+ inherits: 2.0.4
+ string_decoder: 1.3.0
+ util-deprecate: 1.0.2
+ dev: false
+
+ /readdirp@3.6.0:
+ resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
+ engines: {node: '>=8.10.0'}
+ dependencies:
+ picomatch: 2.3.1
+ dev: false
+
+ /regenerator-runtime@0.14.1:
+ resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
+ dev: false
+
+ /resolve-from@4.0.0:
+ resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
+ engines: {node: '>=4'}
+ dev: false
+
+ /resolve@1.22.8:
+ resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
+ hasBin: true
+ dependencies:
+ is-core-module: 2.14.0
+ path-parse: 1.0.7
+ supports-preserve-symlinks-flag: 1.0.0
+ dev: false
+
+ /restore-cursor@3.1.0:
+ resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==}
+ engines: {node: '>=8'}
+ dependencies:
+ onetime: 5.1.2
+ signal-exit: 3.0.7
+ dev: false
+
+ /reusify@1.0.4:
+ resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
+ engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
+ dev: false
+
+ /run-parallel@1.2.0:
+ resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
+ dependencies:
+ queue-microtask: 1.2.3
+ dev: false
+
+ /safe-buffer@5.2.1:
+ resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
+ dev: false
+
+ /scheduler@0.23.2:
+ resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
+ dependencies:
+ loose-envify: 1.4.0
+ dev: false
+
+ /schema-utils@3.3.0:
+ resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==}
+ engines: {node: '>= 10.13.0'}
+ dependencies:
+ '@types/json-schema': 7.0.15
+ ajv: 6.12.6
+ ajv-keywords: 3.5.2(ajv@6.12.6)
+ dev: false
+
+ /selderee@0.11.0:
+ resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==}
+ dependencies:
+ parseley: 0.12.1
+ dev: false
+
+ /semver@6.3.1:
+ resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
+ hasBin: true
+ dev: false
+
+ /semver@7.6.2:
+ resolution: {integrity: sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==}
+ engines: {node: '>=10'}
+ hasBin: true
+ dev: false
+
+ /serialize-javascript@6.0.2:
+ resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==}
+ dependencies:
+ randombytes: 2.1.0
+ dev: false
+
+ /shebang-command@2.0.0:
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+ engines: {node: '>=8'}
+ dependencies:
+ shebang-regex: 3.0.0
+ dev: false
+
+ /shebang-regex@3.0.0:
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+ engines: {node: '>=8'}
+ dev: false
+
+ /signal-exit@3.0.7:
+ resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
+ dev: false
+
+ /signal-exit@4.1.0:
+ resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
+ engines: {node: '>=14'}
+ dev: false
+
+ /socket.io-adapter@2.5.5:
+ resolution: {integrity: sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==}
+ dependencies:
+ debug: 4.3.5
+ ws: 8.17.1
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+ dev: false
+
+ /socket.io-client@4.7.3:
+ resolution: {integrity: sha512-nU+ywttCyBitXIl9Xe0RSEfek4LneYkJxCeNnKCuhwoH4jGXO1ipIUw/VA/+Vvv2G1MTym11fzFC0SxkrcfXDw==}
+ engines: {node: '>=10.0.0'}
+ dependencies:
+ '@socket.io/component-emitter': 3.1.2
+ debug: 4.3.5
+ engine.io-client: 6.5.4
+ socket.io-parser: 4.2.4
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+ dev: false
+
+ /socket.io-parser@4.2.4:
+ resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==}
+ engines: {node: '>=10.0.0'}
+ dependencies:
+ '@socket.io/component-emitter': 3.1.2
+ debug: 4.3.5
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
+ /socket.io@4.7.3:
+ resolution: {integrity: sha512-SE+UIQXBQE+GPG2oszWMlsEmWtHVqw/h1VrYJGK5/MC7CH5p58N448HwIrtREcvR4jfdOJAY4ieQfxMr55qbbw==}
+ engines: {node: '>=10.2.0'}
+ dependencies:
+ accepts: 1.3.8
+ base64id: 2.0.0
+ cors: 2.8.5
+ debug: 4.3.5
+ engine.io: 6.5.5
+ socket.io-adapter: 2.5.5
+ socket.io-parser: 4.2.4
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+ dev: false
+
+ /sonner@1.3.1(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-+rOAO56b2eI3q5BtgljERSn2umRk63KFIvgb2ohbZ5X+Eb5u+a/7/0ZgswYqgBMg8dyl7n6OXd9KasA8QF9ToA==}
+ peerDependencies:
+ react: ^18.0.0
+ react-dom: ^18.0.0
+ dependencies:
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ dev: false
+
+ /source-map-js@1.0.2:
+ resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
+ engines: {node: '>=0.10.0'}
+ dev: false
+
+ /source-map-js@1.2.0:
+ resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==}
+ engines: {node: '>=0.10.0'}
+ dev: false
+
+ /source-map-support@0.5.21:
+ resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
+ dependencies:
+ buffer-from: 1.1.2
+ source-map: 0.6.1
+ dev: false
+
+ /source-map@0.6.1:
+ resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
+ engines: {node: '>=0.10.0'}
+ dev: false
+
+ /stacktrace-parser@0.1.10:
+ resolution: {integrity: sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg==}
+ engines: {node: '>=6'}
+ dependencies:
+ type-fest: 0.7.1
+ dev: false
+
+ /streamsearch@1.1.0:
+ resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
+ engines: {node: '>=10.0.0'}
+ dev: false
+
+ /string-width@4.2.3:
+ resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
+ engines: {node: '>=8'}
+ dependencies:
+ emoji-regex: 8.0.0
+ is-fullwidth-code-point: 3.0.0
+ strip-ansi: 6.0.1
+ dev: false
+
+ /string-width@5.1.2:
+ resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
+ engines: {node: '>=12'}
+ dependencies:
+ eastasianwidth: 0.2.0
+ emoji-regex: 9.2.2
+ strip-ansi: 7.1.0
+ dev: false
+
+ /string_decoder@1.3.0:
+ resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
+ dependencies:
+ safe-buffer: 5.2.1
+ dev: false
+
+ /strip-ansi@6.0.1:
+ resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
+ engines: {node: '>=8'}
+ dependencies:
+ ansi-regex: 5.0.1
+ dev: false
+
+ /strip-ansi@7.1.0:
+ resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
+ engines: {node: '>=12'}
+ dependencies:
+ ansi-regex: 6.0.1
+ dev: false
+
+ /strip-json-comments@3.1.1:
+ resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
+ engines: {node: '>=8'}
+ dev: false
+
+ /styled-jsx@5.1.1(@babel/core@7.24.5)(react@18.3.1):
+ resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==}
+ engines: {node: '>= 12.0.0'}
+ peerDependencies:
+ '@babel/core': '*'
+ babel-plugin-macros: '*'
+ react: '>= 16.8.0 || 17.x.x || ^18.0.0-0'
+ peerDependenciesMeta:
+ '@babel/core':
+ optional: true
+ babel-plugin-macros:
+ optional: true
+ dependencies:
+ '@babel/core': 7.24.5
+ client-only: 0.0.1
+ react: 18.3.1
+ dev: false
+
+ /sucrase@3.35.0:
+ resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==}
+ engines: {node: '>=16 || 14 >=14.17'}
+ hasBin: true
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.5
+ commander: 4.1.1
+ glob: 10.4.5
+ lines-and-columns: 1.2.4
+ mz: 2.7.0
+ pirates: 4.0.6
+ ts-interface-checker: 0.1.13
+ dev: false
+
+ /supports-color@5.5.0:
+ resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
+ engines: {node: '>=4'}
+ dependencies:
+ has-flag: 3.0.0
+ dev: false
+
+ /supports-color@7.2.0:
+ resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+ engines: {node: '>=8'}
+ dependencies:
+ has-flag: 4.0.0
+ dev: false
+
+ /supports-color@8.1.1:
+ resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
+ engines: {node: '>=10'}
+ dependencies:
+ has-flag: 4.0.0
+ dev: false
+
+ /supports-preserve-symlinks-flag@1.0.0:
+ resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
+ engines: {node: '>= 0.4'}
+ dev: false
+
+ /tailwind-merge@2.2.0:
+ resolution: {integrity: sha512-SqqhhaL0T06SW59+JVNfAqKdqLs0497esifRrZ7jOaefP3o64fdFNDMrAQWZFMxTLJPiHVjRLUywT8uFz1xNWQ==}
+ dependencies:
+ '@babel/runtime': 7.24.8
+ dev: false
+
+ /tailwindcss@3.4.0:
+ resolution: {integrity: sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA==}
+ engines: {node: '>=14.0.0'}
+ hasBin: true
+ dependencies:
+ '@alloc/quick-lru': 5.2.0
+ arg: 5.0.2
+ chokidar: 3.5.3
+ didyoumean: 1.2.2
+ dlv: 1.1.3
+ fast-glob: 3.3.2
+ glob-parent: 6.0.2
+ is-glob: 4.0.3
+ jiti: 1.21.6
+ lilconfig: 2.1.0
+ micromatch: 4.0.7
+ normalize-path: 3.0.0
+ object-hash: 3.0.0
+ picocolors: 1.0.1
+ postcss: 8.4.38
+ postcss-import: 15.1.0(postcss@8.4.38)
+ postcss-js: 4.0.1(postcss@8.4.38)
+ postcss-load-config: 4.0.2(postcss@8.4.38)
+ postcss-nested: 6.0.1(postcss@8.4.38)
+ postcss-selector-parser: 6.1.1
+ resolve: 1.22.8
+ sucrase: 3.35.0
+ transitivePeerDependencies:
+ - ts-node
+ dev: false
+
+ /tapable@2.2.1:
+ resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
+ engines: {node: '>=6'}
+ dev: false
+
+ /terser-webpack-plugin@5.3.10(@swc/core@1.3.101)(esbuild@0.19.11)(webpack@5.93.0):
+ resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==}
+ engines: {node: '>= 10.13.0'}
+ peerDependencies:
+ '@swc/core': '*'
+ esbuild: '*'
+ uglify-js: '*'
+ webpack: ^5.1.0
+ peerDependenciesMeta:
+ '@swc/core':
+ optional: true
+ esbuild:
+ optional: true
+ uglify-js:
+ optional: true
+ dependencies:
+ '@jridgewell/trace-mapping': 0.3.25
+ '@swc/core': 1.3.101
+ esbuild: 0.19.11
+ jest-worker: 27.5.1
+ schema-utils: 3.3.0
+ serialize-javascript: 6.0.2
+ terser: 5.31.2
+ webpack: 5.93.0(@swc/core@1.3.101)(esbuild@0.19.11)
+ dev: false
+
+ /terser@5.31.2:
+ resolution: {integrity: sha512-LGyRZVFm/QElZHy/CPr/O4eNZOZIzsrQ92y4v9UJe/pFJjypje2yI3C2FmPtvUEnhadlSbmG2nXtdcjHOjCfxw==}
+ engines: {node: '>=10'}
+ hasBin: true
+ dependencies:
+ '@jridgewell/source-map': 0.3.6
+ acorn: 8.12.1
+ commander: 2.20.3
+ source-map-support: 0.5.21
+ dev: false
+
+ /text-table@0.2.0:
+ resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
+ dev: false
+
+ /thenify-all@1.6.0:
+ resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
+ engines: {node: '>=0.8'}
+ dependencies:
+ thenify: 3.3.1
+ dev: false
+
+ /thenify@3.3.1:
+ resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
+ dependencies:
+ any-promise: 1.3.0
+ dev: false
+
+ /to-fast-properties@2.0.0:
+ resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
+ engines: {node: '>=4'}
+ dev: false
+
+ /to-regex-range@5.0.1:
+ resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
+ engines: {node: '>=8.0'}
+ dependencies:
+ is-number: 7.0.0
+ dev: false
+
+ /ts-interface-checker@0.1.13:
+ resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
+ dev: false
+
+ /tslib@2.6.3:
+ resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==}
+ dev: false
+
+ /type-check@0.4.0:
+ resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
+ engines: {node: '>= 0.8.0'}
+ dependencies:
+ prelude-ls: 1.2.1
+ dev: false
+
+ /type-fest@0.7.1:
+ resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==}
+ engines: {node: '>=8'}
+ dev: false
+
+ /typescript@5.1.6:
+ resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+ dev: false
+
+ /undici-types@5.26.5:
+ resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
+ dev: false
+
+ /update-browserslist-db@1.1.0(browserslist@4.23.2):
+ resolution: {integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+ dependencies:
+ browserslist: 4.23.2
+ escalade: 3.1.2
+ picocolors: 1.0.1
+ dev: false
+
+ /uri-js@4.4.1:
+ resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+ dependencies:
+ punycode: 2.3.1
+ dev: false
+
+ /use-callback-ref@1.3.2(@types/react@18.2.47)(react@18.3.1):
+ resolution: {integrity: sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@types/react': 18.2.47
+ react: 18.3.1
+ tslib: 2.6.3
+ dev: false
+
+ /use-sidecar@1.1.2(@types/react@18.2.47)(react@18.3.1):
+ resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': ^16.9.0 || ^17.0.0 || ^18.0.0
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@types/react': 18.2.47
+ detect-node-es: 1.1.0
+ react: 18.3.1
+ tslib: 2.6.3
+ dev: false
+
+ /util-deprecate@1.0.2:
+ resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+ dev: false
+
+ /vary@1.1.2:
+ resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
+ engines: {node: '>= 0.8'}
+ dev: false
+
+ /watchpack@2.4.1:
+ resolution: {integrity: sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==}
+ engines: {node: '>=10.13.0'}
+ dependencies:
+ glob-to-regexp: 0.4.1
+ graceful-fs: 4.2.11
+ dev: false
+
+ /wcwidth@1.0.1:
+ resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
+ dependencies:
+ defaults: 1.0.4
+ dev: false
+
+ /webpack-sources@3.2.3:
+ resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==}
+ engines: {node: '>=10.13.0'}
+ dev: false
+
+ /webpack@5.93.0(@swc/core@1.3.101)(esbuild@0.19.11):
+ resolution: {integrity: sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==}
+ engines: {node: '>=10.13.0'}
+ hasBin: true
+ peerDependencies:
+ webpack-cli: '*'
+ peerDependenciesMeta:
+ webpack-cli:
+ optional: true
+ dependencies:
+ '@types/eslint-scope': 3.7.7
+ '@types/estree': 1.0.5
+ '@webassemblyjs/ast': 1.12.1
+ '@webassemblyjs/wasm-edit': 1.12.1
+ '@webassemblyjs/wasm-parser': 1.12.1
+ acorn: 8.12.1
+ acorn-import-attributes: 1.9.5(acorn@8.12.1)
+ browserslist: 4.23.2
+ chrome-trace-event: 1.0.4
+ enhanced-resolve: 5.17.0
+ es-module-lexer: 1.5.4
+ eslint-scope: 5.1.1
+ events: 3.3.0
+ glob-to-regexp: 0.4.1
+ graceful-fs: 4.2.11
+ json-parse-even-better-errors: 2.3.1
+ loader-runner: 4.3.0
+ mime-types: 2.1.35
+ neo-async: 2.6.2
+ schema-utils: 3.3.0
+ tapable: 2.2.1
+ terser-webpack-plugin: 5.3.10(@swc/core@1.3.101)(esbuild@0.19.11)(webpack@5.93.0)
+ watchpack: 2.4.1
+ webpack-sources: 3.2.3
+ transitivePeerDependencies:
+ - '@swc/core'
+ - esbuild
+ - uglify-js
+ dev: false
+
+ /which@2.0.2:
+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+ engines: {node: '>= 8'}
+ hasBin: true
+ dependencies:
+ isexe: 2.0.0
+ dev: false
+
+ /word-wrap@1.2.5:
+ resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
+ engines: {node: '>=0.10.0'}
+ dev: false
+
+ /wrap-ansi@7.0.0:
+ resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
+ engines: {node: '>=10'}
+ dependencies:
+ ansi-styles: 4.3.0
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ dev: false
+
+ /wrap-ansi@8.1.0:
+ resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
+ engines: {node: '>=12'}
+ dependencies:
+ ansi-styles: 6.2.1
+ string-width: 5.1.2
+ strip-ansi: 7.1.0
+ dev: false
+
+ /ws@8.17.1:
+ resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==}
+ engines: {node: '>=10.0.0'}
+ peerDependencies:
+ bufferutil: ^4.0.1
+ utf-8-validate: '>=5.0.2'
+ peerDependenciesMeta:
+ bufferutil:
+ optional: true
+ utf-8-validate:
+ optional: true
+ dev: false
+
+ /xmlhttprequest-ssl@2.0.0:
+ resolution: {integrity: sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==}
+ engines: {node: '>=0.4.0'}
+ dev: false
+
+ /yallist@3.1.1:
+ resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
+ dev: false
+
+ /yaml@2.4.5:
+ resolution: {integrity: sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==}
+ engines: {node: '>= 14'}
+ hasBin: true
+ dev: false
+
+ /yocto-queue@0.1.0:
+ resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
+ engines: {node: '>=10'}
+ dev: false
diff --git a/packages/builders/src/emails/readme.md b/packages/builders/src/emails/readme.md
new file mode 100644
index 000000000..fc875bfa3
--- /dev/null
+++ b/packages/builders/src/emails/readme.md
@@ -0,0 +1,27 @@
+# React Email Starter
+
+A live preview right in your browser so you don't need to keep sending real emails during development.
+
+## Getting Started
+
+First, install the dependencies:
+
+```sh
+npm install
+# or
+yarn
+```
+
+Then, run the development server:
+
+```sh
+npm run dev
+# or
+yarn dev
+```
+
+Open [localhost:3000](http://localhost:3000) with your browser to see the result.
+
+## License
+
+MIT License
diff --git a/packages/builders/src/index.ts b/packages/builders/src/index.ts
new file mode 100644
index 000000000..f1583be5e
--- /dev/null
+++ b/packages/builders/src/index.ts
@@ -0,0 +1,137 @@
+export * from "./auth/auth";
+// export * from "./db";
+export * from "./services/admin";
+export * from "./services/user";
+export * from "./services/project";
+export * from "./services/domain";
+export * from "./services/mariadb";
+export * from "./services/mongo";
+export * from "./services/mysql";
+export * from "./services/backup";
+export * from "./services/destination";
+export * from "./services/deployment";
+export * from "./services/mount";
+export * from "./services/certificate";
+export * from "./services/redirect";
+export * from "./services/security";
+export * from "./services/port";
+export * from "./services/redis";
+export * from "./services/compose";
+export * from "./services/registry";
+export * from "./services/notification";
+export * from "./services/ssh-key";
+export * from "./services/git-provider";
+export * from "./services/bitbucket";
+export * from "./services/github";
+export * from "./services/gitlab";
+export * from "./services/server";
+export * from "./services/application";
+export * from "./db/schema/application";
+export * from "./db/schema/postgres";
+export * from "./db/schema/user";
+export * from "./db/schema/admin";
+export * from "./db/schema/auth";
+export * from "./db/schema/project";
+export * from "./db/schema/domain";
+export * from "./db/schema/mariadb";
+export * from "./db/schema/mongo";
+export * from "./db/schema/mysql";
+export * from "./db/schema/backups";
+export * from "./db/schema/destination";
+export * from "./db/schema/deployment";
+export * from "./db/schema/mount";
+export * from "./db/schema/certificate";
+export * from "./db/schema/session";
+export * from "./db/schema/redirects";
+export * from "./db/schema/security";
+export * from "./db/schema/port";
+export * from "./db/schema/redis";
+export * from "./db/schema/shared";
+export * from "./db/schema/compose";
+export * from "./db/schema/registry";
+export * from "./db/schema/notification";
+export * from "./db/schema/ssh-key";
+export * from "./db/schema/git-provider";
+export * from "./db/schema/bitbucket";
+export * from "./db/schema/github";
+export * from "./db/schema/gitlab";
+export * from "./db/schema/server";
+export * from "./db/schema/utils";
+
+export * from "./setup/config-paths";
+export * from "./setup/postgres-setup";
+export * from "./setup/redis-setup";
+export * from "./setup/registry-setup";
+export * from "./setup/server-setup";
+export * from "./setup/setup";
+export * from "./setup/traefik-setup";
+
+export * from "./utils/backups/index";
+export * from "./utils/backups/mariadb";
+export * from "./utils/backups/mongo";
+export * from "./utils/backups/mysql";
+export * from "./utils/backups/postgres";
+export * from "./utils/backups/utils";
+
+export * from "./utils/notifications/build-error";
+export * from "./utils/notifications/build-success";
+export * from "./utils/notifications/database-backup";
+export * from "./utils/notifications/dokploy-restart";
+export * from "./utils/notifications/utils";
+export * from "./utils/notifications/docker-cleanup";
+
+export * from "./utils/builders/index";
+export * from "./utils/builders/compose";
+export * from "./utils/builders/docker-file";
+export * from "./utils/builders/drop";
+export * from "./utils/builders/heroku";
+export * from "./utils/builders/nixpacks";
+export * from "./utils/builders/paketo";
+export * from "./utils/builders/static";
+export * from "./utils/builders/utils";
+
+export * from "./utils/cluster/upload";
+
+export * from "./utils/docker/compose";
+export * from "./utils/docker/domain";
+export * from "./utils/docker/utils";
+export * from "./utils/docker/compose/configs";
+export * from "./utils/docker/compose/network";
+export * from "./utils/docker/compose/secrets";
+export * from "./utils/docker/compose/service";
+export * from "./utils/docker/compose/volume";
+
+export * from "./utils/filesystem/directory";
+export * from "./utils/filesystem/ssh";
+
+export * from "./utils/process/execAsync";
+export * from "./utils/process/spawnAsync";
+export * from "./utils/providers/bitbucket";
+export * from "./utils/providers/docker";
+export * from "./utils/providers/git";
+export * from "./utils/providers/github";
+export * from "./utils/providers/gitlab";
+export * from "./utils/providers/raw";
+
+export * from "./utils/servers/remote-docker";
+
+export * from "./utils/traefik/application";
+export * from "./utils/traefik/domain";
+export * from "./utils/traefik/file-types";
+export * from "./utils/traefik/middleware";
+export * from "./utils/traefik/redirect";
+export * from "./utils/traefik/registry";
+export * from "./utils/traefik/security";
+export * from "./utils/traefik/types";
+export * from "./utils/traefik/web-server";
+
+export * from "./wss/docker-container-logs";
+export * from "./wss/docker-container-terminal";
+export * from "./wss/docker-stats";
+export * from "./wss/listen-deployment";
+export * from "./wss/terminal";
+export * from "./wss/utils";
+
+export * from "./utils/access-log/handler";
+export * from "./utils/access-log/types";
+export * from "./utils/access-log/utils";
diff --git a/packages/builders/src/monitoring/utilts.ts b/packages/builders/src/monitoring/utilts.ts
new file mode 100644
index 000000000..f67d57050
--- /dev/null
+++ b/packages/builders/src/monitoring/utilts.ts
@@ -0,0 +1,198 @@
+import { promises } from "node:fs";
+import type Dockerode from "dockerode";
+import osUtils from "node-os-utils";
+import { paths } from "../constants";
+
+export const recordAdvancedStats = async (
+ stats: Dockerode.ContainerStats,
+ appName: string,
+) => {
+ const { MONITORING_PATH } = paths();
+ const path = `${MONITORING_PATH}/${appName}`;
+
+ await promises.mkdir(path, { recursive: true });
+
+ const cpuPercent = calculateCpuUsagePercent(
+ stats.cpu_stats,
+ stats.precpu_stats,
+ );
+ const memoryStats = calculateMemoryStats(stats.memory_stats);
+ const blockIO = calculateBlockIO(stats.blkio_stats);
+ const networkUsage = calculateNetworkUsage(stats.networks);
+
+ await updateStatsFile(appName, "cpu", cpuPercent);
+ await updateStatsFile(appName, "memory", {
+ used: memoryStats.used,
+ free: memoryStats.free,
+ usedPercentage: memoryStats.usedPercentage,
+ total: memoryStats.total,
+ });
+ await updateStatsFile(appName, "block", {
+ readMb: blockIO.readMb,
+ writeMb: blockIO.writeMb,
+ });
+
+ await updateStatsFile(appName, "network", {
+ inputMb: networkUsage.inputMb,
+ outputMb: networkUsage.outputMb,
+ });
+
+ if (appName === "dokploy") {
+ const disk = await osUtils.drive.info("/");
+
+ const diskUsage = disk.usedGb;
+ const diskTotal = disk.totalGb;
+ const diskUsedPercentage = disk.usedPercentage;
+ const diskFree = disk.freeGb;
+
+ await updateStatsFile(appName, "disk", {
+ diskTotal: +diskTotal,
+ diskUsedPercentage: +diskUsedPercentage,
+ diskUsage: +diskUsage,
+ diskFree: +diskFree,
+ });
+ }
+};
+
+export const getAdvancedStats = async (appName: string) => {
+ return {
+ cpu: await readStatsFile(appName, "cpu"),
+ memory: await readStatsFile(appName, "memory"),
+ disk: await readStatsFile(appName, "disk"),
+ network: await readStatsFile(appName, "network"),
+ block: await readStatsFile(appName, "block"),
+ };
+};
+
+export const readStatsFile = async (
+ appName: string,
+ statType: "cpu" | "memory" | "disk" | "network" | "block",
+) => {
+ try {
+ const { MONITORING_PATH } = paths();
+ const filePath = `${MONITORING_PATH}/${appName}/${statType}.json`;
+ const data = await promises.readFile(filePath, "utf-8");
+ return JSON.parse(data);
+ } catch (error) {
+ return [];
+ }
+};
+
+export const updateStatsFile = async (
+ appName: string,
+ statType: "cpu" | "memory" | "disk" | "network" | "block",
+ value: number | string | unknown,
+) => {
+ const { MONITORING_PATH } = paths();
+ const stats = await readStatsFile(appName, statType);
+ stats.push({ value, time: new Date() });
+
+ if (stats.length > 288) {
+ stats.shift();
+ }
+
+ const content = JSON.stringify(stats);
+ await promises.writeFile(
+ `${MONITORING_PATH}/${appName}/${statType}.json`,
+ content,
+ );
+};
+
+export const readLastValueStatsFile = async (
+ appName: string,
+ statType: "cpu" | "memory" | "disk" | "network" | "block",
+) => {
+ try {
+ const { MONITORING_PATH } = paths();
+ const filePath = `${MONITORING_PATH}/${appName}/${statType}.json`;
+ const data = await promises.readFile(filePath, "utf-8");
+ const stats = JSON.parse(data);
+ return stats[stats.length - 1] || null;
+ } catch (error) {
+ return null;
+ }
+};
+
+export const getLastAdvancedStatsFile = async (appName: string) => {
+ return {
+ cpu: await readLastValueStatsFile(appName, "cpu"),
+ memory: await readLastValueStatsFile(appName, "memory"),
+ disk: await readLastValueStatsFile(appName, "disk"),
+ network: await readLastValueStatsFile(appName, "network"),
+ block: await readLastValueStatsFile(appName, "block"),
+ };
+};
+
+const calculateCpuUsagePercent = (
+ cpu_stats: Dockerode.ContainerStats["cpu_stats"],
+ precpu_stats: Dockerode.ContainerStats["precpu_stats"],
+) => {
+ const cpuDelta =
+ cpu_stats.cpu_usage.total_usage - precpu_stats.cpu_usage.total_usage;
+ const systemDelta =
+ cpu_stats.system_cpu_usage - precpu_stats.system_cpu_usage;
+
+ const numberCpus =
+ cpu_stats.online_cpus ||
+ (cpu_stats.cpu_usage.percpu_usage
+ ? cpu_stats.cpu_usage.percpu_usage.length
+ : 1);
+
+ if (systemDelta > 0 && cpuDelta > 0) {
+ return (cpuDelta / systemDelta) * numberCpus * 100.0;
+ }
+ return 0;
+};
+
+const calculateMemoryStats = (
+ memory_stats: Dockerode.ContainerStats["memory_stats"],
+) => {
+ const usedMemory = memory_stats.usage - (memory_stats.stats.cache || 0);
+ const availableMemory = memory_stats.limit;
+ const memoryUsedPercentage = (usedMemory / availableMemory) * 100.0;
+
+ return {
+ used: usedMemory,
+ free: availableMemory - usedMemory,
+ usedPercentage: memoryUsedPercentage,
+ total: availableMemory,
+ };
+};
+const calculateBlockIO = (
+ blkio_stats: Dockerode.ContainerStats["blkio_stats"],
+) => {
+ let readIO = 0;
+ let writeIO = 0;
+ if (blkio_stats?.io_service_bytes_recursive) {
+ for (const io of blkio_stats.io_service_bytes_recursive) {
+ if (io.op === "read") {
+ readIO += io.value;
+ } else if (io.op === "write") {
+ writeIO += io.value;
+ }
+ }
+ }
+ return {
+ readMb: readIO / (1024 * 1024),
+ writeMb: writeIO / (1024 * 1024),
+ };
+};
+
+const calculateNetworkUsage = (
+ networks: Dockerode.ContainerStats["networks"],
+) => {
+ let totalRx = 0;
+ let totalTx = 0;
+
+ const stats = Object.keys(networks);
+
+ for (const interfaceName of stats) {
+ const net = networks[interfaceName];
+ totalRx += net?.rx_bytes || 0;
+ totalTx += net?.tx_bytes || 0;
+ }
+ return {
+ inputMb: totalRx / (1024 * 1024),
+ outputMb: totalTx / (1024 * 1024),
+ };
+};
diff --git a/packages/builders/src/services/admin.ts b/packages/builders/src/services/admin.ts
new file mode 100644
index 000000000..b9394bc53
--- /dev/null
+++ b/packages/builders/src/services/admin.ts
@@ -0,0 +1,151 @@
+import { randomBytes } from "node:crypto";
+import { db } from "@/server/db";
+import {
+ admins,
+ type apiCreateUserInvitation,
+ auth,
+ users,
+} from "@/server/db/schema";
+import { TRPCError } from "@trpc/server";
+import * as bcrypt from "bcrypt";
+import { eq } from "drizzle-orm";
+
+export type Admin = typeof admins.$inferSelect;
+export const createInvitation = async (
+ input: typeof apiCreateUserInvitation._type,
+) => {
+ await db.transaction(async (tx) => {
+ const admin = await findAdmin();
+
+ const result = await tx
+ .insert(auth)
+ .values({
+ email: input.email,
+ rol: "user",
+ password: bcrypt.hashSync("01231203012312", 10),
+ })
+ .returning()
+ .then((res) => res[0]);
+
+ if (!result) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error to create the user",
+ });
+ }
+ const expiresIn24Hours = new Date();
+ expiresIn24Hours.setDate(expiresIn24Hours.getDate() + 1);
+ const token = randomBytes(32).toString("hex");
+ await tx
+ .insert(users)
+ .values({
+ adminId: admin.adminId,
+ authId: result.id,
+ token,
+ expirationDate: expiresIn24Hours.toISOString(),
+ })
+ .returning();
+ });
+};
+
+export const findAdminById = async (adminId: string) => {
+ const admin = await db.query.admins.findFirst({
+ where: eq(admins.adminId, adminId),
+ });
+ if (!admin) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Admin not found",
+ });
+ }
+ return admin;
+};
+
+export const updateAdmin = async (
+ authId: string,
+ adminData: Partial,
+) => {
+ const admin = await db
+ .update(admins)
+ .set({
+ ...adminData,
+ })
+ .where(eq(admins.authId, authId))
+ .returning()
+ .then((res) => res[0]);
+
+ return admin;
+};
+
+export const isAdminPresent = async () => {
+ const admin = await db.query.admins.findFirst();
+ if (!admin) {
+ return false;
+ }
+ return true;
+};
+
+export const findAdminByAuthId = async (authId: string) => {
+ const admin = await db.query.admins.findFirst({
+ where: eq(admins.authId, authId),
+ });
+ if (!admin) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Admin not found",
+ });
+ }
+ return admin;
+};
+
+export const findAdmin = async () => {
+ const admin = await db.query.admins.findFirst({});
+ if (!admin) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Admin not found",
+ });
+ }
+ return admin;
+};
+
+export const getUserByToken = async (token: string) => {
+ const user = await db.query.users.findFirst({
+ where: eq(users.token, token),
+ with: {
+ auth: {
+ columns: {
+ password: false,
+ },
+ },
+ },
+ });
+
+ if (!user) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Invitation not found",
+ });
+ }
+ return {
+ ...user,
+ isExpired: user.isRegistered,
+ };
+};
+
+export const removeUserByAuthId = async (authId: string) => {
+ await db
+ .delete(auth)
+ .where(eq(auth.id, authId))
+ .returning()
+ .then((res) => res[0]);
+};
+
+export const getDokployUrl = async () => {
+ const admin = await findAdmin();
+
+ if (admin.host) {
+ return `https://${admin.host}`;
+ }
+ return `http://${admin.serverIp}:${process.env.PORT}`;
+};
diff --git a/packages/builders/src/services/application.ts b/packages/builders/src/services/application.ts
new file mode 100644
index 000000000..45a7ad653
--- /dev/null
+++ b/packages/builders/src/services/application.ts
@@ -0,0 +1,394 @@
+import { docker } from "@/server/constants";
+import { db } from "@/server/db";
+import { type apiCreateApplication, applications } from "@/server/db/schema";
+import { generateAppName } from "@/server/db/schema";
+import { getAdvancedStats } from "@/server/monitoring/utilts";
+import {
+ buildApplication,
+ getBuildCommand,
+ mechanizeDockerContainer,
+} from "@/server/utils/builders";
+import { sendBuildErrorNotifications } from "@/server/utils/notifications/build-error";
+import { sendBuildSuccessNotifications } from "@/server/utils/notifications/build-success";
+import { execAsyncRemote } from "@/server/utils/process/execAsync";
+import {
+ cloneBitbucketRepository,
+ getBitbucketCloneCommand,
+} from "@/server/utils/providers/bitbucket";
+import {
+ buildDocker,
+ buildRemoteDocker,
+} from "@/server/utils/providers/docker";
+import {
+ cloneGitRepository,
+ getCustomGitCloneCommand,
+} from "@/server/utils/providers/git";
+import {
+ cloneGithubRepository,
+ getGithubCloneCommand,
+} from "@/server/utils/providers/github";
+import {
+ cloneGitlabRepository,
+ getGitlabCloneCommand,
+} from "@/server/utils/providers/gitlab";
+import { createTraefikConfig } from "@/server/utils/traefik/application";
+import { generatePassword } from "@/server/templates/utils";
+import { TRPCError } from "@trpc/server";
+import { eq } from "drizzle-orm";
+import { getDokployUrl } from "./admin";
+import { createDeployment, updateDeploymentStatus } from "./deployment";
+import { validUniqueServerAppName } from "./project";
+export type Application = typeof applications.$inferSelect;
+
+export const createApplication = async (
+ input: typeof apiCreateApplication._type,
+) => {
+ input.appName =
+ `${input.appName}-${generatePassword(6)}` || generateAppName("app");
+ if (input.appName) {
+ const valid = await validUniqueServerAppName(input.appName);
+
+ if (!valid) {
+ throw new TRPCError({
+ code: "CONFLICT",
+ message: "Application with this 'AppName' already exists",
+ });
+ }
+ }
+
+ return await db.transaction(async (tx) => {
+ const newApplication = await tx
+ .insert(applications)
+ .values({
+ ...input,
+ })
+ .returning()
+ .then((value) => value[0]);
+
+ if (!newApplication) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error to create the application",
+ });
+ }
+
+ if (process.env.NODE_ENV === "development") {
+ createTraefikConfig(newApplication.appName);
+ }
+
+ return newApplication;
+ });
+};
+
+export const findApplicationById = async (applicationId: string) => {
+ const application = await db.query.applications.findFirst({
+ where: eq(applications.applicationId, applicationId),
+ with: {
+ project: true,
+ domains: true,
+ deployments: true,
+ mounts: true,
+ redirects: true,
+ security: true,
+ ports: true,
+ registry: true,
+ gitlab: true,
+ github: true,
+ bitbucket: true,
+ server: true,
+ },
+ });
+ if (!application) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Application not found",
+ });
+ }
+ return application;
+};
+
+export const findApplicationByName = async (appName: string) => {
+ const application = await db.query.applications.findFirst({
+ where: eq(applications.appName, appName),
+ });
+
+ return application;
+};
+
+export const updateApplication = async (
+ applicationId: string,
+ applicationData: Partial,
+) => {
+ const application = await db
+ .update(applications)
+ .set({
+ ...applicationData,
+ })
+ .where(eq(applications.applicationId, applicationId))
+ .returning();
+
+ return application[0];
+};
+
+export const updateApplicationStatus = async (
+ applicationId: string,
+ applicationStatus: Application["applicationStatus"],
+) => {
+ const application = await db
+ .update(applications)
+ .set({
+ applicationStatus: applicationStatus,
+ })
+ .where(eq(applications.applicationId, applicationId))
+ .returning();
+
+ return application;
+};
+
+export const deployApplication = async ({
+ applicationId,
+ titleLog = "Manual deployment",
+ descriptionLog = "",
+}: {
+ applicationId: string;
+ titleLog: string;
+ descriptionLog: string;
+}) => {
+ const application = await findApplicationById(applicationId);
+ const buildLink = `${await getDokployUrl()}/dashboard/project/${application.projectId}/services/application/${application.applicationId}?tab=deployments`;
+ const deployment = await createDeployment({
+ applicationId: applicationId,
+ title: titleLog,
+ description: descriptionLog,
+ });
+
+ try {
+ if (application.sourceType === "github") {
+ await cloneGithubRepository(application, deployment.logPath);
+ await buildApplication(application, deployment.logPath);
+ } else if (application.sourceType === "gitlab") {
+ await cloneGitlabRepository(application, deployment.logPath);
+ await buildApplication(application, deployment.logPath);
+ } else if (application.sourceType === "bitbucket") {
+ await cloneBitbucketRepository(application, deployment.logPath);
+ await buildApplication(application, deployment.logPath);
+ } else if (application.sourceType === "docker") {
+ await buildDocker(application, deployment.logPath);
+ } else if (application.sourceType === "git") {
+ await cloneGitRepository(application, deployment.logPath);
+ await buildApplication(application, deployment.logPath);
+ } else if (application.sourceType === "drop") {
+ await buildApplication(application, deployment.logPath);
+ }
+
+ await updateDeploymentStatus(deployment.deploymentId, "done");
+ await updateApplicationStatus(applicationId, "done");
+
+ await sendBuildSuccessNotifications({
+ projectName: application.project.name,
+ applicationName: application.name,
+ applicationType: "application",
+ buildLink,
+ });
+ } catch (error) {
+ await updateDeploymentStatus(deployment.deploymentId, "error");
+ await updateApplicationStatus(applicationId, "error");
+ await sendBuildErrorNotifications({
+ projectName: application.project.name,
+ applicationName: application.name,
+ applicationType: "application",
+ // @ts-ignore
+ errorMessage: error?.message || "Error to build",
+ buildLink,
+ });
+
+ console.log(
+ "Error on ",
+ application.buildType,
+ "/",
+ application.sourceType,
+ error,
+ );
+
+ throw error;
+ }
+
+ return true;
+};
+
+export const rebuildApplication = async ({
+ applicationId,
+ titleLog = "Rebuild deployment",
+ descriptionLog = "",
+}: {
+ applicationId: string;
+ titleLog: string;
+ descriptionLog: string;
+}) => {
+ const application = await findApplicationById(applicationId);
+ const deployment = await createDeployment({
+ applicationId: applicationId,
+ title: titleLog,
+ description: descriptionLog,
+ });
+
+ try {
+ if (application.sourceType === "github") {
+ await buildApplication(application, deployment.logPath);
+ } else if (application.sourceType === "gitlab") {
+ await buildApplication(application, deployment.logPath);
+ } else if (application.sourceType === "bitbucket") {
+ await buildApplication(application, deployment.logPath);
+ } else if (application.sourceType === "docker") {
+ await buildDocker(application, deployment.logPath);
+ } else if (application.sourceType === "git") {
+ await buildApplication(application, deployment.logPath);
+ } else if (application.sourceType === "drop") {
+ await buildApplication(application, deployment.logPath);
+ }
+ await updateDeploymentStatus(deployment.deploymentId, "done");
+ await updateApplicationStatus(applicationId, "done");
+ } catch (error) {
+ await updateDeploymentStatus(deployment.deploymentId, "error");
+ await updateApplicationStatus(applicationId, "error");
+ throw error;
+ }
+
+ return true;
+};
+
+export const deployRemoteApplication = async ({
+ applicationId,
+ titleLog = "Manual deployment",
+ descriptionLog = "",
+}: {
+ applicationId: string;
+ titleLog: string;
+ descriptionLog: string;
+}) => {
+ const application = await findApplicationById(applicationId);
+ const buildLink = `${await getDokployUrl()}/dashboard/project/${application.projectId}/services/application/${application.applicationId}?tab=deployments`;
+ const deployment = await createDeployment({
+ applicationId: applicationId,
+ title: titleLog,
+ description: descriptionLog,
+ });
+
+ try {
+ if (application.serverId) {
+ let command = "set -e;";
+ if (application.sourceType === "github") {
+ command += await getGithubCloneCommand(application, deployment.logPath);
+ } else if (application.sourceType === "gitlab") {
+ command += await getGitlabCloneCommand(application, deployment.logPath);
+ } else if (application.sourceType === "bitbucket") {
+ command += await getBitbucketCloneCommand(
+ application,
+ deployment.logPath,
+ );
+ } else if (application.sourceType === "git") {
+ command += await getCustomGitCloneCommand(
+ application,
+ deployment.logPath,
+ );
+ } else if (application.sourceType === "docker") {
+ command += await buildRemoteDocker(application, deployment.logPath);
+ }
+
+ if (application.sourceType !== "docker") {
+ command += getBuildCommand(application, deployment.logPath);
+ }
+ await execAsyncRemote(application.serverId, command);
+ await mechanizeDockerContainer(application);
+ }
+
+ await updateDeploymentStatus(deployment.deploymentId, "done");
+ await updateApplicationStatus(applicationId, "done");
+
+ await sendBuildSuccessNotifications({
+ projectName: application.project.name,
+ applicationName: application.name,
+ applicationType: "application",
+ buildLink,
+ });
+ } catch (error) {
+ await updateDeploymentStatus(deployment.deploymentId, "error");
+ await updateApplicationStatus(applicationId, "error");
+ await sendBuildErrorNotifications({
+ projectName: application.project.name,
+ applicationName: application.name,
+ applicationType: "application",
+ // @ts-ignore
+ errorMessage: error?.message || "Error to build",
+ buildLink,
+ });
+
+ console.log(
+ "Error on ",
+ application.buildType,
+ "/",
+ application.sourceType,
+ error,
+ );
+
+ throw error;
+ }
+
+ return true;
+};
+
+export const rebuildRemoteApplication = async ({
+ applicationId,
+ titleLog = "Rebuild deployment",
+ descriptionLog = "",
+}: {
+ applicationId: string;
+ titleLog: string;
+ descriptionLog: string;
+}) => {
+ const application = await findApplicationById(applicationId);
+ const deployment = await createDeployment({
+ applicationId: applicationId,
+ title: titleLog,
+ description: descriptionLog,
+ });
+
+ try {
+ if (application.serverId) {
+ if (application.sourceType !== "docker") {
+ let command = "set -e;";
+ command += getBuildCommand(application, deployment.logPath);
+ await execAsyncRemote(application.serverId, command);
+ }
+ await mechanizeDockerContainer(application);
+ }
+ await updateDeploymentStatus(deployment.deploymentId, "done");
+ await updateApplicationStatus(applicationId, "done");
+ } catch (error) {
+ await updateDeploymentStatus(deployment.deploymentId, "error");
+ await updateApplicationStatus(applicationId, "error");
+ throw error;
+ }
+
+ return true;
+};
+
+export const getApplicationStats = async (appName: string) => {
+ const filter = {
+ status: ["running"],
+ label: [`com.docker.swarm.service.name=${appName}`],
+ };
+
+ const containers = await docker.listContainers({
+ filters: JSON.stringify(filter),
+ });
+
+ const container = containers[0];
+ if (!container || container?.State !== "running") {
+ return null;
+ }
+
+ const data = await getAdvancedStats(appName);
+
+ return data;
+};
diff --git a/packages/builders/src/services/auth.ts b/packages/builders/src/services/auth.ts
new file mode 100644
index 000000000..6bbf325c8
--- /dev/null
+++ b/packages/builders/src/services/auth.ts
@@ -0,0 +1,180 @@
+import { randomBytes } from "node:crypto";
+import { db } from "@/server/db";
+import {
+ admins,
+ type apiCreateAdmin,
+ type apiCreateUser,
+ auth,
+ users,
+} from "@/server/db/schema";
+import { getPublicIpWithFallback } from "@/server/wss/terminal";
+import { TRPCError } from "@trpc/server";
+import * as bcrypt from "bcrypt";
+import { eq } from "drizzle-orm";
+import encode from "hi-base32";
+import { TOTP } from "otpauth";
+import QRCode from "qrcode";
+
+export type Auth = typeof auth.$inferSelect;
+
+export const createAdmin = async (input: typeof apiCreateAdmin._type) => {
+ return await db.transaction(async (tx) => {
+ const hashedPassword = bcrypt.hashSync(input.password, 10);
+ const newAuth = await tx
+ .insert(auth)
+ .values({
+ email: input.email,
+ password: hashedPassword,
+ rol: "admin",
+ })
+ .returning()
+ .then((res) => res[0]);
+
+ if (!newAuth) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error to create the user",
+ });
+ }
+
+ await tx
+ .insert(admins)
+ .values({
+ authId: newAuth.id,
+ serverIp: await getPublicIpWithFallback(),
+ })
+ .returning();
+
+ return newAuth;
+ });
+};
+
+export const createUser = async (input: typeof apiCreateUser._type) => {
+ return await db.transaction(async (tx) => {
+ const hashedPassword = bcrypt.hashSync(input.password, 10);
+ const res = await tx
+ .update(auth)
+ .set({
+ password: hashedPassword,
+ })
+ .where(eq(auth.id, input.id))
+ .returning()
+ .then((res) => res[0]);
+
+ if (!res) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error to create the user",
+ });
+ }
+
+ const user = await tx
+ .update(users)
+ .set({
+ isRegistered: true,
+ expirationDate: undefined,
+ })
+ .where(eq(users.token, input.token))
+ .returning()
+ .then((res) => res[0]);
+
+ return user;
+ });
+};
+
+export const findAuthByEmail = async (email: string) => {
+ const result = await db.query.auth.findFirst({
+ where: eq(auth.email, email),
+ });
+ if (!result) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Auth not found",
+ });
+ }
+ return result;
+};
+
+export const findAuthById = async (authId: string) => {
+ const result = await db.query.auth.findFirst({
+ where: eq(auth.id, authId),
+ columns: {
+ password: false,
+ },
+ });
+ if (!result) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Auth not found",
+ });
+ }
+ return result;
+};
+
+export const updateAuthById = async (
+ authId: string,
+ authData: Partial,
+) => {
+ const result = await db
+ .update(auth)
+ .set({
+ ...authData,
+ })
+ .where(eq(auth.id, authId))
+ .returning();
+
+ return result[0];
+};
+
+export const generate2FASecret = async (authId: string) => {
+ const auth = await findAuthById(authId);
+
+ const base32_secret = generateBase32Secret();
+
+ const totp = new TOTP({
+ issuer: "Dokploy",
+ label: `${auth?.email}`,
+ algorithm: "SHA1",
+ digits: 6,
+ secret: base32_secret,
+ });
+
+ const otpauth_url = totp.toString();
+
+ const qrUrl = await QRCode.toDataURL(otpauth_url);
+
+ return {
+ qrCodeUrl: qrUrl,
+ secret: base32_secret,
+ };
+};
+
+export const verify2FA = async (
+ auth: Omit,
+ secret: string,
+ pin: string,
+) => {
+ const totp = new TOTP({
+ issuer: "Dokploy",
+ label: `${auth?.email}`,
+ algorithm: "SHA1",
+ digits: 6,
+ secret: secret,
+ });
+
+ const delta = totp.validate({ token: pin });
+
+ if (delta === null) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Invalid 2FA code",
+ });
+ }
+ return auth;
+};
+
+const generateBase32Secret = () => {
+ const buffer = randomBytes(15);
+ const base32 = encode.encode(buffer).replace(/=/g, "").substring(0, 24);
+ return base32;
+};
diff --git a/packages/builders/src/services/backup.ts b/packages/builders/src/services/backup.ts
new file mode 100644
index 000000000..55b38c225
--- /dev/null
+++ b/packages/builders/src/services/backup.ts
@@ -0,0 +1,71 @@
+import { db } from "@/server/db";
+import { type apiCreateBackup, backups } from "@/server/db/schema";
+import { TRPCError } from "@trpc/server";
+import { eq } from "drizzle-orm";
+
+export type Backup = typeof backups.$inferSelect;
+
+export type BackupSchedule = Awaited>;
+
+export const createBackup = async (input: typeof apiCreateBackup._type) => {
+ const newBackup = await db
+ .insert(backups)
+ .values({
+ ...input,
+ })
+ .returning()
+ .then((value) => value[0]);
+
+ if (!newBackup) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error to create the Backup",
+ });
+ }
+
+ return newBackup;
+};
+
+export const findBackupById = async (backupId: string) => {
+ const backup = await db.query.backups.findFirst({
+ where: eq(backups.backupId, backupId),
+ with: {
+ postgres: true,
+ mysql: true,
+ mariadb: true,
+ mongo: true,
+ destination: true,
+ },
+ });
+ if (!backup) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Backup not found",
+ });
+ }
+ return backup;
+};
+
+export const updateBackupById = async (
+ backupId: string,
+ backupData: Partial,
+) => {
+ const result = await db
+ .update(backups)
+ .set({
+ ...backupData,
+ })
+ .where(eq(backups.backupId, backupId))
+ .returning();
+
+ return result[0];
+};
+
+export const removeBackupById = async (backupId: string) => {
+ const result = await db
+ .delete(backups)
+ .where(eq(backups.backupId, backupId))
+ .returning();
+
+ return result[0];
+};
diff --git a/packages/builders/src/services/bitbucket.ts b/packages/builders/src/services/bitbucket.ts
new file mode 100644
index 000000000..11ae0903d
--- /dev/null
+++ b/packages/builders/src/services/bitbucket.ts
@@ -0,0 +1,88 @@
+import { db } from "@/server/db";
+import {
+ type apiCreateBitbucket,
+ type apiUpdateBitbucket,
+ bitbucket,
+ gitProvider,
+} from "@/server/db/schema";
+import { TRPCError } from "@trpc/server";
+import { eq } from "drizzle-orm";
+
+export type Bitbucket = typeof bitbucket.$inferSelect;
+
+export const createBitbucket = async (
+ input: typeof apiCreateBitbucket._type,
+) => {
+ return await db.transaction(async (tx) => {
+ const newGitProvider = await tx
+ .insert(gitProvider)
+ .values({
+ providerType: "bitbucket",
+ authId: input.authId,
+ name: input.name,
+ })
+ .returning()
+ .then((response) => response[0]);
+
+ if (!newGitProvider) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error to create the git provider",
+ });
+ }
+
+ await tx
+ .insert(bitbucket)
+ .values({
+ ...input,
+ gitProviderId: newGitProvider?.gitProviderId,
+ })
+ .returning()
+ .then((response) => response[0]);
+ });
+};
+
+export const findBitbucketById = async (bitbucketId: string) => {
+ const bitbucketProviderResult = await db.query.bitbucket.findFirst({
+ where: eq(bitbucket.bitbucketId, bitbucketId),
+ with: {
+ gitProvider: true,
+ },
+ });
+
+ if (!bitbucketProviderResult) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Bitbucket Provider not found",
+ });
+ }
+
+ return bitbucketProviderResult;
+};
+
+export const updateBitbucket = async (
+ bitbucketId: string,
+ input: typeof apiUpdateBitbucket._type,
+) => {
+ return await db.transaction(async (tx) => {
+ const result = await tx
+ .update(bitbucket)
+ .set({
+ ...input,
+ })
+ .where(eq(bitbucket.bitbucketId, bitbucketId))
+ .returning();
+
+ if (input.name) {
+ await tx
+ .update(gitProvider)
+ .set({
+ name: input.name,
+ })
+ .where(eq(gitProvider.gitProviderId, input.gitProviderId))
+ .returning();
+ }
+
+ return result[0];
+ });
+};
diff --git a/packages/builders/src/services/certificate.ts b/packages/builders/src/services/certificate.ts
new file mode 100644
index 000000000..3ed1dcd19
--- /dev/null
+++ b/packages/builders/src/services/certificate.ts
@@ -0,0 +1,108 @@
+import fs from "node:fs";
+import path from "node:path";
+import { paths } from "@/server/constants";
+import { db } from "@/server/db";
+import { type apiCreateCertificate, certificates } from "@/server/db/schema";
+import { removeDirectoryIfExistsContent } from "@/server/utils/filesystem/directory";
+import { TRPCError } from "@trpc/server";
+import { eq } from "drizzle-orm";
+import { dump } from "js-yaml";
+import type { z } from "zod";
+
+export type Certificate = typeof certificates.$inferSelect;
+
+export const findCertificateById = async (certificateId: string) => {
+ const certificate = await db.query.certificates.findFirst({
+ where: eq(certificates.certificateId, certificateId),
+ });
+
+ if (!certificate) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Certificate not found",
+ });
+ }
+
+ return certificate;
+};
+
+export const createCertificate = async (
+ certificateData: z.infer,
+) => {
+ const certificate = await db
+ .insert(certificates)
+ .values({
+ ...certificateData,
+ })
+ .returning();
+
+ if (!certificate || certificate[0] === undefined) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Failed to create the certificate",
+ });
+ }
+
+ const cer = certificate[0];
+
+ createCertificateFiles(cer);
+ return cer;
+};
+
+export const removeCertificateById = async (certificateId: string) => {
+ const { CERTIFICATES_PATH } = paths();
+ const certificate = await findCertificateById(certificateId);
+ const certDir = path.join(CERTIFICATES_PATH, certificate.certificatePath);
+
+ await removeDirectoryIfExistsContent(certDir);
+ const result = await db
+ .delete(certificates)
+ .where(eq(certificates.certificateId, certificateId))
+ .returning();
+
+ if (!result) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Failed to delete the certificate",
+ });
+ }
+
+ return result;
+};
+
+export const findCertificates = async () => {
+ return await db.query.certificates.findMany();
+};
+
+const createCertificateFiles = (certificate: Certificate) => {
+ const { CERTIFICATES_PATH } = paths();
+ const dockerPath = "/etc/traefik";
+ const certDir = path.join(CERTIFICATES_PATH, certificate.certificatePath);
+ const crtPath = path.join(certDir, "chain.crt");
+ const keyPath = path.join(certDir, "privkey.key");
+
+ const chainPath = path.join(dockerPath, certDir, "chain.crt");
+ const keyPathDocker = path.join(dockerPath, certDir, "privkey.key");
+
+ if (!fs.existsSync(certDir)) {
+ fs.mkdirSync(certDir, { recursive: true });
+ }
+
+ fs.writeFileSync(crtPath, certificate.certificateData);
+ fs.writeFileSync(keyPath, certificate.privateKey);
+
+ const traefikConfig = {
+ tls: {
+ certificates: [
+ {
+ certFile: chainPath,
+ keyFile: keyPathDocker,
+ },
+ ],
+ },
+ };
+
+ const yamlConfig = dump(traefikConfig);
+ const configFile = path.join(certDir, "certificate.yml");
+ fs.writeFileSync(configFile, yamlConfig);
+};
diff --git a/packages/builders/src/services/cluster.ts b/packages/builders/src/services/cluster.ts
new file mode 100644
index 000000000..ea71d1aee
--- /dev/null
+++ b/packages/builders/src/services/cluster.ts
@@ -0,0 +1,41 @@
+export interface DockerNode {
+ ID: string;
+ Version: {
+ Index: number;
+ };
+ CreatedAt: string;
+ UpdatedAt: string;
+ Spec: {
+ Name: string;
+ Labels: Record;
+ Role: "worker" | "manager";
+ Availability: "active" | "pause" | "drain";
+ };
+ Description: {
+ Hostname: string;
+ Platform: {
+ Architecture: string;
+ OS: string;
+ };
+ Resources: {
+ NanoCPUs: number;
+ MemoryBytes: number;
+ };
+ Engine: {
+ EngineVersion: string;
+ Plugins: Array<{
+ Type: string;
+ Name: string;
+ }>;
+ };
+ };
+ Status: {
+ State: "unknown" | "down" | "ready" | "disconnected";
+ Message: string;
+ Addr: string;
+ };
+ ManagerStatus?: {
+ Leader: boolean;
+ Addr: string;
+ };
+}
diff --git a/packages/builders/src/services/compose.ts b/packages/builders/src/services/compose.ts
new file mode 100644
index 000000000..bacee42dc
--- /dev/null
+++ b/packages/builders/src/services/compose.ts
@@ -0,0 +1,467 @@
+import { join } from "node:path";
+import { paths } from "@/server/constants";
+import { db } from "@/server/db";
+import { type apiCreateCompose, compose } from "@/server/db/schema";
+import { generateAppName } from "@/server/db/schema";
+import {
+ buildCompose,
+ getBuildComposeCommand,
+} from "@/server/utils/builders/compose";
+import { randomizeSpecificationFile } from "@/server/utils/docker/compose";
+import {
+ cloneCompose,
+ cloneComposeRemote,
+ loadDockerCompose,
+ loadDockerComposeRemote,
+} from "@/server/utils/docker/domain";
+import type { ComposeSpecification } from "@/server/utils/docker/types";
+import { sendBuildErrorNotifications } from "@/server/utils/notifications/build-error";
+import { sendBuildSuccessNotifications } from "@/server/utils/notifications/build-success";
+import { execAsync, execAsyncRemote } from "@/server/utils/process/execAsync";
+import {
+ cloneBitbucketRepository,
+ getBitbucketCloneCommand,
+} from "@/server/utils/providers/bitbucket";
+import {
+ cloneGitRepository,
+ getCustomGitCloneCommand,
+} from "@/server/utils/providers/git";
+import {
+ cloneGithubRepository,
+ getGithubCloneCommand,
+} from "@/server/utils/providers/github";
+import {
+ cloneGitlabRepository,
+ getGitlabCloneCommand,
+} from "@/server/utils/providers/gitlab";
+import {
+ createComposeFile,
+ getCreateComposeFileCommand,
+} from "@/server/utils/providers/raw";
+import { generatePassword } from "@/server/templates/utils";
+import { TRPCError } from "@trpc/server";
+import { eq } from "drizzle-orm";
+import { getDokployUrl } from "./admin";
+import { createDeploymentCompose, updateDeploymentStatus } from "./deployment";
+import { validUniqueServerAppName } from "./project";
+
+export type Compose = typeof compose.$inferSelect;
+
+export const createCompose = async (input: typeof apiCreateCompose._type) => {
+ input.appName =
+ `${input.appName}-${generatePassword(6)}` || generateAppName("compose");
+ if (input.appName) {
+ const valid = await validUniqueServerAppName(input.appName);
+
+ if (!valid) {
+ throw new TRPCError({
+ code: "CONFLICT",
+ message: "Service with this 'AppName' already exists",
+ });
+ }
+ }
+
+ const newDestination = await db
+ .insert(compose)
+ .values({
+ ...input,
+ composeFile: "",
+ })
+ .returning()
+ .then((value) => value[0]);
+
+ if (!newDestination) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error input: Inserting compose",
+ });
+ }
+
+ return newDestination;
+};
+
+export const createComposeByTemplate = async (
+ input: typeof compose.$inferInsert,
+) => {
+ if (input.appName) {
+ const valid = await validUniqueServerAppName(input.appName);
+
+ if (!valid) {
+ throw new TRPCError({
+ code: "CONFLICT",
+ message: "Service with this 'AppName' already exists",
+ });
+ }
+ }
+ const newDestination = await db
+ .insert(compose)
+ .values({
+ ...input,
+ })
+ .returning()
+ .then((value) => value[0]);
+
+ if (!newDestination) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error input: Inserting compose",
+ });
+ }
+
+ return newDestination;
+};
+
+export const findComposeById = async (composeId: string) => {
+ const result = await db.query.compose.findFirst({
+ where: eq(compose.composeId, composeId),
+ with: {
+ project: true,
+ deployments: true,
+ mounts: true,
+ domains: true,
+ github: true,
+ gitlab: true,
+ bitbucket: true,
+ server: true,
+ },
+ });
+ if (!result) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Compose not found",
+ });
+ }
+ return result;
+};
+
+export const loadServices = async (
+ composeId: string,
+ type: "fetch" | "cache" = "fetch",
+) => {
+ const compose = await findComposeById(composeId);
+
+ if (type === "fetch") {
+ if (compose.serverId) {
+ await cloneComposeRemote(compose);
+ } else {
+ await cloneCompose(compose);
+ }
+ }
+
+ let composeData: ComposeSpecification | null;
+
+ if (compose.serverId) {
+ composeData = await loadDockerComposeRemote(compose);
+ } else {
+ composeData = await loadDockerCompose(compose);
+ }
+
+ if (compose.randomize && composeData) {
+ const randomizedCompose = randomizeSpecificationFile(
+ composeData,
+ compose.suffix,
+ );
+ composeData = randomizedCompose;
+ }
+
+ if (!composeData?.services) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Services not found",
+ });
+ }
+
+ const services = Object.keys(composeData.services);
+
+ return [...services];
+};
+
+export const updateCompose = async (
+ composeId: string,
+ composeData: Partial,
+) => {
+ const composeResult = await db
+ .update(compose)
+ .set({
+ ...composeData,
+ })
+ .where(eq(compose.composeId, composeId))
+ .returning();
+
+ return composeResult[0];
+};
+
+export const deployCompose = async ({
+ composeId,
+ titleLog = "Manual deployment",
+ descriptionLog = "",
+}: {
+ composeId: string;
+ titleLog: string;
+ descriptionLog: string;
+}) => {
+ const compose = await findComposeById(composeId);
+ const buildLink = `${await getDokployUrl()}/dashboard/project/${compose.projectId}/services/compose/${compose.composeId}?tab=deployments`;
+ const deployment = await createDeploymentCompose({
+ composeId: composeId,
+ title: titleLog,
+ description: descriptionLog,
+ });
+
+ try {
+ if (compose.sourceType === "github") {
+ await cloneGithubRepository(compose, deployment.logPath, true);
+ } else if (compose.sourceType === "gitlab") {
+ await cloneGitlabRepository(compose, deployment.logPath, true);
+ } else if (compose.sourceType === "bitbucket") {
+ await cloneBitbucketRepository(compose, deployment.logPath, true);
+ } else if (compose.sourceType === "git") {
+ await cloneGitRepository(compose, deployment.logPath, true);
+ } else if (compose.sourceType === "raw") {
+ await createComposeFile(compose, deployment.logPath);
+ }
+ await buildCompose(compose, deployment.logPath);
+ await updateDeploymentStatus(deployment.deploymentId, "done");
+ await updateCompose(composeId, {
+ composeStatus: "done",
+ });
+
+ await sendBuildSuccessNotifications({
+ projectName: compose.project.name,
+ applicationName: compose.name,
+ applicationType: "compose",
+ buildLink,
+ });
+ } catch (error) {
+ await updateDeploymentStatus(deployment.deploymentId, "error");
+ await updateCompose(composeId, {
+ composeStatus: "error",
+ });
+ await sendBuildErrorNotifications({
+ projectName: compose.project.name,
+ applicationName: compose.name,
+ applicationType: "compose",
+ // @ts-ignore
+ errorMessage: error?.message || "Error to build",
+ buildLink,
+ });
+ throw error;
+ }
+};
+
+export const rebuildCompose = async ({
+ composeId,
+ titleLog = "Rebuild deployment",
+ descriptionLog = "",
+}: {
+ composeId: string;
+ titleLog: string;
+ descriptionLog: string;
+}) => {
+ const compose = await findComposeById(composeId);
+ const deployment = await createDeploymentCompose({
+ composeId: composeId,
+ title: titleLog,
+ description: descriptionLog,
+ });
+
+ try {
+ if (compose.serverId) {
+ await getBuildComposeCommand(compose, deployment.logPath);
+ } else {
+ await buildCompose(compose, deployment.logPath);
+ }
+
+ await updateDeploymentStatus(deployment.deploymentId, "done");
+ await updateCompose(composeId, {
+ composeStatus: "done",
+ });
+ } catch (error) {
+ await updateDeploymentStatus(deployment.deploymentId, "error");
+ await updateCompose(composeId, {
+ composeStatus: "error",
+ });
+ throw error;
+ }
+
+ return true;
+};
+
+export const deployRemoteCompose = async ({
+ composeId,
+ titleLog = "Manual deployment",
+ descriptionLog = "",
+}: {
+ composeId: string;
+ titleLog: string;
+ descriptionLog: string;
+}) => {
+ const compose = await findComposeById(composeId);
+ const buildLink = `${await getDokployUrl()}/dashboard/project/${compose.projectId}/services/compose/${compose.composeId}?tab=deployments`;
+ const deployment = await createDeploymentCompose({
+ composeId: composeId,
+ title: titleLog,
+ description: descriptionLog,
+ });
+ try {
+ if (compose.serverId) {
+ let command = "set -e;";
+
+ if (compose.sourceType === "github") {
+ command += await getGithubCloneCommand(
+ compose,
+ deployment.logPath,
+ true,
+ );
+ } else if (compose.sourceType === "gitlab") {
+ command += await getGitlabCloneCommand(
+ compose,
+ deployment.logPath,
+ true,
+ );
+ } else if (compose.sourceType === "bitbucket") {
+ command += await getBitbucketCloneCommand(
+ compose,
+ deployment.logPath,
+ true,
+ );
+ } else if (compose.sourceType === "git") {
+ command += await getCustomGitCloneCommand(
+ compose,
+ deployment.logPath,
+ true,
+ );
+ } else if (compose.sourceType === "raw") {
+ command += getCreateComposeFileCommand(compose, deployment.logPath);
+ }
+
+ await execAsyncRemote(compose.serverId, command);
+ await getBuildComposeCommand(compose, deployment.logPath);
+ }
+
+ await updateDeploymentStatus(deployment.deploymentId, "done");
+ await updateCompose(composeId, {
+ composeStatus: "done",
+ });
+
+ await sendBuildSuccessNotifications({
+ projectName: compose.project.name,
+ applicationName: compose.name,
+ applicationType: "compose",
+ buildLink,
+ });
+ } catch (error) {
+ console.log(error);
+ await updateDeploymentStatus(deployment.deploymentId, "error");
+ await updateCompose(composeId, {
+ composeStatus: "error",
+ });
+ await sendBuildErrorNotifications({
+ projectName: compose.project.name,
+ applicationName: compose.name,
+ applicationType: "compose",
+ // @ts-ignore
+ errorMessage: error?.message || "Error to build",
+ buildLink,
+ });
+ throw error;
+ }
+};
+
+export const rebuildRemoteCompose = async ({
+ composeId,
+ titleLog = "Rebuild deployment",
+ descriptionLog = "",
+}: {
+ composeId: string;
+ titleLog: string;
+ descriptionLog: string;
+}) => {
+ const compose = await findComposeById(composeId);
+ const deployment = await createDeploymentCompose({
+ composeId: composeId,
+ title: titleLog,
+ description: descriptionLog,
+ });
+
+ try {
+ if (compose.serverId) {
+ await getBuildComposeCommand(compose, deployment.logPath);
+ }
+
+ await updateDeploymentStatus(deployment.deploymentId, "done");
+ await updateCompose(composeId, {
+ composeStatus: "done",
+ });
+ } catch (error) {
+ await updateDeploymentStatus(deployment.deploymentId, "error");
+ await updateCompose(composeId, {
+ composeStatus: "error",
+ });
+ throw error;
+ }
+
+ return true;
+};
+
+export const removeCompose = async (compose: Compose) => {
+ try {
+ const { COMPOSE_PATH } = paths(!!compose.serverId);
+ const projectPath = join(COMPOSE_PATH, compose.appName);
+
+ if (compose.composeType === "stack") {
+ const command = `cd ${projectPath} && docker stack rm ${compose.appName} && rm -rf ${projectPath}`;
+ if (compose.serverId) {
+ await execAsyncRemote(compose.serverId, command);
+ } else {
+ await execAsync(command);
+ }
+ await execAsync(command, {
+ cwd: projectPath,
+ });
+ } else {
+ const command = `cd ${projectPath} && docker compose -p ${compose.appName} down && rm -rf ${projectPath}`;
+ if (compose.serverId) {
+ await execAsyncRemote(compose.serverId, command);
+ } else {
+ await execAsync(command, {
+ cwd: projectPath,
+ });
+ }
+ }
+ } catch (error) {
+ throw error;
+ }
+
+ return true;
+};
+
+export const stopCompose = async (composeId: string) => {
+ const compose = await findComposeById(composeId);
+ try {
+ const { COMPOSE_PATH } = paths(!!compose.serverId);
+ if (compose.composeType === "docker-compose") {
+ if (compose.serverId) {
+ await execAsyncRemote(
+ compose.serverId,
+ `cd ${join(COMPOSE_PATH, compose.appName)} && docker compose -p ${compose.appName} stop`,
+ );
+ } else {
+ await execAsync(`docker compose -p ${compose.appName} stop`, {
+ cwd: join(COMPOSE_PATH, compose.appName),
+ });
+ }
+ }
+
+ await updateCompose(composeId, {
+ composeStatus: "idle",
+ });
+ } catch (error) {
+ await updateCompose(composeId, {
+ composeStatus: "error",
+ });
+ throw error;
+ }
+
+ return true;
+};
diff --git a/packages/builders/src/services/deployment.ts b/packages/builders/src/services/deployment.ts
new file mode 100644
index 000000000..5ea39c577
--- /dev/null
+++ b/packages/builders/src/services/deployment.ts
@@ -0,0 +1,372 @@
+import { existsSync, promises as fsPromises } from "node:fs";
+import path from "node:path";
+import { paths } from "@/server/constants";
+import { db } from "@/server/db";
+import {
+ type apiCreateDeployment,
+ type apiCreateDeploymentCompose,
+ type apiCreateDeploymentServer,
+ deployments,
+} from "@/server/db/schema";
+import { removeDirectoryIfExistsContent } from "@/server/utils/filesystem/directory";
+import { TRPCError } from "@trpc/server";
+import { format } from "date-fns";
+import { desc, eq } from "drizzle-orm";
+import {
+ type Application,
+ findApplicationById,
+ updateApplicationStatus,
+} from "./application";
+import { type Compose, findComposeById, updateCompose } from "./compose";
+import { type Server, findServerById } from "./server";
+
+import { execAsyncRemote } from "@/server/utils/process/execAsync";
+
+export type Deployment = typeof deployments.$inferSelect;
+
+export const findDeploymentById = async (applicationId: string) => {
+ const application = await db.query.deployments.findFirst({
+ where: eq(deployments.applicationId, applicationId),
+ with: {
+ application: true,
+ },
+ });
+ if (!application) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Deployment not found",
+ });
+ }
+ return application;
+};
+
+export const createDeployment = async (
+ deployment: Omit<
+ typeof apiCreateDeployment._type,
+ "deploymentId" | "createdAt" | "status" | "logPath"
+ >,
+) => {
+ const application = await findApplicationById(deployment.applicationId);
+
+ try {
+ // await removeLastTenDeployments(deployment.applicationId);
+ const { LOGS_PATH } = paths(!!application.serverId);
+ const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
+ const fileName = `${application.appName}-${formattedDateTime}.log`;
+ const logFilePath = path.join(LOGS_PATH, application.appName, fileName);
+
+ if (application.serverId) {
+ const server = await findServerById(application.serverId);
+
+ const command = `
+ mkdir -p ${LOGS_PATH}/${application.appName};
+ echo "Initializing deployment" >> ${logFilePath};
+ `;
+
+ await execAsyncRemote(server.serverId, command);
+ } else {
+ await fsPromises.mkdir(path.join(LOGS_PATH, application.appName), {
+ recursive: true,
+ });
+ await fsPromises.writeFile(logFilePath, "Initializing deployment");
+ }
+
+ const deploymentCreate = await db
+ .insert(deployments)
+ .values({
+ applicationId: deployment.applicationId,
+ title: deployment.title || "Deployment",
+ status: "running",
+ logPath: logFilePath,
+ description: deployment.description || "",
+ })
+ .returning();
+ if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error to create the deployment",
+ });
+ }
+ return deploymentCreate[0];
+ } catch (error) {
+ await updateApplicationStatus(application.applicationId, "error");
+ console.log(error);
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error to create the deployment",
+ });
+ }
+};
+
+export const createDeploymentCompose = async (
+ deployment: Omit<
+ typeof apiCreateDeploymentCompose._type,
+ "deploymentId" | "createdAt" | "status" | "logPath"
+ >,
+) => {
+ const compose = await findComposeById(deployment.composeId);
+ try {
+ // await removeLastTenComposeDeployments(deployment.composeId);
+ const { LOGS_PATH } = paths(!!compose.serverId);
+ const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
+ const fileName = `${compose.appName}-${formattedDateTime}.log`;
+ const logFilePath = path.join(LOGS_PATH, compose.appName, fileName);
+
+ if (compose.serverId) {
+ const server = await findServerById(compose.serverId);
+
+ const command = `
+mkdir -p ${LOGS_PATH}/${compose.appName};
+echo "Initializing deployment" >> ${logFilePath};
+`;
+
+ await execAsyncRemote(server.serverId, command);
+ } else {
+ await fsPromises.mkdir(path.join(LOGS_PATH, compose.appName), {
+ recursive: true,
+ });
+ await fsPromises.writeFile(logFilePath, "Initializing deployment");
+ }
+
+ const deploymentCreate = await db
+ .insert(deployments)
+ .values({
+ composeId: deployment.composeId,
+ title: deployment.title || "Deployment",
+ description: deployment.description || "",
+ status: "running",
+ logPath: logFilePath,
+ })
+ .returning();
+ if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error to create the deployment",
+ });
+ }
+ return deploymentCreate[0];
+ } catch (error) {
+ await updateCompose(compose.composeId, {
+ composeStatus: "error",
+ });
+ console.log(error);
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error to create the deployment",
+ });
+ }
+};
+
+export const removeDeployment = async (deploymentId: string) => {
+ try {
+ const deployment = await db
+ .delete(deployments)
+ .where(eq(deployments.deploymentId, deploymentId))
+ .returning();
+ return deployment[0];
+ } catch (error) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error to delete this deployment",
+ });
+ }
+};
+
+export const removeDeploymentsByApplicationId = async (
+ applicationId: string,
+) => {
+ await db
+ .delete(deployments)
+ .where(eq(deployments.applicationId, applicationId))
+ .returning();
+};
+
+const removeLastTenDeployments = async (applicationId: string) => {
+ const deploymentList = await db.query.deployments.findMany({
+ where: eq(deployments.applicationId, applicationId),
+ orderBy: desc(deployments.createdAt),
+ });
+ if (deploymentList.length > 10) {
+ const deploymentsToDelete = deploymentList.slice(10);
+ for (const oldDeployment of deploymentsToDelete) {
+ const logPath = path.join(oldDeployment.logPath);
+ if (existsSync(logPath)) {
+ await fsPromises.unlink(logPath);
+ }
+ await removeDeployment(oldDeployment.deploymentId);
+ }
+ }
+};
+
+const removeLastTenComposeDeployments = async (composeId: string) => {
+ const deploymentList = await db.query.deployments.findMany({
+ where: eq(deployments.composeId, composeId),
+ orderBy: desc(deployments.createdAt),
+ });
+ if (deploymentList.length > 10) {
+ const deploymentsToDelete = deploymentList.slice(10);
+ for (const oldDeployment of deploymentsToDelete) {
+ const logPath = path.join(oldDeployment.logPath);
+ if (existsSync(logPath)) {
+ await fsPromises.unlink(logPath);
+ }
+ await removeDeployment(oldDeployment.deploymentId);
+ }
+ }
+};
+
+export const removeDeployments = async (application: Application) => {
+ const { appName, applicationId } = application;
+ const { LOGS_PATH } = paths(!!application.serverId);
+ const logsPath = path.join(LOGS_PATH, appName);
+ if (application.serverId) {
+ await execAsyncRemote(application.serverId, `rm -rf ${logsPath}`);
+ } else {
+ await removeDirectoryIfExistsContent(logsPath);
+ }
+ await removeDeploymentsByApplicationId(applicationId);
+};
+
+export const removeDeploymentsByComposeId = async (compose: Compose) => {
+ const { appName } = compose;
+ const { LOGS_PATH } = paths(!!compose.serverId);
+ const logsPath = path.join(LOGS_PATH, appName);
+ if (compose.serverId) {
+ await execAsyncRemote(compose.serverId, `rm -rf ${logsPath}`);
+ } else {
+ await removeDirectoryIfExistsContent(logsPath);
+ }
+
+ await db
+ .delete(deployments)
+ .where(eq(deployments.composeId, compose.composeId))
+ .returning();
+};
+
+export const findAllDeploymentsByApplicationId = async (
+ applicationId: string,
+) => {
+ const deploymentsList = await db.query.deployments.findMany({
+ where: eq(deployments.applicationId, applicationId),
+ orderBy: desc(deployments.createdAt),
+ });
+ return deploymentsList;
+};
+
+export const findAllDeploymentsByComposeId = async (composeId: string) => {
+ const deploymentsList = await db.query.deployments.findMany({
+ where: eq(deployments.composeId, composeId),
+ orderBy: desc(deployments.createdAt),
+ });
+ return deploymentsList;
+};
+
+export const updateDeployment = async (
+ deploymentId: string,
+ deploymentData: Partial,
+) => {
+ const application = await db
+ .update(deployments)
+ .set({
+ ...deploymentData,
+ })
+ .where(eq(deployments.deploymentId, deploymentId))
+ .returning();
+
+ return application;
+};
+
+export const updateDeploymentStatus = async (
+ deploymentId: string,
+ deploymentStatus: Deployment["status"],
+) => {
+ const application = await db
+ .update(deployments)
+ .set({
+ status: deploymentStatus,
+ })
+ .where(eq(deployments.deploymentId, deploymentId))
+ .returning();
+
+ return application;
+};
+
+export const createServerDeployment = async (
+ deployment: Omit<
+ typeof apiCreateDeploymentServer._type,
+ "deploymentId" | "createdAt" | "status" | "logPath"
+ >,
+) => {
+ try {
+ const { LOGS_PATH } = paths();
+
+ const server = await findServerById(deployment.serverId);
+ await removeLastFiveDeployments(deployment.serverId);
+ const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
+ const fileName = `${server.appName}-${formattedDateTime}.log`;
+ const logFilePath = path.join(LOGS_PATH, server.appName, fileName);
+ await fsPromises.mkdir(path.join(LOGS_PATH, server.appName), {
+ recursive: true,
+ });
+ await fsPromises.writeFile(logFilePath, "Initializing Setup Server");
+ const deploymentCreate = await db
+ .insert(deployments)
+ .values({
+ serverId: server.serverId,
+ title: deployment.title || "Deployment",
+ description: deployment.description || "",
+ status: "running",
+ logPath: logFilePath,
+ })
+ .returning();
+ if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error to create the deployment",
+ });
+ }
+ return deploymentCreate[0];
+ } catch (error) {
+ console.log(error);
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error to create the deployment",
+ });
+ }
+};
+
+export const removeLastFiveDeployments = async (serverId: string) => {
+ const deploymentList = await db.query.deployments.findMany({
+ where: eq(deployments.serverId, serverId),
+ orderBy: desc(deployments.createdAt),
+ });
+ if (deploymentList.length >= 5) {
+ const deploymentsToDelete = deploymentList.slice(4);
+ for (const oldDeployment of deploymentsToDelete) {
+ const logPath = path.join(oldDeployment.logPath);
+ if (existsSync(logPath)) {
+ await fsPromises.unlink(logPath);
+ }
+ await removeDeployment(oldDeployment.deploymentId);
+ }
+ }
+};
+
+export const removeDeploymentsByServerId = async (server: Server) => {
+ const { LOGS_PATH } = paths();
+ const { appName } = server;
+ const logsPath = path.join(LOGS_PATH, appName);
+ await removeDirectoryIfExistsContent(logsPath);
+ await db
+ .delete(deployments)
+ .where(eq(deployments.serverId, server.serverId))
+ .returning();
+};
+
+export const findAllDeploymentsByServerId = async (serverId: string) => {
+ const deploymentsList = await db.query.deployments.findMany({
+ where: eq(deployments.serverId, serverId),
+ orderBy: desc(deployments.createdAt),
+ });
+ return deploymentsList;
+};
diff --git a/packages/builders/src/services/destination.ts b/packages/builders/src/services/destination.ts
new file mode 100644
index 000000000..dfc15649b
--- /dev/null
+++ b/packages/builders/src/services/destination.ts
@@ -0,0 +1,67 @@
+import { db } from "@/server/db";
+import { type apiCreateDestination, destinations } from "@/server/db/schema";
+import { TRPCError } from "@trpc/server";
+import { eq } from "drizzle-orm";
+import { findAdmin } from "./admin";
+
+export type Destination = typeof destinations.$inferSelect;
+
+export const createDestintation = async (
+ input: typeof apiCreateDestination._type,
+) => {
+ const adminResponse = await findAdmin();
+ const newDestination = await db
+ .insert(destinations)
+ .values({
+ ...input,
+ adminId: adminResponse.adminId,
+ })
+ .returning()
+ .then((value) => value[0]);
+
+ if (!newDestination) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error input: Inserting destination",
+ });
+ }
+
+ return newDestination;
+};
+
+export const findDestinationById = async (destinationId: string) => {
+ const destination = await db.query.destinations.findFirst({
+ where: eq(destinations.destinationId, destinationId),
+ });
+ if (!destination) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Destination not found",
+ });
+ }
+ return destination;
+};
+
+export const removeDestinationById = async (destinationId: string) => {
+ const result = await db
+ .delete(destinations)
+ .where(eq(destinations.destinationId, destinationId))
+ .returning();
+
+ return result[0];
+};
+
+export const updateDestinationById = async (
+ destinationId: string,
+ destinationData: Partial,
+) => {
+ const result = await db
+ .update(destinations)
+ .set({
+ ...destinationData,
+ })
+ .where(eq(destinations.destinationId, destinationId))
+ .returning();
+
+ return result[0];
+};
diff --git a/packages/builders/src/services/docker.ts b/packages/builders/src/services/docker.ts
new file mode 100644
index 000000000..d611a11d1
--- /dev/null
+++ b/packages/builders/src/services/docker.ts
@@ -0,0 +1,223 @@
+import { execAsync, execAsyncRemote } from "@/server/utils/process/execAsync";
+
+export const getContainers = async (serverId?: string | null) => {
+ try {
+ const command =
+ "docker ps -a --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | Image: {{.Image}} | Ports: {{.Ports}} | State: {{.State}} | Status: {{.Status}}'";
+ let stdout = "";
+ let stderr = "";
+
+ if (serverId) {
+ const result = await execAsyncRemote(serverId, command);
+
+ stdout = result.stdout;
+ stderr = result.stderr;
+ } else {
+ const result = await execAsync(command);
+ stdout = result.stdout;
+ stderr = result.stderr;
+ }
+ if (stderr) {
+ console.error(`Error: ${stderr}`);
+ return;
+ }
+
+ const lines = stdout.trim().split("\n");
+
+ const containers = lines
+ .map((line) => {
+ const parts = line.split(" | ");
+ const containerId = parts[0]
+ ? parts[0].replace("CONTAINER ID : ", "").trim()
+ : "No container id";
+ const name = parts[1]
+ ? parts[1].replace("Name: ", "").trim()
+ : "No container name";
+ const image = parts[2]
+ ? parts[2].replace("Image: ", "").trim()
+ : "No image";
+ const ports = parts[3]
+ ? parts[3].replace("Ports: ", "").trim()
+ : "No ports";
+ const state = parts[4]
+ ? parts[4].replace("State: ", "").trim()
+ : "No state";
+ const status = parts[5]
+ ? parts[5].replace("Status: ", "").trim()
+ : "No status";
+ return {
+ containerId,
+ name,
+ image,
+ ports,
+ state,
+ status,
+ serverId,
+ };
+ })
+ .filter((container) => !container.name.includes("dokploy"));
+
+ return containers;
+ } catch (error) {
+ console.error(error);
+
+ return [];
+ }
+};
+
+export const getConfig = async (
+ containerId: string,
+ serverId?: string | null,
+) => {
+ try {
+ const command = `docker inspect ${containerId} --format='{{json .}}'`;
+ let stdout = "";
+ let stderr = "";
+ if (serverId) {
+ const result = await execAsyncRemote(serverId, command);
+ stdout = result.stdout;
+ stderr = result.stderr;
+ } else {
+ const result = await execAsync(command);
+ stdout = result.stdout;
+ stderr = result.stderr;
+ }
+
+ if (stderr) {
+ console.error(`Error: ${stderr}`);
+ return;
+ }
+
+ const config = JSON.parse(stdout);
+
+ return config;
+ } catch (error) {}
+};
+
+export const getContainersByAppNameMatch = async (
+ appName: string,
+ appType?: "stack" | "docker-compose",
+ serverId?: string,
+) => {
+ try {
+ let result: string[] = [];
+ const cmd =
+ "docker ps -a --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | State: {{.State}}'";
+
+ const command =
+ appType === "docker-compose"
+ ? `${cmd} --filter='label=com.docker.compose.project=${appName}'`
+ : `${cmd} | grep ${appName}`;
+ if (serverId) {
+ const { stdout, stderr } = await execAsyncRemote(serverId, command);
+
+ if (stderr) {
+ return [];
+ }
+
+ if (!stdout) return [];
+ result = stdout.trim().split("\n");
+ } else {
+ const { stdout, stderr } = await execAsync(command);
+
+ if (stderr) {
+ return [];
+ }
+
+ if (!stdout) return [];
+
+ result = stdout.trim().split("\n");
+ }
+
+ const containers = result.map((line) => {
+ const parts = line.split(" | ");
+ const containerId = parts[0]
+ ? parts[0].replace("CONTAINER ID : ", "").trim()
+ : "No container id";
+ const name = parts[1]
+ ? parts[1].replace("Name: ", "").trim()
+ : "No container name";
+
+ const state = parts[2]
+ ? parts[2].replace("State: ", "").trim()
+ : "No state";
+ return {
+ containerId,
+ name,
+ state,
+ };
+ });
+
+ return containers || [];
+ } catch (error) {}
+
+ return [];
+};
+
+export const getContainersByAppLabel = async (
+ appName: string,
+ serverId?: string,
+) => {
+ try {
+ let stdout = "";
+ let stderr = "";
+
+ const command = `docker ps --filter "label=com.docker.swarm.service.name=${appName}" --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | State: {{.State}}'`;
+ if (serverId) {
+ const result = await execAsyncRemote(serverId, command);
+ stdout = result.stdout;
+ stderr = result.stderr;
+ } else {
+ const result = await execAsync(command);
+ stdout = result.stdout;
+ stderr = result.stderr;
+ }
+ if (stderr) {
+ console.error(`Error: ${stderr}`);
+ return;
+ }
+
+ if (!stdout) return [];
+
+ const lines = stdout.trim().split("\n");
+
+ const containers = lines.map((line) => {
+ const parts = line.split(" | ");
+ const containerId = parts[0]
+ ? parts[0].replace("CONTAINER ID : ", "").trim()
+ : "No container id";
+ const name = parts[1]
+ ? parts[1].replace("Name: ", "").trim()
+ : "No container name";
+ const state = parts[2]
+ ? parts[2].replace("State: ", "").trim()
+ : "No state";
+ return {
+ containerId,
+ name,
+ state,
+ };
+ });
+
+ return containers || [];
+ } catch (error) {}
+
+ return [];
+};
+
+export const containerRestart = async (containerId: string) => {
+ try {
+ const { stdout, stderr } = await execAsync(
+ `docker container restart ${containerId}`,
+ );
+
+ if (stderr) {
+ console.error(`Error: ${stderr}`);
+ return;
+ }
+
+ const config = JSON.parse(stdout);
+
+ return config;
+ } catch (error) {}
+};
diff --git a/packages/builders/src/services/domain.ts b/packages/builders/src/services/domain.ts
new file mode 100644
index 000000000..2ff663c17
--- /dev/null
+++ b/packages/builders/src/services/domain.ts
@@ -0,0 +1,136 @@
+import { db } from "@/server/db";
+import { type apiCreateDomain, domains } from "../db/schema";
+import { manageDomain } from "@/server/utils/traefik/domain";
+import { generateRandomDomain } from "@/server/templates/utils";
+import { TRPCError } from "@trpc/server";
+import { eq } from "drizzle-orm";
+import { findAdmin, findAdminById } from "./admin";
+import { findApplicationById } from "./application";
+import { findServerById } from "./server";
+
+export type Domain = typeof domains.$inferSelect;
+
+export const createDomain = async (input: typeof apiCreateDomain._type) => {
+ const result = await db.transaction(async (tx) => {
+ const domain = await tx
+ .insert(domains)
+ .values({
+ ...input,
+ })
+ .returning()
+ .then((response) => response[0]);
+
+ if (!domain) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error creating domain",
+ });
+ }
+
+ if (domain.applicationId) {
+ const application = await findApplicationById(domain.applicationId);
+ await manageDomain(application, domain);
+ }
+
+ return domain;
+ });
+
+ return result;
+};
+
+export const generateTraefikMeDomain = async (
+ appName: string,
+ adminId: string,
+ serverId?: string,
+) => {
+ if (serverId) {
+ const server = await findServerById(serverId);
+ return generateRandomDomain({
+ serverIp: server.ipAddress,
+ projectName: appName,
+ });
+ }
+
+ if (process.env.NODE_ENV === "development") {
+ return generateRandomDomain({
+ serverIp: "",
+ projectName: appName,
+ });
+ }
+ const admin = await findAdminById(adminId);
+ return generateRandomDomain({
+ serverIp: admin?.serverIp || "",
+ projectName: appName,
+ });
+};
+
+export const generateWildcardDomain = (
+ appName: string,
+ serverDomain: string,
+) => {
+ return `${appName}-${serverDomain}`;
+};
+
+export const findDomainById = async (domainId: string) => {
+ const domain = await db.query.domains.findFirst({
+ where: eq(domains.domainId, domainId),
+ with: {
+ application: true,
+ },
+ });
+ if (!domain) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Domain not found",
+ });
+ }
+ return domain;
+};
+
+export const findDomainsByApplicationId = async (applicationId: string) => {
+ const domainsArray = await db.query.domains.findMany({
+ where: eq(domains.applicationId, applicationId),
+ with: {
+ application: true,
+ },
+ });
+
+ return domainsArray;
+};
+
+export const findDomainsByComposeId = async (composeId: string) => {
+ const domainsArray = await db.query.domains.findMany({
+ where: eq(domains.composeId, composeId),
+ with: {
+ compose: true,
+ },
+ });
+
+ return domainsArray;
+};
+
+export const updateDomainById = async (
+ domainId: string,
+ domainData: Partial,
+) => {
+ const domain = await db
+ .update(domains)
+ .set({
+ ...domainData,
+ })
+ .where(eq(domains.domainId, domainId))
+ .returning();
+
+ return domain[0];
+};
+
+export const removeDomainById = async (domainId: string) => {
+ await findDomainById(domainId);
+ // TODO: fix order
+ const result = await db
+ .delete(domains)
+ .where(eq(domains.domainId, domainId))
+ .returning();
+
+ return result[0];
+};
diff --git a/packages/builders/src/services/git-provider.ts b/packages/builders/src/services/git-provider.ts
new file mode 100644
index 000000000..6846de4dd
--- /dev/null
+++ b/packages/builders/src/services/git-provider.ts
@@ -0,0 +1,29 @@
+import { db } from "@/server/db";
+import { type apiCreateGithub, gitProvider, github } from "@/server/db/schema";
+import { TRPCError } from "@trpc/server";
+import { eq } from "drizzle-orm";
+
+export type GitProvider = typeof gitProvider.$inferSelect;
+
+export const removeGitProvider = async (gitProviderId: string) => {
+ const result = await db
+ .delete(gitProvider)
+ .where(eq(gitProvider.gitProviderId, gitProviderId))
+ .returning();
+
+ return result[0];
+};
+
+export const updateGitProvider = async (
+ gitProviderId: string,
+ input: Partial,
+) => {
+ return await db
+ .update(gitProvider)
+ .set({
+ ...input,
+ })
+ .where(eq(gitProvider.gitProviderId, gitProviderId))
+ .returning()
+ .then((response) => response[0]);
+};
diff --git a/packages/builders/src/services/github.ts b/packages/builders/src/services/github.ts
new file mode 100644
index 000000000..64f8ec939
--- /dev/null
+++ b/packages/builders/src/services/github.ts
@@ -0,0 +1,67 @@
+import { db } from "@/server/db";
+import { type apiCreateGithub, gitProvider, github } from "@/server/db/schema";
+import { TRPCError } from "@trpc/server";
+import { eq } from "drizzle-orm";
+
+export type Github = typeof github.$inferSelect;
+export const createGithub = async (input: typeof apiCreateGithub._type) => {
+ return await db.transaction(async (tx) => {
+ const newGitProvider = await tx
+ .insert(gitProvider)
+ .values({
+ providerType: "github",
+ authId: input.authId,
+ name: input.name,
+ })
+ .returning()
+ .then((response) => response[0]);
+
+ if (!newGitProvider) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error to create the git provider",
+ });
+ }
+
+ return await tx
+ .insert(github)
+ .values({
+ ...input,
+ gitProviderId: newGitProvider?.gitProviderId,
+ })
+ .returning()
+ .then((response) => response[0]);
+ });
+};
+
+export const findGithubById = async (githubId: string) => {
+ const githubProviderResult = await db.query.github.findFirst({
+ where: eq(github.githubId, githubId),
+ with: {
+ gitProvider: true,
+ },
+ });
+
+ if (!githubProviderResult) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Github Provider not found",
+ });
+ }
+
+ return githubProviderResult;
+};
+
+export const updateGithub = async (
+ githubId: string,
+ input: Partial,
+) => {
+ return await db
+ .update(github)
+ .set({
+ ...input,
+ })
+ .where(eq(github.githubId, githubId))
+ .returning()
+ .then((response) => response[0]);
+};
diff --git a/packages/builders/src/services/gitlab.ts b/packages/builders/src/services/gitlab.ts
new file mode 100644
index 000000000..528fb1b6a
--- /dev/null
+++ b/packages/builders/src/services/gitlab.ts
@@ -0,0 +1,74 @@
+import { db } from "@/server/db";
+import {
+ type apiCreateGitlab,
+ type bitbucket,
+ gitProvider,
+ type github,
+ gitlab,
+} from "@/server/db/schema";
+import { TRPCError } from "@trpc/server";
+import { eq } from "drizzle-orm";
+
+export type Gitlab = typeof gitlab.$inferSelect;
+
+export const createGitlab = async (input: typeof apiCreateGitlab._type) => {
+ return await db.transaction(async (tx) => {
+ const newGitProvider = await tx
+ .insert(gitProvider)
+ .values({
+ providerType: "gitlab",
+ authId: input.authId,
+ name: input.name,
+ })
+ .returning()
+ .then((response) => response[0]);
+
+ if (!newGitProvider) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error to create the git provider",
+ });
+ }
+
+ await tx
+ .insert(gitlab)
+ .values({
+ ...input,
+ gitProviderId: newGitProvider?.gitProviderId,
+ })
+ .returning()
+ .then((response) => response[0]);
+ });
+};
+
+export const findGitlabById = async (gitlabId: string) => {
+ const gitlabProviderResult = await db.query.gitlab.findFirst({
+ where: eq(gitlab.gitlabId, gitlabId),
+ with: {
+ gitProvider: true,
+ },
+ });
+
+ if (!gitlabProviderResult) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Gitlab Provider not found",
+ });
+ }
+
+ return gitlabProviderResult;
+};
+
+export const updateGitlab = async (
+ gitlabId: string,
+ input: Partial,
+) => {
+ return await db
+ .update(gitlab)
+ .set({
+ ...input,
+ })
+ .where(eq(gitlab.gitlabId, gitlabId))
+ .returning()
+ .then((response) => response[0]);
+};
diff --git a/packages/builders/src/services/mariadb.ts b/packages/builders/src/services/mariadb.ts
new file mode 100644
index 000000000..237d00e95
--- /dev/null
+++ b/packages/builders/src/services/mariadb.ts
@@ -0,0 +1,147 @@
+import { db } from "@/server/db";
+import { type apiCreateMariaDB, backups, mariadb } from "@/server/db/schema";
+import { generateAppName } from "@/server/db/schema";
+import { buildMariadb } from "@/server/utils/databases/mariadb";
+import { pullImage } from "@/server/utils/docker/utils";
+import { generatePassword } from "@/server/templates/utils";
+import { TRPCError } from "@trpc/server";
+import { eq, getTableColumns } from "drizzle-orm";
+import { validUniqueServerAppName } from "./project";
+
+import { execAsyncRemote } from "@/server/utils/process/execAsync";
+
+export type Mariadb = typeof mariadb.$inferSelect;
+
+export const createMariadb = async (input: typeof apiCreateMariaDB._type) => {
+ input.appName =
+ `${input.appName}-${generatePassword(6)}` || generateAppName("mariadb");
+ if (input.appName) {
+ const valid = await validUniqueServerAppName(input.appName);
+
+ if (!valid) {
+ throw new TRPCError({
+ code: "CONFLICT",
+ message: "Service with this 'AppName' already exists",
+ });
+ }
+ }
+
+ const newMariadb = await db
+ .insert(mariadb)
+ .values({
+ ...input,
+ databasePassword: input.databasePassword
+ ? input.databasePassword
+ : generatePassword(),
+ databaseRootPassword: input.databaseRootPassword
+ ? input.databaseRootPassword
+ : generatePassword(),
+ })
+ .returning()
+ .then((value) => value[0]);
+
+ if (!newMariadb) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error input: Inserting mariadb database",
+ });
+ }
+
+ return newMariadb;
+};
+
+// https://github.com/drizzle-team/drizzle-orm/discussions/1483#discussioncomment-7523881
+export const findMariadbById = async (mariadbId: string) => {
+ const result = await db.query.mariadb.findFirst({
+ where: eq(mariadb.mariadbId, mariadbId),
+ with: {
+ project: true,
+ mounts: true,
+ server: true,
+ backups: {
+ with: {
+ destination: true,
+ },
+ },
+ },
+ });
+ if (!result) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Mariadb not found",
+ });
+ }
+ return result;
+};
+
+export const updateMariadbById = async (
+ mariadbId: string,
+ mariadbData: Partial,
+) => {
+ const result = await db
+ .update(mariadb)
+ .set({
+ ...mariadbData,
+ })
+ .where(eq(mariadb.mariadbId, mariadbId))
+ .returning();
+
+ return result[0];
+};
+
+export const removeMariadbById = async (mariadbId: string) => {
+ const result = await db
+ .delete(mariadb)
+ .where(eq(mariadb.mariadbId, mariadbId))
+ .returning();
+
+ return result[0];
+};
+
+export const findMariadbByBackupId = async (backupId: string) => {
+ const result = await db
+ .select({
+ ...getTableColumns(mariadb),
+ })
+ .from(mariadb)
+ .innerJoin(backups, eq(mariadb.mariadbId, backups.mariadbId))
+ .where(eq(backups.backupId, backupId))
+ .limit(1);
+
+ if (!result || !result[0]) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "MariaDB not found",
+ });
+ }
+ return result[0];
+};
+
+export const deployMariadb = async (mariadbId: string) => {
+ const mariadb = await findMariadbById(mariadbId);
+ try {
+ if (mariadb.serverId) {
+ await execAsyncRemote(
+ mariadb.serverId,
+ `docker pull ${mariadb.dockerImage}`,
+ );
+ } else {
+ await pullImage(mariadb.dockerImage);
+ }
+
+ await buildMariadb(mariadb);
+ await updateMariadbById(mariadbId, {
+ applicationStatus: "done",
+ });
+ } catch (error) {
+ await updateMariadbById(mariadbId, {
+ applicationStatus: "error",
+ });
+
+ throw new TRPCError({
+ code: "INTERNAL_SERVER_ERROR",
+ message: `Error on deploy mariadb${error}`,
+ });
+ }
+ return mariadb;
+};
diff --git a/packages/builders/src/services/mongo.ts b/packages/builders/src/services/mongo.ts
new file mode 100644
index 000000000..41c44f09b
--- /dev/null
+++ b/packages/builders/src/services/mongo.ts
@@ -0,0 +1,140 @@
+import { db } from "@/server/db";
+import { type apiCreateMongo, backups, mongo } from "@/server/db/schema";
+import { generateAppName } from "@/server/db/schema";
+import { buildMongo } from "@/server/utils/databases/mongo";
+import { pullImage } from "@/server/utils/docker/utils";
+import { generatePassword } from "@/server/templates/utils";
+import { TRPCError } from "@trpc/server";
+import { eq, getTableColumns } from "drizzle-orm";
+import { validUniqueServerAppName } from "./project";
+
+import { execAsyncRemote } from "@/server/utils/process/execAsync";
+
+export type Mongo = typeof mongo.$inferSelect;
+
+export const createMongo = async (input: typeof apiCreateMongo._type) => {
+ input.appName =
+ `${input.appName}-${generatePassword(6)}` || generateAppName("postgres");
+ if (input.appName) {
+ const valid = await validUniqueServerAppName(input.appName);
+
+ if (!valid) {
+ throw new TRPCError({
+ code: "CONFLICT",
+ message: "Service with this 'AppName' already exists",
+ });
+ }
+ }
+
+ const newMongo = await db
+ .insert(mongo)
+ .values({
+ ...input,
+ databasePassword: input.databasePassword
+ ? input.databasePassword
+ : generatePassword(),
+ })
+ .returning()
+ .then((value) => value[0]);
+
+ if (!newMongo) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error input: Inserting mongo database",
+ });
+ }
+
+ return newMongo;
+};
+
+export const findMongoById = async (mongoId: string) => {
+ const result = await db.query.mongo.findFirst({
+ where: eq(mongo.mongoId, mongoId),
+ with: {
+ project: true,
+ mounts: true,
+ server: true,
+ backups: {
+ with: {
+ destination: true,
+ },
+ },
+ },
+ });
+ if (!result) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Mongo not found",
+ });
+ }
+ return result;
+};
+
+export const updateMongoById = async (
+ mongoId: string,
+ postgresData: Partial,
+) => {
+ const result = await db
+ .update(mongo)
+ .set({
+ ...postgresData,
+ })
+ .where(eq(mongo.mongoId, mongoId))
+ .returning();
+
+ return result[0];
+};
+
+export const findMongoByBackupId = async (backupId: string) => {
+ const result = await db
+ .select({
+ ...getTableColumns(mongo),
+ })
+ .from(mongo)
+ .innerJoin(backups, eq(mongo.mongoId, backups.mongoId))
+ .where(eq(backups.backupId, backupId))
+ .limit(1);
+
+ if (!result || !result[0]) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Mongo not found",
+ });
+ }
+ return result[0];
+};
+
+export const removeMongoById = async (mongoId: string) => {
+ const result = await db
+ .delete(mongo)
+ .where(eq(mongo.mongoId, mongoId))
+ .returning();
+
+ return result[0];
+};
+
+export const deployMongo = async (mongoId: string) => {
+ const mongo = await findMongoById(mongoId);
+ try {
+ if (mongo.serverId) {
+ await execAsyncRemote(mongo.serverId, `docker pull ${mongo.dockerImage}`);
+ } else {
+ await pullImage(mongo.dockerImage);
+ }
+
+ await buildMongo(mongo);
+ await updateMongoById(mongoId, {
+ applicationStatus: "done",
+ });
+ } catch (error) {
+ await updateMongoById(mongoId, {
+ applicationStatus: "error",
+ });
+
+ throw new TRPCError({
+ code: "INTERNAL_SERVER_ERROR",
+ message: `Error on deploy mongo${error}`,
+ });
+ }
+ return mongo;
+};
diff --git a/packages/builders/src/services/mount.ts b/packages/builders/src/services/mount.ts
new file mode 100644
index 000000000..97528aa5f
--- /dev/null
+++ b/packages/builders/src/services/mount.ts
@@ -0,0 +1,280 @@
+import path from "node:path";
+import { paths } from "@/server/constants";
+import { db } from "@/server/db";
+import {
+ type ServiceType,
+ type apiCreateMount,
+ mounts,
+} from "@/server/db/schema";
+import { createFile, getCreateFileCommand } from "@/server/utils/docker/utils";
+import { removeFileOrDirectory } from "@/server/utils/filesystem/directory";
+import { execAsyncRemote } from "@/server/utils/process/execAsync";
+import { TRPCError } from "@trpc/server";
+import { type SQL, eq, sql } from "drizzle-orm";
+
+export type Mount = typeof mounts.$inferSelect;
+
+export const createMount = async (input: typeof apiCreateMount._type) => {
+ try {
+ const { serviceId, ...rest } = input;
+ const value = await db
+ .insert(mounts)
+ .values({
+ ...rest,
+ ...(input.serviceType === "application" && {
+ applicationId: serviceId,
+ }),
+ ...(input.serviceType === "postgres" && {
+ postgresId: serviceId,
+ }),
+ ...(input.serviceType === "mariadb" && {
+ mariadbId: serviceId,
+ }),
+ ...(input.serviceType === "mongo" && {
+ mongoId: serviceId,
+ }),
+ ...(input.serviceType === "mysql" && {
+ mysqlId: serviceId,
+ }),
+ ...(input.serviceType === "redis" && {
+ redisId: serviceId,
+ }),
+ ...(input.serviceType === "compose" && {
+ composeId: serviceId,
+ }),
+ })
+ .returning()
+ .then((value) => value[0]);
+
+ if (!value) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error input: Inserting mount",
+ });
+ }
+
+ if (value.type === "file") {
+ await createFileMount(value.mountId);
+ }
+ return value;
+ } catch (error) {
+ console.log(error);
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error to create the mount",
+ cause: error,
+ });
+ }
+};
+
+export const createFileMount = async (mountId: string) => {
+ try {
+ const mount = await findMountById(mountId);
+ const baseFilePath = await getBaseFilesPath(mountId);
+
+ const serverId = await getServerId(mount);
+
+ if (serverId) {
+ const command = getCreateFileCommand(
+ baseFilePath,
+ mount.filePath || "",
+ mount.content || "",
+ );
+ await execAsyncRemote(serverId, command);
+ } else {
+ await createFile(baseFilePath, mount.filePath || "", mount.content || "");
+ }
+ } catch (error) {
+ console.log(`Error to create the file mount: ${error}`);
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error to create the mount",
+ cause: error,
+ });
+ }
+};
+
+export const findMountById = async (mountId: string) => {
+ const mount = await db.query.mounts.findFirst({
+ where: eq(mounts.mountId, mountId),
+ with: {
+ application: true,
+ postgres: true,
+ mariadb: true,
+ mongo: true,
+ mysql: true,
+ redis: true,
+ compose: true,
+ },
+ });
+ if (!mount) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Mount not found",
+ });
+ }
+ return mount;
+};
+
+export const updateMount = async (
+ mountId: string,
+ mountData: Partial,
+) => {
+ return await db.transaction(async (transaction) => {
+ const mount = await db
+ .update(mounts)
+ .set({
+ ...mountData,
+ })
+ .where(eq(mounts.mountId, mountId))
+ .returning()
+ .then((value) => value[0]);
+
+ if (!mount) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Mount not found",
+ });
+ }
+
+ if (mount.type === "file") {
+ await deleteFileMount(mountId);
+ await createFileMount(mountId);
+ }
+ return mount;
+ });
+};
+
+export const findMountsByApplicationId = async (
+ serviceId: string,
+ serviceType: ServiceType,
+) => {
+ const sqlChunks: SQL[] = [];
+
+ switch (serviceType) {
+ case "application":
+ sqlChunks.push(eq(mounts.applicationId, serviceId));
+ break;
+ case "postgres":
+ sqlChunks.push(eq(mounts.postgresId, serviceId));
+ break;
+ case "mariadb":
+ sqlChunks.push(eq(mounts.mariadbId, serviceId));
+ break;
+ case "mongo":
+ sqlChunks.push(eq(mounts.mongoId, serviceId));
+ break;
+ case "mysql":
+ sqlChunks.push(eq(mounts.mysqlId, serviceId));
+ break;
+ case "redis":
+ sqlChunks.push(eq(mounts.redisId, serviceId));
+ break;
+ default:
+ throw new Error(`Unknown service type: ${serviceType}`);
+ }
+ const mount = await db.query.mounts.findMany({
+ where: sql.join(sqlChunks, sql.raw(" ")),
+ });
+
+ return mount;
+};
+
+export const deleteMount = async (mountId: string) => {
+ const { type } = await findMountById(mountId);
+
+ if (type === "file") {
+ await deleteFileMount(mountId);
+ }
+
+ const deletedMount = await db
+ .delete(mounts)
+ .where(eq(mounts.mountId, mountId))
+ .returning();
+ return deletedMount[0];
+};
+
+export const deleteFileMount = async (mountId: string) => {
+ const mount = await findMountById(mountId);
+ if (!mount.filePath) return;
+ const basePath = await getBaseFilesPath(mountId);
+
+ const fullPath = path.join(basePath, mount.filePath);
+ try {
+ const serverId = await getServerId(mount);
+ if (serverId) {
+ const command = `rm -rf ${fullPath}`;
+ await execAsyncRemote(serverId, command);
+ } else {
+ await removeFileOrDirectory(fullPath);
+ }
+ } catch (error) {}
+};
+
+export const getBaseFilesPath = async (mountId: string) => {
+ const mount = await findMountById(mountId);
+
+ let absoluteBasePath = "";
+ let appName = "";
+ let directoryPath = "";
+
+ if (mount.serviceType === "application" && mount.application) {
+ const { APPLICATIONS_PATH } = paths(!!mount.application.serverId);
+ absoluteBasePath = path.resolve(APPLICATIONS_PATH);
+ appName = mount.application.appName;
+ } else if (mount.serviceType === "postgres" && mount.postgres) {
+ const { APPLICATIONS_PATH } = paths(!!mount.postgres.serverId);
+ absoluteBasePath = path.resolve(APPLICATIONS_PATH);
+ appName = mount.postgres.appName;
+ } else if (mount.serviceType === "mariadb" && mount.mariadb) {
+ const { APPLICATIONS_PATH } = paths(!!mount.mariadb.serverId);
+ absoluteBasePath = path.resolve(APPLICATIONS_PATH);
+ appName = mount.mariadb.appName;
+ } else if (mount.serviceType === "mongo" && mount.mongo) {
+ const { APPLICATIONS_PATH } = paths(!!mount.mongo.serverId);
+ absoluteBasePath = path.resolve(APPLICATIONS_PATH);
+ appName = mount.mongo.appName;
+ } else if (mount.serviceType === "mysql" && mount.mysql) {
+ const { APPLICATIONS_PATH } = paths(!!mount.mysql.serverId);
+ absoluteBasePath = path.resolve(APPLICATIONS_PATH);
+ appName = mount.mysql.appName;
+ } else if (mount.serviceType === "redis" && mount.redis) {
+ const { APPLICATIONS_PATH } = paths(!!mount.redis.serverId);
+ absoluteBasePath = path.resolve(APPLICATIONS_PATH);
+ appName = mount.redis.appName;
+ } else if (mount.serviceType === "compose" && mount.compose) {
+ const { COMPOSE_PATH } = paths(!!mount.compose.serverId);
+ appName = mount.compose.appName;
+ absoluteBasePath = path.resolve(COMPOSE_PATH);
+ }
+ directoryPath = path.join(absoluteBasePath, appName, "files");
+
+ return directoryPath;
+};
+
+type MountNested = Awaited>;
+export const getServerId = async (mount: MountNested) => {
+ if (mount.serviceType === "application" && mount?.application?.serverId) {
+ return mount.application.serverId;
+ }
+ if (mount.serviceType === "postgres" && mount?.postgres?.serverId) {
+ return mount.postgres.serverId;
+ }
+ if (mount.serviceType === "mariadb" && mount?.mariadb?.serverId) {
+ return mount.mariadb.serverId;
+ }
+ if (mount.serviceType === "mongo" && mount?.mongo?.serverId) {
+ return mount.mongo.serverId;
+ }
+ if (mount.serviceType === "mysql" && mount?.mysql?.serverId) {
+ return mount.mysql.serverId;
+ }
+ if (mount.serviceType === "redis" && mount?.redis?.serverId) {
+ return mount.redis.serverId;
+ }
+ if (mount.serviceType === "compose" && mount?.compose?.serverId) {
+ return mount.compose.serverId;
+ }
+
+ return null;
+};
diff --git a/packages/builders/src/services/mysql.ts b/packages/builders/src/services/mysql.ts
new file mode 100644
index 000000000..e221c9b75
--- /dev/null
+++ b/packages/builders/src/services/mysql.ts
@@ -0,0 +1,144 @@
+import { db } from "@/server/db";
+import { type apiCreateMySql, backups, mysql } from "@/server/db/schema";
+import { generateAppName } from "@/server/db/schema";
+import { buildMysql } from "@/server/utils/databases/mysql";
+import { pullImage } from "@/server/utils/docker/utils";
+import { generatePassword } from "@/server/templates/utils";
+import { TRPCError } from "@trpc/server";
+import { eq, getTableColumns } from "drizzle-orm";
+import { validUniqueServerAppName } from "./project";
+
+import { execAsyncRemote } from "@/server/utils/process/execAsync";
+
+export type MySql = typeof mysql.$inferSelect;
+
+export const createMysql = async (input: typeof apiCreateMySql._type) => {
+ input.appName =
+ `${input.appName}-${generatePassword(6)}` || generateAppName("mysql");
+
+ if (input.appName) {
+ const valid = await validUniqueServerAppName(input.appName);
+
+ if (!valid) {
+ throw new TRPCError({
+ code: "CONFLICT",
+ message: "Service with this 'AppName' already exists",
+ });
+ }
+ }
+
+ const newMysql = await db
+ .insert(mysql)
+ .values({
+ ...input,
+ databasePassword: input.databasePassword
+ ? input.databasePassword
+ : generatePassword(),
+ databaseRootPassword: input.databaseRootPassword
+ ? input.databaseRootPassword
+ : generatePassword(),
+ })
+ .returning()
+ .then((value) => value[0]);
+
+ if (!newMysql) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error input: Inserting mysql database",
+ });
+ }
+
+ return newMysql;
+};
+
+// https://github.com/drizzle-team/drizzle-orm/discussions/1483#discussioncomment-7523881
+export const findMySqlById = async (mysqlId: string) => {
+ const result = await db.query.mysql.findFirst({
+ where: eq(mysql.mysqlId, mysqlId),
+ with: {
+ project: true,
+ mounts: true,
+ server: true,
+ backups: {
+ with: {
+ destination: true,
+ },
+ },
+ },
+ });
+ if (!result) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "MySql not found",
+ });
+ }
+ return result;
+};
+
+export const updateMySqlById = async (
+ mysqlId: string,
+ mysqlData: Partial,
+) => {
+ const result = await db
+ .update(mysql)
+ .set({
+ ...mysqlData,
+ })
+ .where(eq(mysql.mysqlId, mysqlId))
+ .returning();
+
+ return result[0];
+};
+
+export const findMySqlByBackupId = async (backupId: string) => {
+ const result = await db
+ .select({
+ ...getTableColumns(mysql),
+ })
+ .from(mysql)
+ .innerJoin(backups, eq(mysql.mysqlId, backups.mysqlId))
+ .where(eq(backups.backupId, backupId))
+ .limit(1);
+
+ if (!result || !result[0]) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Mysql not found",
+ });
+ }
+ return result[0];
+};
+
+export const removeMySqlById = async (mysqlId: string) => {
+ const result = await db
+ .delete(mysql)
+ .where(eq(mysql.mysqlId, mysqlId))
+ .returning();
+
+ return result[0];
+};
+
+export const deployMySql = async (mysqlId: string) => {
+ const mysql = await findMySqlById(mysqlId);
+ try {
+ if (mysql.serverId) {
+ await execAsyncRemote(mysql.serverId, `docker pull ${mysql.dockerImage}`);
+ } else {
+ await pullImage(mysql.dockerImage);
+ }
+
+ await buildMysql(mysql);
+ await updateMySqlById(mysqlId, {
+ applicationStatus: "done",
+ });
+ } catch (error) {
+ await updateMySqlById(mysqlId, {
+ applicationStatus: "error",
+ });
+ throw new TRPCError({
+ code: "INTERNAL_SERVER_ERROR",
+ message: `Error on deploy mysql${error}`,
+ });
+ }
+ return mysql;
+};
diff --git a/packages/builders/src/services/notification.ts b/packages/builders/src/services/notification.ts
new file mode 100644
index 000000000..15d8f9d7c
--- /dev/null
+++ b/packages/builders/src/services/notification.ts
@@ -0,0 +1,409 @@
+import { db } from "@/server/db";
+import {
+ type apiCreateDiscord,
+ type apiCreateEmail,
+ type apiCreateSlack,
+ type apiCreateTelegram,
+ type apiUpdateDiscord,
+ type apiUpdateEmail,
+ type apiUpdateSlack,
+ type apiUpdateTelegram,
+ discord,
+ email,
+ notifications,
+ slack,
+ telegram,
+} from "@/server/db/schema";
+import { TRPCError } from "@trpc/server";
+import { eq } from "drizzle-orm";
+
+export type Notification = typeof notifications.$inferSelect;
+
+export const createSlackNotification = async (
+ input: typeof apiCreateSlack._type,
+) => {
+ await db.transaction(async (tx) => {
+ const newSlack = await tx
+ .insert(slack)
+ .values({
+ channel: input.channel,
+ webhookUrl: input.webhookUrl,
+ })
+ .returning()
+ .then((value) => value[0]);
+
+ if (!newSlack) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error input: Inserting slack",
+ });
+ }
+
+ const newDestination = await tx
+ .insert(notifications)
+ .values({
+ slackId: newSlack.slackId,
+ name: input.name,
+ appDeploy: input.appDeploy,
+ appBuildError: input.appBuildError,
+ databaseBackup: input.databaseBackup,
+ dokployRestart: input.dokployRestart,
+ dockerCleanup: input.dockerCleanup,
+ notificationType: "slack",
+ })
+ .returning()
+ .then((value) => value[0]);
+
+ if (!newDestination) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error input: Inserting notification",
+ });
+ }
+
+ return newDestination;
+ });
+};
+
+export const updateSlackNotification = async (
+ input: typeof apiUpdateSlack._type,
+) => {
+ await db.transaction(async (tx) => {
+ const newDestination = await tx
+ .update(notifications)
+ .set({
+ name: input.name,
+ appDeploy: input.appDeploy,
+ appBuildError: input.appBuildError,
+ databaseBackup: input.databaseBackup,
+ dokployRestart: input.dokployRestart,
+ dockerCleanup: input.dockerCleanup,
+ })
+ .where(eq(notifications.notificationId, input.notificationId))
+ .returning()
+ .then((value) => value[0]);
+
+ if (!newDestination) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error Updating notification",
+ });
+ }
+
+ await tx
+ .update(slack)
+ .set({
+ channel: input.channel,
+ webhookUrl: input.webhookUrl,
+ })
+ .where(eq(slack.slackId, input.slackId))
+ .returning()
+ .then((value) => value[0]);
+
+ return newDestination;
+ });
+};
+
+export const createTelegramNotification = async (
+ input: typeof apiCreateTelegram._type,
+) => {
+ await db.transaction(async (tx) => {
+ const newTelegram = await tx
+ .insert(telegram)
+ .values({
+ botToken: input.botToken,
+ chatId: input.chatId,
+ })
+ .returning()
+ .then((value) => value[0]);
+
+ if (!newTelegram) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error input: Inserting telegram",
+ });
+ }
+
+ const newDestination = await tx
+ .insert(notifications)
+ .values({
+ telegramId: newTelegram.telegramId,
+ name: input.name,
+ appDeploy: input.appDeploy,
+ appBuildError: input.appBuildError,
+ databaseBackup: input.databaseBackup,
+ dokployRestart: input.dokployRestart,
+ dockerCleanup: input.dockerCleanup,
+ notificationType: "telegram",
+ })
+ .returning()
+ .then((value) => value[0]);
+
+ if (!newDestination) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error input: Inserting notification",
+ });
+ }
+
+ return newDestination;
+ });
+};
+
+export const updateTelegramNotification = async (
+ input: typeof apiUpdateTelegram._type,
+) => {
+ await db.transaction(async (tx) => {
+ const newDestination = await tx
+ .update(notifications)
+ .set({
+ name: input.name,
+ appDeploy: input.appDeploy,
+ appBuildError: input.appBuildError,
+ databaseBackup: input.databaseBackup,
+ dokployRestart: input.dokployRestart,
+ dockerCleanup: input.dockerCleanup,
+ })
+ .where(eq(notifications.notificationId, input.notificationId))
+ .returning()
+ .then((value) => value[0]);
+
+ if (!newDestination) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error Updating notification",
+ });
+ }
+
+ await tx
+ .update(telegram)
+ .set({
+ botToken: input.botToken,
+ chatId: input.chatId,
+ })
+ .where(eq(telegram.telegramId, input.telegramId))
+ .returning()
+ .then((value) => value[0]);
+
+ return newDestination;
+ });
+};
+
+export const createDiscordNotification = async (
+ input: typeof apiCreateDiscord._type,
+) => {
+ await db.transaction(async (tx) => {
+ const newDiscord = await tx
+ .insert(discord)
+ .values({
+ webhookUrl: input.webhookUrl,
+ })
+ .returning()
+ .then((value) => value[0]);
+
+ if (!newDiscord) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error input: Inserting discord",
+ });
+ }
+
+ const newDestination = await tx
+ .insert(notifications)
+ .values({
+ discordId: newDiscord.discordId,
+ name: input.name,
+ appDeploy: input.appDeploy,
+ appBuildError: input.appBuildError,
+ databaseBackup: input.databaseBackup,
+ dokployRestart: input.dokployRestart,
+ dockerCleanup: input.dockerCleanup,
+ notificationType: "discord",
+ })
+ .returning()
+ .then((value) => value[0]);
+
+ if (!newDestination) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error input: Inserting notification",
+ });
+ }
+
+ return newDestination;
+ });
+};
+
+export const updateDiscordNotification = async (
+ input: typeof apiUpdateDiscord._type,
+) => {
+ await db.transaction(async (tx) => {
+ const newDestination = await tx
+ .update(notifications)
+ .set({
+ name: input.name,
+ appDeploy: input.appDeploy,
+ appBuildError: input.appBuildError,
+ databaseBackup: input.databaseBackup,
+ dokployRestart: input.dokployRestart,
+ dockerCleanup: input.dockerCleanup,
+ })
+ .where(eq(notifications.notificationId, input.notificationId))
+ .returning()
+ .then((value) => value[0]);
+
+ if (!newDestination) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error Updating notification",
+ });
+ }
+
+ await tx
+ .update(discord)
+ .set({
+ webhookUrl: input.webhookUrl,
+ })
+ .where(eq(discord.discordId, input.discordId))
+ .returning()
+ .then((value) => value[0]);
+
+ return newDestination;
+ });
+};
+
+export const createEmailNotification = async (
+ input: typeof apiCreateEmail._type,
+) => {
+ await db.transaction(async (tx) => {
+ const newEmail = await tx
+ .insert(email)
+ .values({
+ smtpServer: input.smtpServer,
+ smtpPort: input.smtpPort,
+ username: input.username,
+ password: input.password,
+ fromAddress: input.fromAddress,
+ toAddresses: input.toAddresses,
+ })
+ .returning()
+ .then((value) => value[0]);
+
+ if (!newEmail) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error input: Inserting email",
+ });
+ }
+
+ const newDestination = await tx
+ .insert(notifications)
+ .values({
+ emailId: newEmail.emailId,
+ name: input.name,
+ appDeploy: input.appDeploy,
+ appBuildError: input.appBuildError,
+ databaseBackup: input.databaseBackup,
+ dokployRestart: input.dokployRestart,
+ dockerCleanup: input.dockerCleanup,
+ notificationType: "email",
+ })
+ .returning()
+ .then((value) => value[0]);
+
+ if (!newDestination) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error input: Inserting notification",
+ });
+ }
+
+ return newDestination;
+ });
+};
+
+export const updateEmailNotification = async (
+ input: typeof apiUpdateEmail._type,
+) => {
+ await db.transaction(async (tx) => {
+ const newDestination = await tx
+ .update(notifications)
+ .set({
+ name: input.name,
+ appDeploy: input.appDeploy,
+ appBuildError: input.appBuildError,
+ databaseBackup: input.databaseBackup,
+ dokployRestart: input.dokployRestart,
+ dockerCleanup: input.dockerCleanup,
+ })
+ .where(eq(notifications.notificationId, input.notificationId))
+ .returning()
+ .then((value) => value[0]);
+
+ if (!newDestination) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error Updating notification",
+ });
+ }
+
+ await tx
+ .update(email)
+ .set({
+ smtpServer: input.smtpServer,
+ smtpPort: input.smtpPort,
+ username: input.username,
+ password: input.password,
+ fromAddress: input.fromAddress,
+ toAddresses: input.toAddresses,
+ })
+ .where(eq(email.emailId, input.emailId))
+ .returning()
+ .then((value) => value[0]);
+
+ return newDestination;
+ });
+};
+
+export const findNotificationById = async (notificationId: string) => {
+ const notification = await db.query.notifications.findFirst({
+ where: eq(notifications.notificationId, notificationId),
+ with: {
+ slack: true,
+ telegram: true,
+ discord: true,
+ email: true,
+ },
+ });
+ if (!notification) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Notification not found",
+ });
+ }
+ return notification;
+};
+
+export const removeNotificationById = async (notificationId: string) => {
+ const result = await db
+ .delete(notifications)
+ .where(eq(notifications.notificationId, notificationId))
+ .returning();
+
+ return result[0];
+};
+
+export const updateNotificationById = async (
+ notificationId: string,
+ notificationData: Partial,
+) => {
+ const result = await db
+ .update(notifications)
+ .set({
+ ...notificationData,
+ })
+ .where(eq(notifications.notificationId, notificationId))
+ .returning();
+
+ return result[0];
+};
diff --git a/packages/builders/src/services/port.ts b/packages/builders/src/services/port.ts
new file mode 100644
index 000000000..19229a9c9
--- /dev/null
+++ b/packages/builders/src/services/port.ts
@@ -0,0 +1,62 @@
+import { db } from "@/server/db";
+import { type apiCreatePort, ports } from "@/server/db/schema";
+import { TRPCError } from "@trpc/server";
+import { eq } from "drizzle-orm";
+
+export type Port = typeof ports.$inferSelect;
+
+export const createPort = async (input: typeof apiCreatePort._type) => {
+ const newPort = await db
+ .insert(ports)
+ .values({
+ ...input,
+ })
+ .returning()
+ .then((value) => value[0]);
+
+ if (!newPort) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error input: Inserting port",
+ });
+ }
+
+ return newPort;
+};
+
+export const finPortById = async (portId: string) => {
+ const result = await db.query.ports.findFirst({
+ where: eq(ports.portId, portId),
+ });
+ if (!result) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Port not found",
+ });
+ }
+ return result;
+};
+
+export const removePortById = async (portId: string) => {
+ const result = await db
+ .delete(ports)
+ .where(eq(ports.portId, portId))
+ .returning();
+
+ return result[0];
+};
+
+export const updatePortById = async (
+ portId: string,
+ portData: Partial,
+) => {
+ const result = await db
+ .update(ports)
+ .set({
+ ...portData,
+ })
+ .where(eq(ports.portId, portId))
+ .returning();
+
+ return result[0];
+};
diff --git a/packages/builders/src/services/postgres.ts b/packages/builders/src/services/postgres.ts
new file mode 100644
index 000000000..857df630b
--- /dev/null
+++ b/packages/builders/src/services/postgres.ts
@@ -0,0 +1,142 @@
+import { db } from "@/server/db";
+import { type apiCreatePostgres, backups, postgres } from "@/server/db/schema";
+import { generateAppName } from "@/server/db/schema";
+import { buildPostgres } from "@/server/utils/databases/postgres";
+import { pullImage } from "@/server/utils/docker/utils";
+import { generatePassword } from "@/server/templates/utils";
+import { TRPCError } from "@trpc/server";
+import { eq, getTableColumns } from "drizzle-orm";
+import { validUniqueServerAppName } from "./project";
+
+import { execAsyncRemote } from "@/server/utils/process/execAsync";
+
+export type Postgres = typeof postgres.$inferSelect;
+
+export const createPostgres = async (input: typeof apiCreatePostgres._type) => {
+ input.appName =
+ `${input.appName}-${generatePassword(6)}` || generateAppName("postgres");
+ if (input.appName) {
+ const valid = await validUniqueServerAppName(input.appName);
+
+ if (!valid) {
+ throw new TRPCError({
+ code: "CONFLICT",
+ message: "Service with this 'AppName' already exists",
+ });
+ }
+ }
+
+ const newPostgres = await db
+ .insert(postgres)
+ .values({
+ ...input,
+ databasePassword: input.databasePassword
+ ? input.databasePassword
+ : generatePassword(),
+ })
+ .returning()
+ .then((value) => value[0]);
+
+ if (!newPostgres) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error input: Inserting postgresql database",
+ });
+ }
+
+ return newPostgres;
+};
+export const findPostgresById = async (postgresId: string) => {
+ const result = await db.query.postgres.findFirst({
+ where: eq(postgres.postgresId, postgresId),
+ with: {
+ project: true,
+ mounts: true,
+ server: true,
+ backups: {
+ with: {
+ destination: true,
+ },
+ },
+ },
+ });
+ if (!result) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Postgres not found",
+ });
+ }
+ return result;
+};
+
+export const findPostgresByBackupId = async (backupId: string) => {
+ const result = await db
+ .select({
+ ...getTableColumns(postgres),
+ })
+ .from(postgres)
+ .innerJoin(backups, eq(postgres.postgresId, backups.postgresId))
+ .where(eq(backups.backupId, backupId))
+ .limit(1);
+
+ if (!result || !result[0]) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Postgres not found",
+ });
+ }
+ return result[0];
+};
+
+export const updatePostgresById = async (
+ postgresId: string,
+ postgresData: Partial,
+) => {
+ const result = await db
+ .update(postgres)
+ .set({
+ ...postgresData,
+ })
+ .where(eq(postgres.postgresId, postgresId))
+ .returning();
+
+ return result[0];
+};
+
+export const removePostgresById = async (postgresId: string) => {
+ const result = await db
+ .delete(postgres)
+ .where(eq(postgres.postgresId, postgresId))
+ .returning();
+
+ return result[0];
+};
+
+export const deployPostgres = async (postgresId: string) => {
+ const postgres = await findPostgresById(postgresId);
+ try {
+ const promises = [];
+ if (postgres.serverId) {
+ const result = await execAsyncRemote(
+ postgres.serverId,
+ `docker pull ${postgres.dockerImage}`,
+ );
+ } else {
+ await pullImage(postgres.dockerImage);
+ }
+
+ await buildPostgres(postgres);
+ await updatePostgresById(postgresId, {
+ applicationStatus: "done",
+ });
+ } catch (error) {
+ await updatePostgresById(postgresId, {
+ applicationStatus: "error",
+ });
+ throw new TRPCError({
+ code: "INTERNAL_SERVER_ERROR",
+ message: `Error on deploy postgres${error}`,
+ });
+ }
+ return postgres;
+};
diff --git a/packages/builders/src/services/project.ts b/packages/builders/src/services/project.ts
new file mode 100644
index 000000000..902dd06b3
--- /dev/null
+++ b/packages/builders/src/services/project.ts
@@ -0,0 +1,124 @@
+import { db } from "@/server/db";
+import {
+ type apiCreateProject,
+ applications,
+ mariadb,
+ mongo,
+ mysql,
+ postgres,
+ projects,
+ redis,
+} from "@/server/db/schema";
+import { TRPCError } from "@trpc/server";
+import { eq } from "drizzle-orm";
+
+export type Project = typeof projects.$inferSelect;
+
+export const createProject = async (
+ input: typeof apiCreateProject._type,
+ adminId: string,
+) => {
+ const newProject = await db
+ .insert(projects)
+ .values({
+ ...input,
+ adminId: adminId,
+ })
+ .returning()
+ .then((value) => value[0]);
+
+ if (!newProject) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error to create the project",
+ });
+ }
+
+ return newProject;
+};
+
+export const findProjectById = async (projectId: string) => {
+ const project = await db.query.projects.findFirst({
+ where: eq(projects.projectId, projectId),
+ with: {
+ applications: true,
+ mariadb: true,
+ mongo: true,
+ mysql: true,
+ postgres: true,
+ redis: true,
+ compose: true,
+ },
+ });
+ if (!project) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Project not found",
+ });
+ }
+ return project;
+};
+
+export const deleteProject = async (projectId: string) => {
+ const project = await db
+ .delete(projects)
+ .where(eq(projects.projectId, projectId))
+ .returning()
+ .then((value) => value[0]);
+
+ return project;
+};
+
+export const updateProjectById = async (
+ projectId: string,
+ projectData: Partial,
+) => {
+ const result = await db
+ .update(projects)
+ .set({
+ ...projectData,
+ })
+ .where(eq(projects.projectId, projectId))
+ .returning()
+ .then((res) => res[0]);
+
+ return result;
+};
+
+export const validUniqueServerAppName = async (appName: string) => {
+ const query = await db.query.projects.findMany({
+ with: {
+ applications: {
+ where: eq(applications.appName, appName),
+ },
+ mariadb: {
+ where: eq(mariadb.appName, appName),
+ },
+ mongo: {
+ where: eq(mongo.appName, appName),
+ },
+ mysql: {
+ where: eq(mysql.appName, appName),
+ },
+ postgres: {
+ where: eq(postgres.appName, appName),
+ },
+ redis: {
+ where: eq(redis.appName, appName),
+ },
+ },
+ });
+
+ // Filter out items with non-empty fields
+ const nonEmptyProjects = query.filter(
+ (project) =>
+ project.applications.length > 0 ||
+ project.mariadb.length > 0 ||
+ project.mongo.length > 0 ||
+ project.mysql.length > 0 ||
+ project.postgres.length > 0 ||
+ project.redis.length > 0,
+ );
+
+ return nonEmptyProjects.length === 0;
+};
diff --git a/packages/builders/src/services/redirect.ts b/packages/builders/src/services/redirect.ts
new file mode 100644
index 000000000..972603f2e
--- /dev/null
+++ b/packages/builders/src/services/redirect.ts
@@ -0,0 +1,123 @@
+import { db } from "@/server/db";
+import { type apiCreateRedirect, redirects } from "@/server/db/schema";
+import {
+ createRedirectMiddleware,
+ removeRedirectMiddleware,
+ updateRedirectMiddleware,
+} from "@/server/utils/traefik/redirect";
+import { TRPCError } from "@trpc/server";
+import { desc, eq } from "drizzle-orm";
+import type { z } from "zod";
+import { findApplicationById } from "./application";
+export type Redirect = typeof redirects.$inferSelect;
+
+export const findRedirectById = async (redirectId: string) => {
+ const application = await db.query.redirects.findFirst({
+ where: eq(redirects.redirectId, redirectId),
+ });
+ if (!application) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Redirect not found",
+ });
+ }
+ return application;
+};
+
+export const createRedirect = async (
+ redirectData: z.infer,
+) => {
+ try {
+ await db.transaction(async (tx) => {
+ const redirect = await tx
+ .insert(redirects)
+ .values({
+ ...redirectData,
+ })
+ .returning()
+ .then((res) => res[0]);
+
+ if (!redirect) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error to create the redirect",
+ });
+ }
+
+ const application = await findApplicationById(redirect.applicationId);
+
+ createRedirectMiddleware(application, redirect);
+ });
+
+ return true;
+ } catch (error) {
+ console.log(error);
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error to create this redirect",
+ cause: error,
+ });
+ }
+};
+
+export const removeRedirectById = async (redirectId: string) => {
+ try {
+ const response = await db
+ .delete(redirects)
+ .where(eq(redirects.redirectId, redirectId))
+ .returning()
+ .then((res) => res[0]);
+
+ if (!response) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Redirect not found",
+ });
+ }
+
+ const application = await findApplicationById(response.applicationId);
+
+ await removeRedirectMiddleware(application, response);
+
+ return response;
+ } catch (error) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error to remove this redirect",
+ cause: error,
+ });
+ }
+};
+
+export const updateRedirectById = async (
+ redirectId: string,
+ redirectData: Partial,
+) => {
+ try {
+ const redirect = await db
+ .update(redirects)
+ .set({
+ ...redirectData,
+ })
+ .where(eq(redirects.redirectId, redirectId))
+ .returning()
+ .then((res) => res[0]);
+
+ if (!redirect) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Redirect not found",
+ });
+ }
+ const application = await findApplicationById(redirect.applicationId);
+
+ await updateRedirectMiddleware(application, redirect);
+
+ return redirect;
+ } catch (error) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error to update this redirect",
+ });
+ }
+};
diff --git a/packages/builders/src/services/redis.ts b/packages/builders/src/services/redis.ts
new file mode 100644
index 000000000..498c5f4cf
--- /dev/null
+++ b/packages/builders/src/services/redis.ts
@@ -0,0 +1,117 @@
+import { db } from "@/server/db";
+import { type apiCreateRedis, redis } from "@/server/db/schema";
+import { generateAppName } from "@/server/db/schema";
+import { buildRedis } from "@/server/utils/databases/redis";
+import { pullImage } from "@/server/utils/docker/utils";
+import { generatePassword } from "@/server/templates/utils";
+import { TRPCError } from "@trpc/server";
+import { eq } from "drizzle-orm";
+import { validUniqueServerAppName } from "./project";
+
+import { execAsyncRemote } from "@/server/utils/process/execAsync";
+
+export type Redis = typeof redis.$inferSelect;
+
+// https://github.com/drizzle-team/drizzle-orm/discussions/1483#discussioncomment-7523881
+export const createRedis = async (input: typeof apiCreateRedis._type) => {
+ input.appName =
+ `${input.appName}-${generatePassword(6)}` || generateAppName("redis");
+ if (input.appName) {
+ const valid = await validUniqueServerAppName(input.appName);
+
+ if (!valid) {
+ throw new TRPCError({
+ code: "CONFLICT",
+ message: "Service with this 'AppName' already exists",
+ });
+ }
+ }
+
+ const newRedis = await db
+ .insert(redis)
+ .values({
+ ...input,
+ databasePassword: input.databasePassword
+ ? input.databasePassword
+ : generatePassword(),
+ })
+ .returning()
+ .then((value) => value[0]);
+
+ if (!newRedis) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error input: Inserting redis database",
+ });
+ }
+
+ return newRedis;
+};
+
+export const findRedisById = async (redisId: string) => {
+ const result = await db.query.redis.findFirst({
+ where: eq(redis.redisId, redisId),
+ with: {
+ project: true,
+ mounts: true,
+ server: true,
+ },
+ });
+ if (!result) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Redis not found",
+ });
+ }
+ return result;
+};
+
+export const updateRedisById = async (
+ redisId: string,
+ redisData: Partial,
+) => {
+ const result = await db
+ .update(redis)
+ .set({
+ ...redisData,
+ })
+ .where(eq(redis.redisId, redisId))
+ .returning();
+
+ return result[0];
+};
+
+export const removeRedisById = async (redisId: string) => {
+ const result = await db
+ .delete(redis)
+ .where(eq(redis.redisId, redisId))
+ .returning();
+
+ return result[0];
+};
+
+export const deployRedis = async (redisId: string) => {
+ const redis = await findRedisById(redisId);
+ try {
+ if (redis.serverId) {
+ await execAsyncRemote(redis.serverId, `docker pull ${redis.dockerImage}`);
+ } else {
+ await pullImage(redis.dockerImage);
+ }
+
+ await buildRedis(redis);
+ await updateRedisById(redisId, {
+ applicationStatus: "done",
+ });
+ } catch (error) {
+ await updateRedisById(redisId, {
+ applicationStatus: "error",
+ });
+
+ throw new TRPCError({
+ code: "INTERNAL_SERVER_ERROR",
+ message: `Error on deploy redis${error}`,
+ });
+ }
+ return redis;
+};
diff --git a/packages/builders/src/services/registry.ts b/packages/builders/src/services/registry.ts
new file mode 100644
index 000000000..83dcc2a21
--- /dev/null
+++ b/packages/builders/src/services/registry.ts
@@ -0,0 +1,124 @@
+import { db } from "@/server/db";
+import { type apiCreateRegistry, registry } from "@/server/db/schema";
+import { initializeRegistry } from "@/server/setup/registry-setup";
+import { removeService } from "@/server/utils/docker/utils";
+import { execAsync } from "@/server/utils/process/execAsync";
+import {
+ manageRegistry,
+ removeSelfHostedRegistry,
+} from "@/server/utils/traefik/registry";
+import { TRPCError } from "@trpc/server";
+import { eq } from "drizzle-orm";
+import { findAdmin } from "./admin";
+
+export type Registry = typeof registry.$inferSelect;
+
+export const createRegistry = async (input: typeof apiCreateRegistry._type) => {
+ const admin = await findAdmin();
+
+ return await db.transaction(async (tx) => {
+ const newRegistry = await tx
+ .insert(registry)
+ .values({
+ ...input,
+ adminId: admin.adminId,
+ })
+ .returning()
+ .then((value) => value[0]);
+
+ if (!newRegistry) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error input: Inserting registry",
+ });
+ }
+
+ if (newRegistry.registryType === "cloud") {
+ const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`;
+ await execAsync(loginCommand);
+ }
+
+ return newRegistry;
+ });
+};
+
+export const removeRegistry = async (registryId: string) => {
+ try {
+ const response = await db
+ .delete(registry)
+ .where(eq(registry.registryId, registryId))
+ .returning()
+ .then((res) => res[0]);
+
+ if (!response) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Registry not found",
+ });
+ }
+
+ if (response.registryType === "selfHosted") {
+ await removeSelfHostedRegistry();
+ await removeService("dokploy-registry");
+ }
+
+ await execAsync(`docker logout ${response.registryUrl}`);
+
+ return response;
+ } catch (error) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error to remove this registry",
+ cause: error,
+ });
+ }
+};
+
+export const updateRegistry = async (
+ registryId: string,
+ registryData: Partial,
+) => {
+ try {
+ const response = await db
+ .update(registry)
+ .set({
+ ...registryData,
+ })
+ .where(eq(registry.registryId, registryId))
+ .returning()
+ .then((res) => res[0]);
+
+ if (response?.registryType === "selfHosted") {
+ await manageRegistry(response);
+ await initializeRegistry(response.username, response.password);
+ }
+
+ return response;
+ } catch (error) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error to update this registry",
+ });
+ }
+};
+
+export const findRegistryById = async (registryId: string) => {
+ const registryResponse = await db.query.registry.findFirst({
+ where: eq(registry.registryId, registryId),
+ columns: {
+ password: false,
+ },
+ });
+ if (!registryResponse) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Registry not found",
+ });
+ }
+ return registryResponse;
+};
+
+export const findAllRegistry = async () => {
+ const registryResponse = await db.query.registry.findMany();
+ return registryResponse;
+};
diff --git a/packages/builders/src/services/security.ts b/packages/builders/src/services/security.ts
new file mode 100644
index 000000000..2da78e266
--- /dev/null
+++ b/packages/builders/src/services/security.ts
@@ -0,0 +1,107 @@
+import { db } from "@/server/db";
+import { type apiCreateSecurity, security } from "@/server/db/schema";
+import {
+ createSecurityMiddleware,
+ removeSecurityMiddleware,
+} from "@/server/utils/traefik/security";
+import { TRPCError } from "@trpc/server";
+import { eq } from "drizzle-orm";
+import type { z } from "zod";
+import { findApplicationById } from "./application";
+export type Security = typeof security.$inferSelect;
+
+export const findSecurityById = async (securityId: string) => {
+ const application = await db.query.security.findFirst({
+ where: eq(security.securityId, securityId),
+ });
+ if (!application) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Security not found",
+ });
+ }
+ return application;
+};
+
+export const createSecurity = async (
+ data: z.infer,
+) => {
+ try {
+ await db.transaction(async (tx) => {
+ const application = await findApplicationById(data.applicationId);
+
+ const securityResponse = await tx
+ .insert(security)
+ .values({
+ ...data,
+ })
+ .returning()
+ .then((res) => res[0]);
+
+ if (!securityResponse) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error to create the security",
+ });
+ }
+ await createSecurityMiddleware(application, securityResponse);
+ return true;
+ });
+ } catch (error) {
+ console.log(error);
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error to create this security",
+ cause: error,
+ });
+ }
+};
+
+export const deleteSecurityById = async (securityId: string) => {
+ try {
+ const result = await db
+ .delete(security)
+ .where(eq(security.securityId, securityId))
+ .returning()
+ .then((res) => res[0]);
+
+ if (!result) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Security not found",
+ });
+ }
+
+ const application = await findApplicationById(result.applicationId);
+
+ await removeSecurityMiddleware(application, result);
+ return result;
+ } catch (error) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error to remove this security",
+ });
+ }
+};
+
+export const updateSecurityById = async (
+ securityId: string,
+ data: Partial,
+) => {
+ try {
+ const response = await db
+ .update(security)
+ .set({
+ ...data,
+ })
+ .where(eq(security.securityId, securityId))
+ .returning();
+
+ return response[0];
+ } catch (error) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error to update this security",
+ });
+ }
+};
diff --git a/packages/builders/src/services/server.ts b/packages/builders/src/services/server.ts
new file mode 100644
index 000000000..4cd79cdcc
--- /dev/null
+++ b/packages/builders/src/services/server.ts
@@ -0,0 +1,120 @@
+import { db } from "@/server/db";
+import { type apiCreateServer, server } from "@/server/db/schema";
+import { TRPCError } from "@trpc/server";
+import { desc, eq } from "drizzle-orm";
+
+export type Server = typeof server.$inferSelect;
+
+export const createServer = async (
+ input: typeof apiCreateServer._type,
+ adminId: string,
+) => {
+ const newServer = await db
+ .insert(server)
+ .values({
+ ...input,
+ adminId: adminId,
+ })
+ .returning()
+ .then((value) => value[0]);
+
+ if (!newServer) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error to create the server",
+ });
+ }
+
+ return newServer;
+};
+
+export const findServerById = async (serverId: string) => {
+ const currentServer = await db.query.server.findFirst({
+ where: eq(server.serverId, serverId),
+ with: {
+ deployments: true,
+ sshKey: true,
+ },
+ });
+ if (!currentServer) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Server not found",
+ });
+ }
+ return currentServer;
+};
+
+export const findServersByAdminId = async (adminId: string) => {
+ const servers = await db.query.server.findMany({
+ where: eq(server.adminId, adminId),
+ orderBy: desc(server.createdAt),
+ });
+
+ return servers;
+};
+
+export const deleteServer = async (serverId: string) => {
+ const currentServer = await db
+ .delete(server)
+ .where(eq(server.serverId, serverId))
+ .returning()
+ .then((value) => value[0]);
+
+ return currentServer;
+};
+
+export const haveActiveServices = async (serverId: string) => {
+ const currentServer = await db.query.server.findFirst({
+ where: eq(server.serverId, serverId),
+ with: {
+ applications: true,
+ compose: true,
+ redis: true,
+ mariadb: true,
+ mongo: true,
+ mysql: true,
+ postgres: true,
+ },
+ });
+
+ if (!currentServer) {
+ return false;
+ }
+
+ const total =
+ currentServer?.applications?.length +
+ currentServer?.compose?.length +
+ currentServer?.redis?.length +
+ currentServer?.mariadb?.length +
+ currentServer?.mongo?.length +
+ currentServer?.mysql?.length +
+ currentServer?.postgres?.length;
+
+ if (total === 0) {
+ return false;
+ }
+
+ return true;
+};
+
+export const updateServerById = async (
+ serverId: string,
+ serverData: Partial,
+) => {
+ const result = await db
+ .update(server)
+ .set({
+ ...serverData,
+ })
+ .where(eq(server.serverId, serverId))
+ .returning()
+ .then((res) => res[0]);
+
+ return result;
+};
+
+export const getAllServers = async () => {
+ const servers = await db.query.server.findMany();
+ return servers;
+};
diff --git a/packages/builders/src/services/settings.ts b/packages/builders/src/services/settings.ts
new file mode 100644
index 000000000..eacfd8caf
--- /dev/null
+++ b/packages/builders/src/services/settings.ts
@@ -0,0 +1,148 @@
+import { readdirSync } from "node:fs";
+import { join } from "node:path";
+import { docker } from "@/server/constants";
+import { getServiceContainer } from "@/server/utils/docker/utils";
+import { execAsyncRemote } from "@/server/utils/process/execAsync";
+// import packageInfo from "../../../package.json";
+
+const updateIsAvailable = async () => {
+ try {
+ const service = await getServiceContainer("dokploy");
+
+ const localImage = await docker.getImage(getDokployImage()).inspect();
+ return localImage.Id !== service?.ImageID;
+ } catch (error) {
+ return false;
+ }
+};
+
+export const getDokployImage = () => {
+ return `dokploy/dokploy:${process.env.RELEASE_TAG || "latest"}`;
+};
+
+export const pullLatestRelease = async () => {
+ try {
+ const stream = await docker.pull(getDokployImage(), {});
+ await new Promise((resolve, reject) => {
+ docker.modem.followProgress(stream, (err, res) =>
+ err ? reject(err) : resolve(res),
+ );
+ });
+ const newUpdateIsAvailable = await updateIsAvailable();
+ return newUpdateIsAvailable;
+ } catch (error) {}
+
+ return false;
+};
+export const getDokployVersion = () => {
+ // return packageInfo.version;
+};
+
+interface TreeDataItem {
+ id: string;
+ name: string;
+ type: "file" | "directory";
+ children?: TreeDataItem[];
+}
+
+export const readDirectory = async (
+ dirPath: string,
+ serverId?: string,
+): Promise => {
+ if (serverId) {
+ const { stdout } = await execAsyncRemote(
+ serverId,
+ `
+process_items() {
+ local parent_dir="$1"
+ local __resultvar=$2
+
+ local items_json=""
+ local first=true
+ for item in "$parent_dir"/*; do
+ [ -e "$item" ] || continue
+ process_item "$item" item_json
+ if [ "$first" = true ]; then
+ first=false
+ items_json="$item_json"
+ else
+ items_json="$items_json,$item_json"
+ fi
+ done
+
+ eval $__resultvar="'[$items_json]'"
+}
+
+process_item() {
+ local item_path="$1"
+ local __resultvar=$2
+
+ local item_name=$(basename "$item_path")
+ local escaped_name=$(echo "$item_name" | sed 's/"/\\"/g')
+ local escaped_path=$(echo "$item_path" | sed 's/"/\\"/g')
+
+ if [ -d "$item_path" ]; then
+ # Is directory
+ process_items "$item_path" children_json
+ local json='{"id":"'"$escaped_path"'","name":"'"$escaped_name"'","type":"directory","children":'"$children_json"'}'
+ else
+ # Is file
+ local json='{"id":"'"$escaped_path"'","name":"'"$escaped_name"'","type":"file"}'
+ fi
+
+ eval $__resultvar="'$json'"
+}
+
+root_dir=${dirPath}
+
+process_items "$root_dir" json_output
+
+echo "$json_output"
+ `,
+ );
+ const result = JSON.parse(stdout);
+ return result;
+ }
+ const items = readdirSync(dirPath, { withFileTypes: true });
+
+ const stack = [dirPath];
+ const result: TreeDataItem[] = [];
+ const parentMap: Record = {};
+
+ while (stack.length > 0) {
+ const currentPath = stack.pop();
+ if (!currentPath) continue;
+
+ const items = readdirSync(currentPath, { withFileTypes: true });
+ const currentDirectoryResult: TreeDataItem[] = [];
+
+ for (const item of items) {
+ const fullPath = join(currentPath, item.name);
+ if (item.isDirectory()) {
+ stack.push(fullPath);
+ const directoryItem: TreeDataItem = {
+ id: fullPath,
+ name: item.name,
+ type: "directory",
+ children: [],
+ };
+ currentDirectoryResult.push(directoryItem);
+ parentMap[fullPath] = directoryItem.children as TreeDataItem[];
+ } else {
+ const fileItem: TreeDataItem = {
+ id: fullPath,
+ name: item.name,
+ type: "file",
+ };
+ currentDirectoryResult.push(fileItem);
+ }
+ }
+
+ if (parentMap[currentPath]) {
+ parentMap[currentPath].push(...currentDirectoryResult);
+ } else {
+ result.push(...currentDirectoryResult);
+ }
+ }
+ return result;
+};
diff --git a/packages/builders/src/services/ssh-key.ts b/packages/builders/src/services/ssh-key.ts
new file mode 100644
index 000000000..9d6f0070e
--- /dev/null
+++ b/packages/builders/src/services/ssh-key.ts
@@ -0,0 +1,78 @@
+import { db } from "@/server/db";
+import {
+ type apiCreateSshKey,
+ type apiFindOneSshKey,
+ type apiRemoveSshKey,
+ type apiUpdateSshKey,
+ sshKeys,
+} from "@/server/db/schema";
+import { removeSSHKey, saveSSHKey } from "@/server/utils/filesystem/ssh";
+import { TRPCError } from "@trpc/server";
+import { eq } from "drizzle-orm";
+
+export const createSshKey = async ({
+ privateKey,
+ ...input
+}: typeof apiCreateSshKey._type) => {
+ await db.transaction(async (tx) => {
+ const sshKey = await tx
+ .insert(sshKeys)
+ .values(input)
+ .returning()
+ .then((response) => response[0])
+ .catch((e) => console.error(e));
+
+ if (sshKey) {
+ saveSSHKey(sshKey.sshKeyId, sshKey.publicKey, privateKey);
+ }
+
+ if (!sshKey) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Error to create the ssh key",
+ });
+ }
+ return sshKey;
+ });
+};
+
+export const removeSSHKeyById = async (
+ sshKeyId: (typeof apiRemoveSshKey._type)["sshKeyId"],
+) => {
+ const result = await db
+ .delete(sshKeys)
+ .where(eq(sshKeys.sshKeyId, sshKeyId))
+ .returning();
+
+ removeSSHKey(sshKeyId);
+
+ return result[0];
+};
+
+export const updateSSHKeyById = async ({
+ sshKeyId,
+ ...input
+}: typeof apiUpdateSshKey._type) => {
+ const result = await db
+ .update(sshKeys)
+ .set(input)
+ .where(eq(sshKeys.sshKeyId, sshKeyId))
+ .returning();
+
+ return result[0];
+};
+
+export const findSSHKeyById = async (
+ sshKeyId: (typeof apiFindOneSshKey._type)["sshKeyId"],
+) => {
+ const sshKey = await db.query.sshKeys.findFirst({
+ where: eq(sshKeys.sshKeyId, sshKeyId),
+ });
+ if (!sshKey) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "SSH Key not found",
+ });
+ }
+ return sshKey;
+};
diff --git a/packages/builders/src/services/user.ts b/packages/builders/src/services/user.ts
new file mode 100644
index 000000000..044bc3cb4
--- /dev/null
+++ b/packages/builders/src/services/user.ts
@@ -0,0 +1,207 @@
+import { db } from "@/server/db";
+import { users } from "@/server/db/schema";
+import { TRPCError } from "@trpc/server";
+import { eq } from "drizzle-orm";
+
+export type User = typeof users.$inferSelect;
+
+export const findUserById = async (userId: string) => {
+ const user = await db.query.users.findFirst({
+ where: eq(users.userId, userId),
+ });
+ if (!user) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "User not found",
+ });
+ }
+ return user;
+};
+
+export const findUserByAuthId = async (authId: string) => {
+ const user = await db.query.users.findFirst({
+ where: eq(users.authId, authId),
+ with: {
+ auth: true,
+ },
+ });
+ if (!user) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "User not found",
+ });
+ }
+ return user;
+};
+
+export const findUsers = async () => {
+ const users = await db.query.users.findMany({
+ with: {
+ auth: {
+ columns: {
+ secret: false,
+ },
+ },
+ },
+ });
+ return users;
+};
+
+export const addNewProject = async (authId: string, projectId: string) => {
+ const user = await findUserByAuthId(authId);
+
+ await db
+ .update(users)
+ .set({
+ accesedProjects: [...user.accesedProjects, projectId],
+ })
+ .where(eq(users.authId, authId));
+};
+
+export const addNewService = async (authId: string, serviceId: string) => {
+ const user = await findUserByAuthId(authId);
+ await db
+ .update(users)
+ .set({
+ accesedServices: [...user.accesedServices, serviceId],
+ })
+ .where(eq(users.authId, authId));
+};
+
+export const canPerformCreationService = async (
+ userId: string,
+ projectId: string,
+) => {
+ const { accesedProjects, canCreateServices } = await findUserByAuthId(userId);
+ const haveAccessToProject = accesedProjects.includes(projectId);
+
+ if (canCreateServices && haveAccessToProject) {
+ return true;
+ }
+
+ return false;
+};
+
+export const canPerformAccessService = async (
+ userId: string,
+ serviceId: string,
+) => {
+ const { accesedServices } = await findUserByAuthId(userId);
+ const haveAccessToService = accesedServices.includes(serviceId);
+
+ if (haveAccessToService) {
+ return true;
+ }
+
+ return false;
+};
+
+export const canPeformDeleteService = async (
+ authId: string,
+ serviceId: string,
+) => {
+ const { accesedServices, canDeleteServices } = await findUserByAuthId(authId);
+ const haveAccessToService = accesedServices.includes(serviceId);
+
+ if (canDeleteServices && haveAccessToService) {
+ return true;
+ }
+
+ return false;
+};
+
+export const canPerformCreationProject = async (authId: string) => {
+ const { canCreateProjects } = await findUserByAuthId(authId);
+
+ if (canCreateProjects) {
+ return true;
+ }
+
+ return false;
+};
+
+export const canPerformDeleteProject = async (authId: string) => {
+ const { canDeleteProjects } = await findUserByAuthId(authId);
+
+ if (canDeleteProjects) {
+ return true;
+ }
+
+ return false;
+};
+
+export const canPerformAccessProject = async (
+ authId: string,
+ projectId: string,
+) => {
+ const { accesedProjects } = await findUserByAuthId(authId);
+
+ const haveAccessToProject = accesedProjects.includes(projectId);
+
+ if (haveAccessToProject) {
+ return true;
+ }
+ return false;
+};
+
+export const canAccessToTraefikFiles = async (authId: string) => {
+ const { canAccessToTraefikFiles } = await findUserByAuthId(authId);
+ return canAccessToTraefikFiles;
+};
+
+export const checkServiceAccess = async (
+ authId: string,
+ serviceId: string,
+ action = "access" as "access" | "create" | "delete",
+) => {
+ let hasPermission = false;
+ switch (action) {
+ case "create":
+ hasPermission = await canPerformCreationService(authId, serviceId);
+ break;
+ case "access":
+ hasPermission = await canPerformAccessService(authId, serviceId);
+ break;
+ case "delete":
+ hasPermission = await canPeformDeleteService(authId, serviceId);
+ break;
+ default:
+ hasPermission = false;
+ }
+ if (!hasPermission) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "Permission denied",
+ });
+ }
+};
+
+export const checkProjectAccess = async (
+ authId: string,
+ action: "create" | "delete" | "access",
+ projectId?: string,
+) => {
+ let hasPermission = false;
+ switch (action) {
+ case "access":
+ hasPermission = await canPerformAccessProject(
+ authId,
+ projectId as string,
+ );
+ break;
+ case "create":
+ hasPermission = await canPerformCreationProject(authId);
+ break;
+ case "delete":
+ hasPermission = await canPerformDeleteProject(authId);
+ break;
+ default:
+ hasPermission = false;
+ }
+ if (!hasPermission) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "Permission denied",
+ });
+ }
+};
diff --git a/packages/builders/src/setup/config-paths.ts b/packages/builders/src/setup/config-paths.ts
new file mode 100644
index 000000000..190e438b0
--- /dev/null
+++ b/packages/builders/src/setup/config-paths.ts
@@ -0,0 +1,43 @@
+import { chmodSync, existsSync, mkdirSync } from "node:fs";
+import { paths } from "../constants";
+
+const createDirectoryIfNotExist = (dirPath: string) => {
+ if (!existsSync(dirPath)) {
+ mkdirSync(dirPath, { recursive: true });
+ console.log(`Directory created: ${dirPath}`);
+ }
+};
+
+export const setupDirectories = () => {
+ const {
+ APPLICATIONS_PATH,
+ BASE_PATH,
+ CERTIFICATES_PATH,
+ DYNAMIC_TRAEFIK_PATH,
+ LOGS_PATH,
+ MAIN_TRAEFIK_PATH,
+ MONITORING_PATH,
+ SSH_PATH,
+ } = paths();
+ const directories = [
+ BASE_PATH,
+ MAIN_TRAEFIK_PATH,
+ DYNAMIC_TRAEFIK_PATH,
+ LOGS_PATH,
+ APPLICATIONS_PATH,
+ SSH_PATH,
+ CERTIFICATES_PATH,
+ MONITORING_PATH,
+ ];
+
+ for (const dir of directories) {
+ try {
+ createDirectoryIfNotExist(dir);
+ if (dir === SSH_PATH) {
+ chmodSync(SSH_PATH, "700");
+ }
+ } catch (error) {
+ console.log(error, " On path: ", dir);
+ }
+ }
+};
diff --git a/packages/builders/src/setup/postgres-setup.ts b/packages/builders/src/setup/postgres-setup.ts
new file mode 100644
index 000000000..ca606c2d1
--- /dev/null
+++ b/packages/builders/src/setup/postgres-setup.ts
@@ -0,0 +1,61 @@
+import type { CreateServiceOptions } from "dockerode";
+import { docker } from "../constants";
+import { pullImage } from "../utils/docker/utils";
+export const initializePostgres = async () => {
+ const imageName = "postgres:16";
+ const containerName = "dokploy-postgres";
+ const settings: CreateServiceOptions = {
+ Name: containerName,
+ TaskTemplate: {
+ ContainerSpec: {
+ Image: imageName,
+ Env: [
+ "POSTGRES_USER=dokploy",
+ "POSTGRES_DB=dokploy",
+ "POSTGRES_PASSWORD=amukds4wi9001583845717ad2",
+ ],
+ Mounts: [
+ {
+ Type: "volume",
+ Source: "dokploy-postgres-database",
+ Target: "/var/lib/postgresql/data",
+ },
+ ],
+ },
+ Networks: [{ Target: "dokploy-network" }],
+ Placement: {
+ Constraints: ["node.role==manager"],
+ },
+ },
+ Mode: {
+ Replicated: {
+ Replicas: 1,
+ },
+ },
+ EndpointSpec: {
+ Ports: [
+ {
+ TargetPort: 5432,
+ PublishedPort: process.env.NODE_ENV === "development" ? 5432 : 0,
+ Protocol: "tcp",
+ PublishMode: "host",
+ },
+ ],
+ },
+ };
+ try {
+ await pullImage(imageName);
+
+ const service = docker.getService(containerName);
+ const inspect = await service.inspect();
+ await service.update({
+ version: Number.parseInt(inspect.Version.Index),
+ ...settings,
+ });
+
+ console.log("Postgres Started ✅");
+ } catch (error) {
+ await docker.createService(settings);
+ console.log("Postgres Not Found: Starting ✅");
+ }
+};
diff --git a/packages/builders/src/setup/redis-setup.ts b/packages/builders/src/setup/redis-setup.ts
new file mode 100644
index 000000000..abbacdd83
--- /dev/null
+++ b/packages/builders/src/setup/redis-setup.ts
@@ -0,0 +1,57 @@
+import type { CreateServiceOptions } from "dockerode";
+import { docker } from "../constants";
+import { pullImage } from "../utils/docker/utils";
+
+export const initializeRedis = async () => {
+ const imageName = "redis:7";
+ const containerName = "dokploy-redis";
+
+ const settings: CreateServiceOptions = {
+ Name: containerName,
+ TaskTemplate: {
+ ContainerSpec: {
+ Image: imageName,
+ Mounts: [
+ {
+ Type: "volume",
+ Source: "redis-data-volume",
+ Target: "/data",
+ },
+ ],
+ },
+ Networks: [{ Target: "dokploy-network" }],
+ Placement: {
+ Constraints: ["node.role==manager"],
+ },
+ },
+ Mode: {
+ Replicated: {
+ Replicas: 1,
+ },
+ },
+ EndpointSpec: {
+ Ports: [
+ {
+ TargetPort: 6379,
+ PublishedPort: process.env.NODE_ENV === "development" ? 6379 : 0,
+ Protocol: "tcp",
+ PublishMode: "host",
+ },
+ ],
+ },
+ };
+ try {
+ await pullImage(imageName);
+
+ const service = docker.getService(containerName);
+ const inspect = await service.inspect();
+ await service.update({
+ version: Number.parseInt(inspect.Version.Index),
+ ...settings,
+ });
+ console.log("Redis Started ✅");
+ } catch (error) {
+ await docker.createService(settings);
+ console.log("Redis Not Found: Starting ✅");
+ }
+};
diff --git a/packages/builders/src/setup/registry-setup.ts b/packages/builders/src/setup/registry-setup.ts
new file mode 100644
index 000000000..02c1d3d01
--- /dev/null
+++ b/packages/builders/src/setup/registry-setup.ts
@@ -0,0 +1,91 @@
+import type { CreateServiceOptions } from "dockerode";
+import { docker, paths } from "../constants";
+import { pullImage } from "../utils/docker/utils";
+import { execAsync } from "../utils/process/execAsync";
+import { generatePassword } from "../templates/utils";
+
+export const initializeRegistry = async (
+ username: string,
+ password: string,
+) => {
+ const { REGISTRY_PATH } = paths();
+ const imageName = "registry:2.8.3";
+ const containerName = "dokploy-registry";
+ await generateRegistryPassword(username, password);
+ const randomPass = generatePassword();
+ const settings: CreateServiceOptions = {
+ Name: containerName,
+ TaskTemplate: {
+ ContainerSpec: {
+ Image: imageName,
+ Env: [
+ "REGISTRY_STORAGE_DELETE_ENABLED=true",
+ "REGISTRY_AUTH=htpasswd",
+ "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm",
+ "REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd",
+ `REGISTRY_HTTP_SECRET=${randomPass}`,
+ ],
+ Mounts: [
+ {
+ Type: "bind",
+ Source: `${REGISTRY_PATH}/htpasswd`,
+ Target: "/auth/htpasswd",
+ ReadOnly: true,
+ },
+ {
+ Type: "volume",
+ Source: "registry-data",
+ Target: "/var/lib/registry",
+ ReadOnly: false,
+ },
+ ],
+ },
+ Networks: [{ Target: "dokploy-network" }],
+ Placement: {
+ Constraints: ["node.role==manager"],
+ },
+ },
+ Mode: {
+ Replicated: {
+ Replicas: 1,
+ },
+ },
+ EndpointSpec: {
+ Ports: [
+ {
+ TargetPort: 5000,
+ PublishedPort: 5000,
+ Protocol: "tcp",
+ PublishMode: "host",
+ },
+ ],
+ },
+ };
+ try {
+ await pullImage(imageName);
+
+ const service = docker.getService(containerName);
+ const inspect = await service.inspect();
+ await service.update({
+ version: Number.parseInt(inspect.Version.Index),
+ ...settings,
+ });
+ console.log("Registry Started ✅");
+ } catch (error) {
+ await docker.createService(settings);
+ console.log("Registry Not Found: Starting ✅");
+ }
+};
+
+const generateRegistryPassword = async (username: string, password: string) => {
+ try {
+ const { REGISTRY_PATH } = paths();
+ const command = `htpasswd -nbB ${username} "${password}" > ${REGISTRY_PATH}/htpasswd`;
+ const result = await execAsync(command);
+ console.log("Password generated ✅");
+ return result.stdout.trim();
+ } catch (error) {
+ console.error("Error generating password:", error);
+ return null;
+ }
+};
diff --git a/packages/builders/src/setup/server-setup.ts b/packages/builders/src/setup/server-setup.ts
new file mode 100644
index 000000000..ffc29db1f
--- /dev/null
+++ b/packages/builders/src/setup/server-setup.ts
@@ -0,0 +1,315 @@
+import { createWriteStream } from "node:fs";
+import path from "node:path";
+import {
+ createServerDeployment,
+ updateDeploymentStatus,
+} from "@/server/services/deployment";
+import { findServerById } from "@/server/services/server";
+import { paths } from "@/server/constants";
+import {
+ getDefaultMiddlewares,
+ getDefaultServerTraefikConfig,
+} from "@/server/setup/traefik-setup";
+import { Client } from "ssh2";
+import { recreateDirectory } from "../utils/filesystem/directory";
+import { readSSHKey } from "../utils/filesystem/ssh";
+
+import slug from "slugify";
+
+export const slugify = (text: string | undefined) => {
+ if (!text) {
+ return "";
+ }
+
+ const cleanedText = text.trim().replace(/[^a-zA-Z0-9\s]/g, "");
+
+ return slug(cleanedText, {
+ lower: true,
+ trim: true,
+ strict: true,
+ });
+};
+
+export const serverSetup = async (serverId: string) => {
+ const server = await findServerById(serverId);
+ const { LOGS_PATH } = paths();
+
+ const slugifyName = slugify(`server ${server.name}`);
+
+ const fullPath = path.join(LOGS_PATH, slugifyName);
+
+ await recreateDirectory(fullPath);
+
+ const deployment = await createServerDeployment({
+ serverId: server.serverId,
+ title: "Setup Server",
+ description: "Setup Server",
+ });
+
+ const writeStream = createWriteStream(deployment.logPath, { flags: "a" });
+ try {
+ writeStream.write("\nInstalling Server Dependencies: ✅\n");
+ await installRequirements(serverId, deployment.logPath);
+ writeStream.close();
+
+ await updateDeploymentStatus(deployment.deploymentId, "done");
+ } catch (err) {
+ console.log(err);
+ await updateDeploymentStatus(deployment.deploymentId, "error");
+ writeStream.write(err);
+ writeStream.close();
+ }
+};
+
+const installRequirements = async (serverId: string, logPath: string) => {
+ const writeStream = createWriteStream(logPath, { flags: "a" });
+ const client = new Client();
+ const server = await findServerById(serverId);
+ if (!server.sshKeyId) {
+ writeStream.write("❌ No SSH Key found");
+ writeStream.close();
+ throw new Error("No SSH Key found");
+ }
+ const keys = await readSSHKey(server.sshKeyId);
+
+ if (!keys.privateKey) {
+ writeStream.write("❌ No SSH Key found");
+ writeStream.close();
+ throw new Error("No SSH Key found");
+ }
+ return new Promise((resolve, reject) => {
+ client
+ .once("ready", () => {
+ const bashCommand = `
+
+ ${validatePorts()}
+
+ command_exists() {
+ command -v "$@" > /dev/null 2>&1
+ }
+ ${installRClone()}
+ ${installDocker()}
+ ${setupSwarm()}
+ ${setupNetwork()}
+ ${setupMainDirectory()}
+ ${setupDirectories()}
+ ${createTraefikConfig()}
+ ${createDefaultMiddlewares()}
+ ${createTraefikInstance()}
+ ${installNixpacks()}
+ ${installBuildpacks()}
+ `;
+
+ client.exec(bashCommand, (err, stream) => {
+ if (err) {
+ writeStream.write(err);
+ reject(err);
+ return;
+ }
+ stream
+ .on("close", () => {
+ writeStream.write("Connection closed ✅");
+ client.end();
+ resolve();
+ })
+ .on("data", (data: string) => {
+ writeStream.write(data.toString());
+ })
+ .stderr.on("data", (data) => {
+ writeStream.write(data.toString());
+ });
+ });
+ })
+ .on("error", (err) => {
+ client.end();
+ if (err.level === "client-authentication") {
+ writeStream.write(
+ `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`,
+ );
+ reject(
+ new Error(
+ `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`,
+ ),
+ );
+ } else {
+ writeStream.write(
+ `SSH connection error: ${err.message} ${err.level}`,
+ );
+ reject(new Error(`SSH connection error: ${err.message}`));
+ }
+ })
+ .connect({
+ host: server.ipAddress,
+ port: server.port,
+ username: server.username,
+ privateKey: keys.privateKey,
+ timeout: 99999,
+ });
+ });
+};
+
+const setupDirectories = () => {
+ const { SSH_PATH } = paths(true);
+ const directories = Object.values(paths(true));
+
+ const createDirsCommand = directories
+ .map((dir) => `mkdir -p "${dir}"`)
+ .join(" && ");
+ const chmodCommand = `chmod 700 "${SSH_PATH}"`;
+
+ const command = `
+ ${createDirsCommand}
+ ${chmodCommand}
+ `;
+
+ return command;
+};
+
+const setupMainDirectory = () => `
+ # Check if the /etc/dokploy directory exists
+ if [ -d /etc/dokploy ]; then
+ echo "/etc/dokploy already exists ✅"
+ else
+ # Create the /etc/dokploy directory
+ mkdir -p /etc/dokploy
+ chmod 777 /etc/dokploy
+
+ echo "Directory /etc/dokploy created ✅"
+ fi
+`;
+
+export const setupSwarm = () => `
+ # Check if the node is already part of a Docker Swarm
+ if docker info | grep -q 'Swarm: active'; then
+ echo "Already part of a Docker Swarm ✅"
+ else
+ # Get IP address
+ get_ip() {
+ # Try to get IPv4
+ local ipv4=\$(curl -4s https://ifconfig.io 2>/dev/null)
+
+ if [ -n "\$ipv4" ]; then
+ echo "\$ipv4"
+ else
+ # Try to get IPv6
+ local ipv6=\$(curl -6s https://ifconfig.io 2>/dev/null)
+ if [ -n "\$ipv6" ]; then
+ echo "\$ipv6"
+ fi
+ fi
+ }
+ advertise_addr=\$(get_ip)
+
+ # Initialize Docker Swarm
+ docker swarm init --advertise-addr \$advertise_addr
+ echo "Swarm initialized ✅"
+ fi
+ `;
+
+const setupNetwork = () => `
+ # Check if the dokploy-network already exists
+ if docker network ls | grep -q 'dokploy-network'; then
+ echo "Network dokploy-network already exists ✅"
+ else
+ # Create the dokploy-network if it doesn't exist
+ docker network create --driver overlay --attachable dokploy-network
+ echo "Network created ✅"
+ fi
+`;
+
+const installDocker = () => `
+ if command_exists docker; then
+ echo "Docker already installed ✅"
+ else
+ echo "Installing Docker ✅"
+ curl -sSL https://get.docker.com | sh -s -- --version 27.2.0
+ fi
+`;
+
+const validatePorts = () => `
+ # check if something is running on port 80
+ if ss -tulnp | grep ':80 ' >/dev/null; then
+ echo "Something is already running on port 80" >&2
+ fi
+
+ # check if something is running on port 443
+ if ss -tulnp | grep ':443 ' >/dev/null; then
+ echo "Something is already running on port 443" >&2
+ fi
+`;
+
+const createTraefikConfig = () => {
+ const config = getDefaultServerTraefikConfig();
+
+ const command = `
+ if [ -f "/etc/dokploy/traefik/dynamic/acme.json" ]; then
+ chmod 600 "/etc/dokploy/traefik/dynamic/acme.json"
+ fi
+ if [ -f "/etc/dokploy/traefik/traefik.yml" ]; then
+ echo "Traefik config already exists ✅"
+ else
+ echo "${config}" > /etc/dokploy/traefik/traefik.yml
+ fi
+ `;
+
+ return command;
+};
+
+const createDefaultMiddlewares = () => {
+ const config = getDefaultMiddlewares();
+ const command = `
+ if [ -f "/etc/dokploy/traefik/dynamic/middlewares.yml" ]; then
+ echo "Middlewares config already exists ✅"
+ else
+ echo "${config}" > /etc/dokploy/traefik/dynamic/middlewares.yml
+ fi
+ `;
+ return command;
+};
+
+export const installRClone = () => `
+curl https://rclone.org/install.sh | sudo bash
+`;
+
+export const createTraefikInstance = () => {
+ const command = `
+ # Check if dokpyloy-traefik exists
+ if docker service ls | grep -q 'dokploy-traefik'; then
+ echo "Traefik already exists ✅"
+ else
+ # Create the dokploy-traefik service
+ docker service create \
+ --name dokploy-traefik \
+ --replicas 1 \
+ --constraint 'node.role==manager' \
+ --network dokploy-network \
+ --mount type=bind,src=/etc/dokploy/traefik/traefik.yml,dst=/etc/traefik/traefik.yml \
+ --mount type=bind,src=/etc/dokploy/traefik/dynamic,dst=/etc/dokploy/traefik/dynamic \
+ --mount type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \
+ --label traefik.enable=true \
+ --publish mode=host,target=443,published=443 \
+ --publish mode=host,target=80,published=80 \
+ traefik:v3.1.2
+ fi
+ `;
+
+ return command;
+};
+
+const installNixpacks = () => `
+ if command_exists nixpacks; then
+ echo "Nixpacks already installed ✅"
+ else
+ VERSION=1.28.1 bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
+ echo "Nixpacks version 1.28.1 installed ✅"
+ fi
+`;
+
+const installBuildpacks = () => `
+ if command_exists pack; then
+ echo "Buildpacks already installed ✅"
+ else
+ curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v0.35.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
+ echo "Buildpacks version 0.35.0 installed ✅"
+ fi
+`;
diff --git a/packages/builders/src/setup/setup.ts b/packages/builders/src/setup/setup.ts
new file mode 100644
index 000000000..c59877022
--- /dev/null
+++ b/packages/builders/src/setup/setup.ts
@@ -0,0 +1,47 @@
+import { docker } from "../constants";
+
+export const initializeSwarm = async () => {
+ const swarmInitialized = await dockerSwarmInitialized();
+ if (swarmInitialized) {
+ console.log("Swarm is already initilized");
+ } else {
+ await docker.swarmInit({
+ AdvertiseAddr: "127.0.0.1",
+ ListenAddr: "0.0.0.0",
+ });
+ console.log("Swarm was initilized");
+ }
+};
+
+export const dockerSwarmInitialized = async () => {
+ try {
+ await docker.swarmInspect();
+
+ return true;
+ } catch (e) {
+ return false;
+ }
+};
+
+export const initializeNetwork = async () => {
+ const networkInitialized = await dockerNetworkInitialized();
+ if (networkInitialized) {
+ console.log("Network is already initilized");
+ } else {
+ docker.createNetwork({
+ Attachable: true,
+ Name: "dokploy-network",
+ Driver: "overlay",
+ });
+ console.log("Network was initilized");
+ }
+};
+
+export const dockerNetworkInitialized = async () => {
+ try {
+ await docker.getNetwork("dokploy-network").inspect();
+ return true;
+ } catch (e) {
+ return false;
+ }
+};
diff --git a/packages/builders/src/setup/traefik-setup.ts b/packages/builders/src/setup/traefik-setup.ts
new file mode 100644
index 000000000..27bc99a4f
--- /dev/null
+++ b/packages/builders/src/setup/traefik-setup.ts
@@ -0,0 +1,320 @@
+import { chmodSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
+import path from "node:path";
+import type { ContainerTaskSpec, CreateServiceOptions } from "dockerode";
+import { dump } from "js-yaml";
+import { paths } from "../constants";
+import { pullImage, pullRemoteImage } from "../utils/docker/utils";
+import { getRemoteDocker } from "../utils/servers/remote-docker";
+import type { FileConfig } from "../utils/traefik/file-types";
+import type { MainTraefikConfig } from "../utils/traefik/types";
+
+const TRAEFIK_SSL_PORT =
+ Number.parseInt(process.env.TRAEFIK_SSL_PORT ?? "", 10) || 443;
+const TRAEFIK_PORT = Number.parseInt(process.env.TRAEFIK_PORT ?? "", 10) || 80;
+
+interface TraefikOptions {
+ enableDashboard?: boolean;
+ env?: string[];
+ serverId?: string;
+}
+
+export const initializeTraefik = async ({
+ enableDashboard = false,
+ env,
+ serverId,
+}: TraefikOptions = {}) => {
+ const { MAIN_TRAEFIK_PATH, DYNAMIC_TRAEFIK_PATH } = paths(!!serverId);
+ const imageName = "traefik:v3.1.2";
+ const containerName = "dokploy-traefik";
+ const settings: CreateServiceOptions = {
+ Name: containerName,
+ TaskTemplate: {
+ ContainerSpec: {
+ Image: imageName,
+ Env: env,
+ Mounts: [
+ {
+ Type: "bind",
+ Source: `${MAIN_TRAEFIK_PATH}/traefik.yml`,
+ Target: "/etc/traefik/traefik.yml",
+ },
+ {
+ Type: "bind",
+ Source: DYNAMIC_TRAEFIK_PATH,
+ Target: "/etc/dokploy/traefik/dynamic",
+ },
+ {
+ Type: "bind",
+ Source: "/var/run/docker.sock",
+ Target: "/var/run/docker.sock",
+ },
+ ],
+ },
+ Networks: [{ Target: "dokploy-network" }],
+ Placement: {
+ Constraints: ["node.role==manager"],
+ },
+ },
+ Mode: {
+ Replicated: {
+ Replicas: 1,
+ },
+ },
+ Labels: {
+ "traefik.enable": "true",
+ },
+ EndpointSpec: {
+ Ports: [
+ {
+ TargetPort: 443,
+ PublishedPort: TRAEFIK_SSL_PORT,
+ PublishMode: "host",
+ },
+ {
+ TargetPort: 80,
+ PublishedPort: TRAEFIK_PORT,
+ PublishMode: "host",
+ },
+ ...(enableDashboard
+ ? [
+ {
+ TargetPort: 8080,
+ PublishedPort: 8080,
+ PublishMode: "host" as const,
+ },
+ ]
+ : []),
+ ],
+ },
+ };
+ const docker = await getRemoteDocker(serverId);
+ try {
+ if (serverId) {
+ await pullRemoteImage(imageName, serverId);
+ } else {
+ await pullImage(imageName);
+ }
+
+ const service = docker.getService(containerName);
+ const inspect = await service.inspect();
+
+ const existingEnv = inspect.Spec.TaskTemplate.ContainerSpec.Env || [];
+ const updatedEnv = !env ? existingEnv : env;
+
+ const updatedSettings = {
+ ...settings,
+ TaskTemplate: {
+ ...settings.TaskTemplate,
+ ContainerSpec: {
+ ...(settings?.TaskTemplate as ContainerTaskSpec).ContainerSpec,
+ Env: updatedEnv,
+ },
+ },
+ };
+ await service.update({
+ version: Number.parseInt(inspect.Version.Index),
+ ...updatedSettings,
+ });
+
+ console.log("Traefik Started ✅");
+ } catch (error) {
+ await docker.createService(settings);
+ console.log("Traefik Not Found: Starting ✅");
+ }
+};
+
+export const createDefaultServerTraefikConfig = () => {
+ const { DYNAMIC_TRAEFIK_PATH } = paths();
+ const configFilePath = path.join(DYNAMIC_TRAEFIK_PATH, "dokploy.yml");
+
+ if (existsSync(configFilePath)) {
+ console.log("Default traefik config already exists");
+ return;
+ }
+
+ const appName = "dokploy";
+ const serviceURLDefault = `http://${appName}:${process.env.PORT || 3000}`;
+ const config: FileConfig = {
+ http: {
+ routers: {
+ [`${appName}-router-app`]: {
+ rule: `Host(\`${appName}.docker.localhost\`) && PathPrefix(\`/\`)`,
+ service: `${appName}-service-app`,
+ entryPoints: ["web"],
+ },
+ },
+ services: {
+ [`${appName}-service-app`]: {
+ loadBalancer: {
+ servers: [{ url: serviceURLDefault }],
+ passHostHeader: true,
+ },
+ },
+ },
+ },
+ };
+
+ const yamlStr = dump(config);
+ mkdirSync(DYNAMIC_TRAEFIK_PATH, { recursive: true });
+ writeFileSync(
+ path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`),
+ yamlStr,
+ "utf8",
+ );
+};
+
+export const getDefaultTraefikConfig = () => {
+ const configObject: MainTraefikConfig = {
+ providers: {
+ ...(process.env.NODE_ENV === "development"
+ ? {
+ docker: {
+ defaultRule:
+ "Host(`{{ trimPrefix `/` .Name }}.docker.localhost`)",
+ },
+ }
+ : {
+ swarm: {
+ exposedByDefault: false,
+ watch: false,
+ },
+ docker: {
+ exposedByDefault: false,
+ },
+ }),
+ file: {
+ directory: "/etc/dokploy/traefik/dynamic",
+ watch: true,
+ },
+ },
+ entryPoints: {
+ web: {
+ address: `:${TRAEFIK_PORT}`,
+ },
+ websecure: {
+ address: `:${TRAEFIK_SSL_PORT}`,
+ ...(process.env.NODE_ENV === "production" && {
+ http: {
+ tls: {
+ certResolver: "letsencrypt",
+ },
+ },
+ }),
+ },
+ },
+ api: {
+ insecure: true,
+ },
+ ...(process.env.NODE_ENV === "production" && {
+ certificatesResolvers: {
+ letsencrypt: {
+ acme: {
+ email: "test@localhost.com",
+ storage: "/etc/dokploy/traefik/dynamic/acme.json",
+ httpChallenge: {
+ entryPoint: "web",
+ },
+ },
+ },
+ },
+ }),
+ };
+
+ const yamlStr = dump(configObject);
+
+ return yamlStr;
+};
+
+export const getDefaultServerTraefikConfig = () => {
+ const configObject: MainTraefikConfig = {
+ providers: {
+ swarm: {
+ exposedByDefault: false,
+ watch: false,
+ },
+ docker: {
+ exposedByDefault: false,
+ },
+ file: {
+ directory: "/etc/dokploy/traefik/dynamic",
+ watch: true,
+ },
+ },
+ entryPoints: {
+ web: {
+ address: `:${TRAEFIK_PORT}`,
+ },
+ websecure: {
+ address: `:${TRAEFIK_SSL_PORT}`,
+ http: {
+ tls: {
+ certResolver: "letsencrypt",
+ },
+ },
+ },
+ },
+ api: {
+ insecure: true,
+ },
+ certificatesResolvers: {
+ letsencrypt: {
+ acme: {
+ email: "test@localhost.com",
+ storage: "/etc/dokploy/traefik/dynamic/acme.json",
+ httpChallenge: {
+ entryPoint: "web",
+ },
+ },
+ },
+ },
+ };
+
+ const yamlStr = dump(configObject);
+
+ return yamlStr;
+};
+
+export const createDefaultTraefikConfig = () => {
+ const { MAIN_TRAEFIK_PATH, DYNAMIC_TRAEFIK_PATH } = paths();
+ const mainConfig = path.join(MAIN_TRAEFIK_PATH, "traefik.yml");
+ const acmeJsonPath = path.join(DYNAMIC_TRAEFIK_PATH, "acme.json");
+
+ if (existsSync(acmeJsonPath)) {
+ chmodSync(acmeJsonPath, "600");
+ }
+ if (existsSync(mainConfig)) {
+ console.log("Main config already exists");
+ return;
+ }
+ const yamlStr = getDefaultTraefikConfig();
+ mkdirSync(MAIN_TRAEFIK_PATH, { recursive: true });
+ writeFileSync(mainConfig, yamlStr, "utf8");
+};
+
+export const getDefaultMiddlewares = () => {
+ const defaultMiddlewares = {
+ http: {
+ middlewares: {
+ "redirect-to-https": {
+ redirectScheme: {
+ scheme: "https",
+ permanent: true,
+ },
+ },
+ },
+ },
+ };
+ const yamlStr = dump(defaultMiddlewares);
+ return yamlStr;
+};
+export const createDefaultMiddlewares = () => {
+ const { DYNAMIC_TRAEFIK_PATH } = paths();
+ const middlewaresPath = path.join(DYNAMIC_TRAEFIK_PATH, "middlewares.yml");
+ if (existsSync(middlewaresPath)) {
+ console.log("Default middlewares already exists");
+ return;
+ }
+ const yamlStr = getDefaultMiddlewares();
+ mkdirSync(DYNAMIC_TRAEFIK_PATH, { recursive: true });
+ writeFileSync(middlewaresPath, yamlStr, "utf8");
+};
diff --git a/packages/builders/src/templates/appsmith/docker-compose.yml b/packages/builders/src/templates/appsmith/docker-compose.yml
new file mode 100644
index 000000000..f520ee362
--- /dev/null
+++ b/packages/builders/src/templates/appsmith/docker-compose.yml
@@ -0,0 +1,6 @@
+version: "3.8"
+services:
+ appsmith:
+ image: index.docker.io/appsmith/appsmith-ee:v1.29
+ volumes:
+ - ../files/stacks:/appsmith-stacks
diff --git a/packages/builders/src/templates/appsmith/index.ts b/packages/builders/src/templates/appsmith/index.ts
new file mode 100644
index 000000000..ff744a249
--- /dev/null
+++ b/packages/builders/src/templates/appsmith/index.ts
@@ -0,0 +1,23 @@
+import {
+ type DomainSchema,
+ type Schema,
+ type Template,
+ generateHash,
+ generateRandomDomain,
+} from "../utils";
+
+export function generate(schema: Schema): Template {
+ const mainServiceHash = generateHash(schema.projectName);
+
+ const domains: DomainSchema[] = [
+ {
+ host: generateRandomDomain(schema),
+ port: 80,
+ serviceName: "appsmith",
+ },
+ ];
+
+ return {
+ domains,
+ };
+}
diff --git a/packages/builders/src/templates/aptabase/docker-compose.yml b/packages/builders/src/templates/aptabase/docker-compose.yml
new file mode 100644
index 000000000..934fd1eea
--- /dev/null
+++ b/packages/builders/src/templates/aptabase/docker-compose.yml
@@ -0,0 +1,51 @@
+services:
+ aptabase_db:
+ image: postgres:15-alpine
+ restart: always
+ volumes:
+ - db-data:/var/lib/postgresql/data
+ environment:
+ POSTGRES_USER: aptabase
+ POSTGRES_PASSWORD: sTr0NGp4ssw0rd
+ networks:
+ - dokploy-network
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U aptabase"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+
+ aptabase_events_db:
+ image: clickhouse/clickhouse-server:23.8.16.16-alpine
+ restart: always
+ volumes:
+ - events-db-data:/var/lib/clickhouse
+ environment:
+ CLICKHOUSE_USER: aptabase
+ CLICKHOUSE_PASSWORD: sTr0NGp4ssw0rd
+ ulimits:
+ nofile:
+ soft: 262144
+ hard: 262144
+ networks:
+ - dokploy-network
+ healthcheck:
+ test: ["CMD-SHELL", "curl -f http://localhost:8123 || exit 1"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+
+ aptabase:
+ image: ghcr.io/aptabase/aptabase:main
+ restart: always
+ environment:
+ BASE_URL: http://${APTABASE_HOST}
+ AUTH_SECRET: ${AUTH_SECRET}
+ DATABASE_URL: Server=aptabase_db;Port=5432;User Id=aptabase;Password=sTr0NGp4ssw0rd;Database=aptabase
+ CLICKHOUSE_URL: Host=aptabase_events_db;Port=8123;Username=aptabase;Password=sTr0NGp4ssw0rd
+
+volumes:
+ db-data:
+ driver: local
+ events-db-data:
+ driver: local
diff --git a/packages/builders/src/templates/aptabase/index.ts b/packages/builders/src/templates/aptabase/index.ts
new file mode 100644
index 000000000..38b077ae8
--- /dev/null
+++ b/packages/builders/src/templates/aptabase/index.ts
@@ -0,0 +1,27 @@
+import {
+ type DomainSchema,
+ type Schema,
+ type Template,
+ generateBase64,
+ generateRandomDomain,
+} from "../utils";
+
+export function generate(schema: Schema): Template {
+ const mainDomain = generateRandomDomain(schema);
+ const authSecret = generateBase64(32);
+
+ const domains: DomainSchema[] = [
+ {
+ host: mainDomain,
+ port: 8080,
+ serviceName: "aptabase",
+ },
+ ];
+
+ const envs = [`APTABASE_HOST=${mainDomain}`, `AUTH_SECRET=${authSecret}`];
+
+ return {
+ envs,
+ domains,
+ };
+}
diff --git a/packages/builders/src/templates/baserow/docker-compose.yml b/packages/builders/src/templates/baserow/docker-compose.yml
new file mode 100644
index 000000000..db588e83a
--- /dev/null
+++ b/packages/builders/src/templates/baserow/docker-compose.yml
@@ -0,0 +1,10 @@
+version: "3.8"
+services:
+ baserow:
+ image: baserow/baserow:1.25.2
+ environment:
+ BASEROW_PUBLIC_URL: "http://${BASEROW_HOST}"
+ volumes:
+ - baserow_data:/baserow/data
+volumes:
+ baserow_data:
diff --git a/packages/builders/src/templates/baserow/index.ts b/packages/builders/src/templates/baserow/index.ts
new file mode 100644
index 000000000..fa57417cc
--- /dev/null
+++ b/packages/builders/src/templates/baserow/index.ts
@@ -0,0 +1,24 @@
+import {
+ type DomainSchema,
+ type Schema,
+ type Template,
+ generateRandomDomain,
+} from "../utils";
+
+export function generate(schema: Schema): Template {
+ const mainHost = generateRandomDomain(schema);
+
+ const domains: DomainSchema[] = [
+ {
+ host: mainHost,
+ port: 80,
+ serviceName: "baserow",
+ },
+ ];
+ const envs = [`BASEROW_HOST=${mainHost}`];
+
+ return {
+ envs,
+ domains,
+ };
+}
diff --git a/packages/builders/src/templates/calcom/docker-compose.yml b/packages/builders/src/templates/calcom/docker-compose.yml
new file mode 100644
index 000000000..7a1d8c92a
--- /dev/null
+++ b/packages/builders/src/templates/calcom/docker-compose.yml
@@ -0,0 +1,26 @@
+services:
+ postgres:
+ image: postgres:16-alpine
+ networks:
+ - dokploy-network
+ volumes:
+ - calcom-data:/var/lib/postgresql/data
+ environment:
+ - POSTGRES_USER=postgres
+ - POSTGRES_PASSWORD=password
+ - POSTGRES_DB=db
+ - DATABASE_URL=postgres://postgres:password@postgres:5432/db
+
+ calcom:
+ image: calcom/cal.com:v2.7.6
+ depends_on:
+ - postgres
+ environment:
+ - NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
+ - CALENDSO_ENCRYPTION_KEY=${CALENDSO_ENCRYPTION_KEY}
+ - DATABASE_URL=postgres://postgres:password@postgres:5432/db
+ - NEXT_PUBLIC_WEBAPP_URL=http://${CALCOM_HOST}
+ - NEXTAUTH_URL=http://${CALCOM_HOST}/api/auth
+
+volumes:
+ calcom-data:
diff --git a/packages/builders/src/templates/calcom/index.ts b/packages/builders/src/templates/calcom/index.ts
new file mode 100644
index 000000000..d359e9c7c
--- /dev/null
+++ b/packages/builders/src/templates/calcom/index.ts
@@ -0,0 +1,32 @@
+import {
+ type DomainSchema,
+ type Schema,
+ type Template,
+ generateBase64,
+ generateRandomDomain,
+} from "../utils";
+
+export function generate(schema: Schema): Template {
+ const mainDomain = generateRandomDomain(schema);
+ const calcomEncryptionKey = generateBase64(32);
+ const nextAuthSecret = generateBase64(32);
+
+ const domains: DomainSchema[] = [
+ {
+ host: mainDomain,
+ port: 3000,
+ serviceName: "calcom",
+ },
+ ];
+
+ const envs = [
+ `CALCOM_HOST=${mainDomain}`,
+ `NEXTAUTH_SECRET=${nextAuthSecret}`,
+ `CALENDSO_ENCRYPTION_KEY=${calcomEncryptionKey}`,
+ ];
+
+ return {
+ envs,
+ domains,
+ };
+}
diff --git a/packages/builders/src/templates/directus/docker-compose.yml b/packages/builders/src/templates/directus/docker-compose.yml
new file mode 100644
index 000000000..6f058ba61
--- /dev/null
+++ b/packages/builders/src/templates/directus/docker-compose.yml
@@ -0,0 +1,47 @@
+version: "3.8"
+services:
+ database:
+ image: postgis/postgis:13-master
+ volumes:
+ - directus:/var/lib/postgresql/data
+ networks:
+ - dokploy-network
+ environment:
+ POSTGRES_USER: "directus"
+ POSTGRES_PASSWORD: "directus"
+ POSTGRES_DB: "directus"
+
+ cache:
+ image: redis:6
+ networks:
+ - dokploy-network
+
+ directus:
+ image: directus/directus:10.12.1
+ ports:
+ - 8055
+ volumes:
+ - ../files/uploads:/directus/uploads
+ - ../files/extensions:/directus/extensions
+ depends_on:
+ - cache
+ - database
+ environment:
+ SECRET: "replace-with-secure-random-value"
+
+ DB_CLIENT: "pg"
+ DB_HOST: "database"
+ DB_PORT: "5432"
+ DB_DATABASE: "directus"
+ DB_USER: "directus"
+ DB_PASSWORD: "directus"
+
+ CACHE_ENABLED: "true"
+ CACHE_AUTO_PURGE: "true"
+ CACHE_STORE: "redis"
+ REDIS: "redis://cache:6379"
+
+ ADMIN_EMAIL: "admin@example.com"
+ ADMIN_PASSWORD: "d1r3ctu5"
+volumes:
+ directus:
diff --git a/packages/builders/src/templates/directus/index.ts b/packages/builders/src/templates/directus/index.ts
new file mode 100644
index 000000000..42a05aee0
--- /dev/null
+++ b/packages/builders/src/templates/directus/index.ts
@@ -0,0 +1,20 @@
+import {
+ type DomainSchema,
+ type Schema,
+ type Template,
+ generateRandomDomain,
+} from "../utils";
+
+export function generate(schema: Schema): Template {
+ const domains: DomainSchema[] = [
+ {
+ host: generateRandomDomain(schema),
+ port: 8055,
+ serviceName: "directus",
+ },
+ ];
+
+ return {
+ domains,
+ };
+}
diff --git a/packages/builders/src/templates/documenso/docker-compose.yml b/packages/builders/src/templates/documenso/docker-compose.yml
new file mode 100644
index 000000000..562fe4987
--- /dev/null
+++ b/packages/builders/src/templates/documenso/docker-compose.yml
@@ -0,0 +1,43 @@
+version: "3.8"
+services:
+ postgres:
+ image: postgres:16
+ networks:
+ - dokploy-network
+ volumes:
+ - documenso-data:/var/lib/postgresql/data
+ environment:
+ - POSTGRES_USER=documenso
+ - POSTGRES_PASSWORD=password
+ - POSTGRES_DB=documenso
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U documenso"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+ start_period: 10s
+
+ documenso:
+ image: documenso/documenso:v1.5.6-rc.2
+ depends_on:
+ postgres:
+ condition: service_healthy
+ environment:
+ - PORT=${DOCUMENSO_PORT}
+ - NEXTAUTH_URL=http://${DOCUMENSO_HOST}
+ - NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
+ - NEXT_PRIVATE_ENCRYPTION_KEY=${NEXT_PRIVATE_ENCRYPTION_KEY}
+ - NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY=${NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY}
+ - NEXT_PUBLIC_WEBAPP_URL=http://${DOCUMENSO_HOST}
+ - NEXT_PRIVATE_DATABASE_URL=postgres://documenso:password@postgres:5432/documenso
+ - NEXT_PRIVATE_DIRECT_DATABASE_URL=postgres://documenso:password@postgres:5432/documenso
+ - NEXT_PUBLIC_UPLOAD_TRANSPORT=database
+ - NEXT_PRIVATE_SMTP_TRANSPORT=smtp-auth
+ - NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH=/opt/documenso/cert.p12
+ ports:
+ - ${DOCUMENSO_PORT}
+ volumes:
+ - /opt/documenso/cert.p12:/opt/documenso/cert.p12
+
+volumes:
+ documenso-data:
diff --git a/packages/builders/src/templates/documenso/index.ts b/packages/builders/src/templates/documenso/index.ts
new file mode 100644
index 000000000..c70d1db4a
--- /dev/null
+++ b/packages/builders/src/templates/documenso/index.ts
@@ -0,0 +1,36 @@
+import {
+ type DomainSchema,
+ type Schema,
+ type Template,
+ generateBase64,
+ generatePassword,
+ generateRandomDomain,
+} from "../utils";
+
+export function generate(schema: Schema): Template {
+ const mainDomain = generateRandomDomain(schema);
+ const nextAuthSecret = generateBase64(32);
+ const documensoEncryptionKey = generatePassword(32);
+ const documensoSecondaryEncryptionKey = generatePassword(64);
+
+ const domains: DomainSchema[] = [
+ {
+ host: mainDomain,
+ port: 3000,
+ serviceName: "documenso",
+ },
+ ];
+
+ const envs = [
+ `DOCUMENSO_HOST=${mainDomain}`,
+ "DOCUMENSO_PORT=3000",
+ `NEXTAUTH_SECRET=${nextAuthSecret}`,
+ `NEXT_PRIVATE_ENCRYPTION_KEY=${documensoEncryptionKey}`,
+ `NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY=${documensoSecondaryEncryptionKey}`,
+ ];
+
+ return {
+ envs,
+ domains,
+ };
+}
diff --git a/packages/builders/src/templates/doublezero/docker-compose.yml b/packages/builders/src/templates/doublezero/docker-compose.yml
new file mode 100644
index 000000000..352470334
--- /dev/null
+++ b/packages/builders/src/templates/doublezero/docker-compose.yml
@@ -0,0 +1,19 @@
+services:
+ doublezero:
+ restart: always
+ image: liltechnomancer/double-zero:0.2.1
+ volumes:
+ - db-data:/var/lib/doublezero/data
+ environment:
+ AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
+ AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
+ AWS_REGION: ${AWS_REGION}
+ SQS_URL: ${SQS_URL}
+ SYSTEM_EMAIL: ${SYSTEM_EMAIL}
+ SECRET_KEY_BASE: ${SECRET_KEY_BASE}
+ PHX_HOST: ${DOUBLEZERO_HOST}
+ DATABASE_PATH: ./00.db
+
+volumes:
+ db-data:
+ driver: local
diff --git a/packages/builders/src/templates/doublezero/index.ts b/packages/builders/src/templates/doublezero/index.ts
new file mode 100644
index 000000000..fa774e9dc
--- /dev/null
+++ b/packages/builders/src/templates/doublezero/index.ts
@@ -0,0 +1,36 @@
+import {
+ type DomainSchema,
+ type Schema,
+ type Template,
+ generateBase64,
+ generateRandomDomain,
+} from "../utils";
+
+export function generate(schema: Schema): Template {
+ const mainDomain = generateRandomDomain(schema);
+ const secretKeyBase = generateBase64(64);
+
+ const domains: DomainSchema[] = [
+ {
+ host: mainDomain,
+ port: 4000,
+ serviceName: "doublezero",
+ },
+ ];
+
+ const envs = [
+ `DOUBLEZERO_HOST=${mainDomain}`,
+ "DOUBLEZERO_PORT=4000",
+ `SECRET_KEY_BASE=${secretKeyBase}`,
+ "AWS_ACCESS_KEY_ID=your-aws-access-key",
+ "AWS_SECRET_ACCESS_KEY=your-aws-secret-key",
+ "AWS_REGION=your-aws-region",
+ "SQS_URL=your-aws-sqs-url",
+ "SYSTEM_EMAIL=",
+ ];
+
+ return {
+ envs,
+ domains,
+ };
+}
diff --git a/packages/builders/src/templates/excalidraw/docker-compose.yml b/packages/builders/src/templates/excalidraw/docker-compose.yml
new file mode 100644
index 000000000..8743434b6
--- /dev/null
+++ b/packages/builders/src/templates/excalidraw/docker-compose.yml
@@ -0,0 +1,7 @@
+version: "3.8"
+
+services:
+ excalidraw:
+ networks:
+ - dokploy-network
+ image: excalidraw/excalidraw:latest
diff --git a/packages/builders/src/templates/excalidraw/index.ts b/packages/builders/src/templates/excalidraw/index.ts
new file mode 100644
index 000000000..13a43c440
--- /dev/null
+++ b/packages/builders/src/templates/excalidraw/index.ts
@@ -0,0 +1,23 @@
+import {
+ type DomainSchema,
+ type Schema,
+ type Template,
+ generateHash,
+ generateRandomDomain,
+} from "../utils";
+
+export function generate(schema: Schema): Template {
+ const mainDomain = generateRandomDomain(schema);
+
+ const domains: DomainSchema[] = [
+ {
+ host: mainDomain,
+ port: 80,
+ serviceName: "excalidraw",
+ },
+ ];
+
+ return {
+ domains,
+ };
+}
diff --git a/packages/builders/src/templates/ghost/docker-compose.yml b/packages/builders/src/templates/ghost/docker-compose.yml
new file mode 100644
index 000000000..288c59e54
--- /dev/null
+++ b/packages/builders/src/templates/ghost/docker-compose.yml
@@ -0,0 +1,29 @@
+version: "3.8"
+services:
+ ghost:
+ image: ghost:5-alpine
+ restart: always
+ environment:
+ database__client: mysql
+ database__connection__host: db
+ database__connection__user: root
+ database__connection__password: example
+ database__connection__database: ghost
+ url: http://${GHOST_HOST}
+
+ volumes:
+ - ghost:/var/lib/ghost/content
+
+ db:
+ image: mysql:8.0
+ restart: always
+ networks:
+ - dokploy-network
+ environment:
+ MYSQL_ROOT_PASSWORD: example
+ volumes:
+ - db:/var/lib/mysql
+
+volumes:
+ ghost:
+ db:
diff --git a/packages/builders/src/templates/ghost/index.ts b/packages/builders/src/templates/ghost/index.ts
new file mode 100644
index 000000000..1a88c3629
--- /dev/null
+++ b/packages/builders/src/templates/ghost/index.ts
@@ -0,0 +1,25 @@
+import {
+ type DomainSchema,
+ type Schema,
+ type Template,
+ generateHash,
+ generateRandomDomain,
+} from "../utils";
+
+export function generate(schema: Schema): Template {
+ const mainDomain = generateRandomDomain(schema);
+
+ const domains: DomainSchema[] = [
+ {
+ host: mainDomain,
+ port: 2368,
+ serviceName: "ghost",
+ },
+ ];
+ const envs = [`GHOST_HOST=${mainDomain}`];
+
+ return {
+ envs,
+ domains,
+ };
+}
diff --git a/packages/builders/src/templates/gitea/docker-compose.yml b/packages/builders/src/templates/gitea/docker-compose.yml
new file mode 100644
index 000000000..679936fb7
--- /dev/null
+++ b/packages/builders/src/templates/gitea/docker-compose.yml
@@ -0,0 +1,37 @@
+version: "3.8"
+services:
+ gitea:
+ image: gitea/gitea:1.22.2
+ environment:
+ - USER_UID=${USER_UID}
+ - USER_GID=${USER_GID}
+ - GITEA__database__DB_TYPE=postgres
+ - GITEA__database__HOST=db:5432
+ - GITEA__database__NAME=gitea
+ - GITEA__database__USER=gitea
+ - GITEA__database__PASSWD=gitea
+ restart: always
+ networks:
+ - dokploy-network
+ volumes:
+ - gitea_server:/data
+ - /etc/timezone:/etc/timezone:ro
+ - /etc/localtime:/etc/localtime:ro
+ depends_on:
+ - db
+
+ db:
+ image: postgres:16
+ restart: always
+ environment:
+ - POSTGRES_USER=gitea
+ - POSTGRES_PASSWORD=gitea
+ - POSTGRES_DB=gitea
+ networks:
+ - dokploy-network
+ volumes:
+ - gitea_db:/var/lib/postgresql/data
+
+volumes:
+ gitea_db:
+ gitea_server:
\ No newline at end of file
diff --git a/packages/builders/src/templates/gitea/index.ts b/packages/builders/src/templates/gitea/index.ts
new file mode 100644
index 000000000..92a20df64
--- /dev/null
+++ b/packages/builders/src/templates/gitea/index.ts
@@ -0,0 +1,24 @@
+import {
+ type DomainSchema,
+ type Schema,
+ type Template,
+ generateRandomDomain,
+} from "../utils";
+
+export function generate(schema: Schema): Template {
+ const mainDomain = generateRandomDomain(schema);
+
+ const domains: DomainSchema[] = [
+ {
+ host: mainDomain,
+ port: 3000,
+ serviceName: "gitea",
+ },
+ ];
+ const envs = ["USER_UID=1000", "USER_GID=1000"];
+
+ return {
+ envs,
+ domains,
+ };
+}
diff --git a/packages/builders/src/templates/glitchtip/docker-compose.yml b/packages/builders/src/templates/glitchtip/docker-compose.yml
new file mode 100644
index 000000000..e45c76627
--- /dev/null
+++ b/packages/builders/src/templates/glitchtip/docker-compose.yml
@@ -0,0 +1,59 @@
+x-environment: &default-environment
+ DATABASE_URL: postgres://postgres:postgres@postgres:5432/postgres
+ SECRET_KEY: ${SECRET_KEY}
+ PORT: ${GLITCHTIP_PORT}
+ EMAIL_URL: consolemail://
+ GLITCHTIP_DOMAIN: http://${GLITCHTIP_HOST}
+ DEFAULT_FROM_EMAIL: email@glitchtip.com
+ CELERY_WORKER_AUTOSCALE: "1,3"
+ CELERY_WORKER_MAX_TASKS_PER_CHILD: "10000"
+
+x-depends_on: &default-depends_on
+ - postgres
+ - redis
+
+services:
+ postgres:
+ image: postgres:16
+ environment:
+ POSTGRES_HOST_AUTH_METHOD: "trust"
+ restart: unless-stopped
+ volumes:
+ - pg-data:/var/lib/postgresql/data
+ networks:
+ - dokploy-network
+ redis:
+ image: redis
+ restart: unless-stopped
+ networks:
+ - dokploy-network
+ web:
+ image: glitchtip/glitchtip:v4.0
+ depends_on: *default-depends_on
+ ports:
+ - ${GLITCHTIP_PORT}
+ environment: *default-environment
+ restart: unless-stopped
+ volumes:
+ - uploads:/code/uploads
+ worker:
+ image: glitchtip/glitchtip:v4.0
+ command: ./bin/run-celery-with-beat.sh
+ depends_on: *default-depends_on
+ environment: *default-environment
+ restart: unless-stopped
+ volumes:
+ - uploads:/code/uploads
+ networks:
+ - dokploy-network
+ migrate:
+ image: glitchtip/glitchtip:v4.0
+ depends_on: *default-depends_on
+ command: "./manage.py migrate"
+ environment: *default-environment
+ networks:
+ - dokploy-network
+
+volumes:
+ pg-data:
+ uploads:
diff --git a/packages/builders/src/templates/glitchtip/index.ts b/packages/builders/src/templates/glitchtip/index.ts
new file mode 100644
index 000000000..093d752e1
--- /dev/null
+++ b/packages/builders/src/templates/glitchtip/index.ts
@@ -0,0 +1,30 @@
+import {
+ type DomainSchema,
+ type Schema,
+ type Template,
+ generateBase64,
+ generateRandomDomain,
+} from "../utils";
+
+export function generate(schema: Schema): Template {
+ const mainDomain = generateRandomDomain(schema);
+ const secretKey = generateBase64(32);
+
+ const domains: DomainSchema[] = [
+ {
+ host: mainDomain,
+ port: 8000,
+ serviceName: "web",
+ },
+ ];
+ const envs = [
+ `GLITCHTIP_HOST=${mainDomain}`,
+ "GLITCHTIP_PORT=8000",
+ `SECRET_KEY=${secretKey}`,
+ ];
+
+ return {
+ envs,
+ domains,
+ };
+}
diff --git a/packages/builders/src/templates/grafana/docker-compose.yml b/packages/builders/src/templates/grafana/docker-compose.yml
new file mode 100644
index 000000000..9d913c17f
--- /dev/null
+++ b/packages/builders/src/templates/grafana/docker-compose.yml
@@ -0,0 +1,9 @@
+version: "3.8"
+services:
+ grafana:
+ image: grafana/grafana-enterprise:9.5.20
+ restart: unless-stopped
+ volumes:
+ - grafana-storage:/var/lib/grafana
+volumes:
+ grafana-storage: {}
diff --git a/packages/builders/src/templates/grafana/index.ts b/packages/builders/src/templates/grafana/index.ts
new file mode 100644
index 000000000..fb614ef32
--- /dev/null
+++ b/packages/builders/src/templates/grafana/index.ts
@@ -0,0 +1,19 @@
+import {
+ type DomainSchema,
+ type Schema,
+ type Template,
+ generateRandomDomain,
+} from "../utils";
+
+export function generate(schema: Schema): Template {
+ const domains: DomainSchema[] = [
+ {
+ host: generateRandomDomain(schema),
+ port: 3000,
+ serviceName: "grafana",
+ },
+ ];
+ return {
+ domains,
+ };
+}
diff --git a/packages/builders/src/templates/jellyfin/docker-compose.yml b/packages/builders/src/templates/jellyfin/docker-compose.yml
new file mode 100644
index 000000000..cb61476ab
--- /dev/null
+++ b/packages/builders/src/templates/jellyfin/docker-compose.yml
@@ -0,0 +1,19 @@
+version: "3.8"
+services:
+ jellyfin:
+ image: jellyfin/jellyfin:10
+ volumes:
+ - config:/config
+ - cache:/cache
+ - media:/media
+ restart: "unless-stopped"
+ # Optional - alternative address used for autodiscovery
+ environment:
+ - JELLYFIN_PublishedServerUrl=http://${JELLYFIN_HOST}
+ # Optional - may be necessary for docker healthcheck to pass if running in host network mode
+ extra_hosts:
+ - "host.docker.internal:host-gateway"
+volumes:
+ config:
+ cache:
+ media:
diff --git a/packages/builders/src/templates/jellyfin/index.ts b/packages/builders/src/templates/jellyfin/index.ts
new file mode 100644
index 000000000..61c9c9b7c
--- /dev/null
+++ b/packages/builders/src/templates/jellyfin/index.ts
@@ -0,0 +1,25 @@
+// EXAMPLE
+import {
+ type DomainSchema,
+ type Schema,
+ type Template,
+ generateRandomDomain,
+} from "../utils";
+
+export function generate(schema: Schema): Template {
+ const domain = generateRandomDomain(schema);
+ const domains: DomainSchema[] = [
+ {
+ host: domain,
+ port: 8096,
+ serviceName: "jellyfin",
+ },
+ ];
+
+ const envs = [`JELLYFIN_HOST=${domain}`];
+
+ return {
+ envs,
+ domains,
+ };
+}
diff --git a/packages/builders/src/templates/listmonk/docker-compose.yml b/packages/builders/src/templates/listmonk/docker-compose.yml
new file mode 100644
index 000000000..725d0a098
--- /dev/null
+++ b/packages/builders/src/templates/listmonk/docker-compose.yml
@@ -0,0 +1,49 @@
+services:
+ db:
+ image: postgres:13
+ ports:
+ - 5432
+ networks:
+ - dokploy-network
+ environment:
+ - POSTGRES_PASSWORD=listmonk
+ - POSTGRES_USER=listmonk
+ - POSTGRES_DB=listmonk
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U listmonk"]
+ interval: 10s
+ timeout: 5s
+ retries: 6
+ volumes:
+ - listmonk-data:/var/lib/postgresql/data
+
+ setup:
+ image: listmonk/listmonk:v3.0.0
+ networks:
+ - dokploy-network
+ volumes:
+ - ../files/config.toml:/listmonk/config.toml
+ depends_on:
+ - db
+ command:
+ [
+ sh,
+ -c,
+ "sleep 3 && ./listmonk --install --idempotent --yes --config config.toml",
+ ]
+
+ app:
+ restart: unless-stopped
+ image: listmonk/listmonk:v3.0.0
+ environment:
+ - TZ=Etc/UTC
+ depends_on:
+ - db
+ - setup
+ volumes:
+ - ../files/config.toml:/listmonk/config.toml
+
+volumes:
+ listmonk-data:
+ driver: local
diff --git a/packages/builders/src/templates/listmonk/index.ts b/packages/builders/src/templates/listmonk/index.ts
new file mode 100644
index 000000000..725659ca4
--- /dev/null
+++ b/packages/builders/src/templates/listmonk/index.ts
@@ -0,0 +1,57 @@
+import {
+ type DomainSchema,
+ type Schema,
+ type Template,
+ generatePassword,
+ generateRandomDomain,
+} from "../utils";
+
+export function generate(schema: Schema): Template {
+ const randomDomain = generateRandomDomain(schema);
+ const adminPassword = generatePassword(32);
+
+ const domains: DomainSchema[] = [
+ {
+ host: randomDomain,
+ port: 9000,
+ serviceName: "app",
+ },
+ ];
+
+ const envs = [
+ `# login with admin:${adminPassword}`,
+ "# check config.toml in Advanced / Volumes for more options",
+ ];
+
+ const mounts: Template["mounts"] = [
+ {
+ filePath: "config.toml",
+ content: `[app]
+address = "0.0.0.0:9000"
+
+admin_username = "admin"
+admin_password = "${adminPassword}"
+
+[db]
+host = "db"
+port = 5432
+user = "listmonk"
+password = "listmonk"
+database = "listmonk"
+
+ssl_mode = "disable"
+max_open = 25
+max_idle = 25
+max_lifetime = "300s"
+
+params = ""
+`,
+ },
+ ];
+
+ return {
+ envs,
+ mounts,
+ domains,
+ };
+}
diff --git a/packages/builders/src/templates/meilisearch/docker-compose.yml b/packages/builders/src/templates/meilisearch/docker-compose.yml
new file mode 100644
index 000000000..ae5ebcb1a
--- /dev/null
+++ b/packages/builders/src/templates/meilisearch/docker-compose.yml
@@ -0,0 +1,14 @@
+version: "3.8"
+
+services:
+ meilisearch:
+ image: getmeili/meilisearch:v1.8.3
+ volumes:
+ - meili_data:/meili_data
+ environment:
+ MEILI_MASTER_KEY: ${MEILI_MASTER_KEY}
+ MEILI_ENV: ${MEILI_ENV}
+
+volumes:
+ meili_data:
+ driver: local
diff --git a/packages/builders/src/templates/meilisearch/index.ts b/packages/builders/src/templates/meilisearch/index.ts
new file mode 100644
index 000000000..cfb8a9a4b
--- /dev/null
+++ b/packages/builders/src/templates/meilisearch/index.ts
@@ -0,0 +1,26 @@
+import {
+ type DomainSchema,
+ type Schema,
+ type Template,
+ generateBase64,
+ generateRandomDomain,
+} from "../utils";
+
+export function generate(schema: Schema): Template {
+ const mainDomain = generateRandomDomain(schema);
+ const masterKey = generateBase64(32);
+
+ const domains: DomainSchema[] = [
+ {
+ host: mainDomain,
+ port: 7700,
+ serviceName: "meilisearch",
+ },
+ ];
+ const envs = ["MEILI_ENV=development", `MEILI_MASTER_KEY=${masterKey}`];
+
+ return {
+ envs,
+ domains,
+ };
+}
diff --git a/packages/builders/src/templates/metabase/docker-compose.yml b/packages/builders/src/templates/metabase/docker-compose.yml
new file mode 100644
index 000000000..4dca4d015
--- /dev/null
+++ b/packages/builders/src/templates/metabase/docker-compose.yml
@@ -0,0 +1,26 @@
+version: "3.8"
+services:
+ metabase:
+ image: metabase/metabase:v0.50.8
+ volumes:
+ - /dev/urandom:/dev/random:ro
+ environment:
+ MB_DB_TYPE: postgres
+ MB_DB_DBNAME: metabaseappdb
+ MB_DB_PORT: 5432
+ MB_DB_USER: metabase
+ MB_DB_PASS: mysecretpassword
+ MB_DB_HOST: postgres
+ healthcheck:
+ test: curl --fail -I http://localhost:3000/api/health || exit 1
+ interval: 15s
+ timeout: 5s
+ retries: 5
+ postgres:
+ image: postgres:14
+ environment:
+ POSTGRES_USER: metabase
+ POSTGRES_DB: metabaseappdb
+ POSTGRES_PASSWORD: mysecretpassword
+ networks:
+ - dokploy-network
diff --git a/packages/builders/src/templates/metabase/index.ts b/packages/builders/src/templates/metabase/index.ts
new file mode 100644
index 000000000..0a08916e6
--- /dev/null
+++ b/packages/builders/src/templates/metabase/index.ts
@@ -0,0 +1,22 @@
+import {
+ type DomainSchema,
+ type Schema,
+ type Template,
+ generateRandomDomain,
+} from "../utils";
+
+export function generate(schema: Schema): Template {
+ const randomDomain = generateRandomDomain(schema);
+
+ const domains: DomainSchema[] = [
+ {
+ host: randomDomain,
+ port: 3000,
+ serviceName: "metabase",
+ },
+ ];
+
+ return {
+ domains,
+ };
+}
diff --git a/packages/builders/src/templates/minio/docker-compose.yml b/packages/builders/src/templates/minio/docker-compose.yml
new file mode 100644
index 000000000..4b24bbcce
--- /dev/null
+++ b/packages/builders/src/templates/minio/docker-compose.yml
@@ -0,0 +1,13 @@
+version: "3.8"
+services:
+ minio:
+ image: minio/minio
+ volumes:
+ - minio-data:/data
+ environment:
+ - MINIO_ROOT_USER=minioadmin
+ - MINIO_ROOT_PASSWORD=minioadmin123
+ command: server /data --console-address ":9001"
+
+volumes:
+ minio-data:
diff --git a/packages/builders/src/templates/minio/index.ts b/packages/builders/src/templates/minio/index.ts
new file mode 100644
index 000000000..1345aafdc
--- /dev/null
+++ b/packages/builders/src/templates/minio/index.ts
@@ -0,0 +1,28 @@
+import {
+ type DomainSchema,
+ type Schema,
+ type Template,
+ generateRandomDomain,
+} from "../utils";
+
+export function generate(schema: Schema): Template {
+ const mainDomain = generateRandomDomain(schema);
+ const apiDomain = generateRandomDomain(schema);
+
+ const domains: DomainSchema[] = [
+ {
+ host: mainDomain,
+ port: 9001,
+ serviceName: "minio",
+ },
+ {
+ host: apiDomain,
+ port: 9000,
+ serviceName: "minio",
+ },
+ ];
+
+ return {
+ domains,
+ };
+}
diff --git a/packages/builders/src/templates/n8n/docker-compose.yml b/packages/builders/src/templates/n8n/docker-compose.yml
new file mode 100644
index 000000000..f8fb1f169
--- /dev/null
+++ b/packages/builders/src/templates/n8n/docker-compose.yml
@@ -0,0 +1,18 @@
+version: "3.8"
+services:
+ n8n:
+ image: docker.n8n.io/n8nio/n8n:1.48.1
+ restart: always
+ environment:
+ - N8N_HOST=${N8N_HOST}
+ - N8N_PORT=${N8N_PORT}
+ - N8N_PROTOCOL=http
+ - NODE_ENV=production
+ - WEBHOOK_URL=https://${N8N_HOST}/
+ - GENERIC_TIMEZONE=${GENERIC_TIMEZONE}
+ - N8N_SECURE_COOKIE=false
+ volumes:
+ - n8n_data:/home/node/.n8n
+
+volumes:
+ n8n_data:
diff --git a/packages/builders/src/templates/n8n/index.ts b/packages/builders/src/templates/n8n/index.ts
new file mode 100644
index 000000000..da93c025f
--- /dev/null
+++ b/packages/builders/src/templates/n8n/index.ts
@@ -0,0 +1,28 @@
+import {
+ type DomainSchema,
+ type Schema,
+ type Template,
+ generateRandomDomain,
+} from "../utils";
+
+export function generate(schema: Schema): Template {
+ const mainDomain = generateRandomDomain(schema);
+
+ const domains: DomainSchema[] = [
+ {
+ host: mainDomain,
+ port: 5678,
+ serviceName: "n8n",
+ },
+ ];
+ const envs = [
+ `N8N_HOST=${mainDomain}`,
+ "N8N_PORT=5678",
+ "GENERIC_TIMEZONE=Europe/Berlin",
+ ];
+
+ return {
+ envs,
+ domains,
+ };
+}
diff --git a/packages/builders/src/templates/nocodb/docker-compose.yml b/packages/builders/src/templates/nocodb/docker-compose.yml
new file mode 100644
index 000000000..726cf5e61
--- /dev/null
+++ b/packages/builders/src/templates/nocodb/docker-compose.yml
@@ -0,0 +1,32 @@
+version: "3.8"
+services:
+ nocodb:
+ image: nocodb/nocodb:0.251.1
+ restart: always
+ environment:
+ NC_DB: "pg://root_db?u=postgres&p=password&d=root_db"
+ PORT: ${NOCODB_PORT}
+ NC_REDIS_URL: ${NC_REDIS_URL}
+ volumes:
+ - nc_data:/usr/app/data
+
+ root_db:
+ image: postgres:14.7
+ restart: always
+ networks:
+ - dokploy-network
+ environment:
+ POSTGRES_DB: root_db
+ POSTGRES_PASSWORD: password
+ POSTGRES_USER: postgres
+ healthcheck:
+ interval: 10s
+ retries: 10
+ test: 'pg_isready -U "$$POSTGRES_USER" -d "$$POSTGRES_DB"'
+ timeout: 2s
+ volumes:
+ - "db_data:/var/lib/postgresql/data"
+
+volumes:
+ db_data: {}
+ nc_data: {}
diff --git a/packages/builders/src/templates/nocodb/index.ts b/packages/builders/src/templates/nocodb/index.ts
new file mode 100644
index 000000000..60620dbd4
--- /dev/null
+++ b/packages/builders/src/templates/nocodb/index.ts
@@ -0,0 +1,28 @@
+// EXAMPLE
+import {
+ type DomainSchema,
+ type Schema,
+ type Template,
+ generateBase64,
+ generateRandomDomain,
+} from "../utils";
+
+export function generate(schema: Schema): Template {
+ const randomDomain = generateRandomDomain(schema);
+ const secretBase = generateBase64(64);
+
+ const domains: DomainSchema[] = [
+ {
+ host: randomDomain,
+ port: 8000,
+ serviceName: "nocodb",
+ },
+ ];
+
+ const envs = ["NOCODB_PORT=8000", `NC_AUTH_JWT_SECRET=${secretBase}`];
+
+ return {
+ envs,
+ domains,
+ };
+}
diff --git a/packages/builders/src/templates/odoo/docker-compose.yml b/packages/builders/src/templates/odoo/docker-compose.yml
new file mode 100644
index 000000000..80b34f0c0
--- /dev/null
+++ b/packages/builders/src/templates/odoo/docker-compose.yml
@@ -0,0 +1,29 @@
+version: "3.8"
+services:
+ web:
+ image: odoo:16.0
+ depends_on:
+ - db
+ environment:
+ - HOST=db
+ - USER=odoo
+ - PASSWORD=odoo
+ volumes:
+ - odoo-web-data:/var/lib/odoo
+ - ../files/config:/etc/odoo
+ - ../files/addons:/mnt/extra-addons
+
+ db:
+ image: postgres:13
+ networks:
+ - dokploy-network
+ environment:
+ - POSTGRES_DB=postgres
+ - POSTGRES_USER=odoo
+ - POSTGRES_PASSWORD=odoo
+ volumes:
+ - odoo-db-data:/var/lib/postgresql/data
+
+volumes:
+ odoo-web-data:
+ odoo-db-data:
diff --git a/packages/builders/src/templates/odoo/index.ts b/packages/builders/src/templates/odoo/index.ts
new file mode 100644
index 000000000..904293c11
--- /dev/null
+++ b/packages/builders/src/templates/odoo/index.ts
@@ -0,0 +1,22 @@
+import {
+ type DomainSchema,
+ type Schema,
+ type Template,
+ generateRandomDomain,
+} from "../utils";
+
+export function generate(schema: Schema): Template {
+ const randomDomain = generateRandomDomain(schema);
+
+ const domains: DomainSchema[] = [
+ {
+ host: randomDomain,
+ port: 8069,
+ serviceName: "web",
+ },
+ ];
+
+ return {
+ domains,
+ };
+}
diff --git a/packages/builders/src/templates/open-webui/docker-compose.yml b/packages/builders/src/templates/open-webui/docker-compose.yml
new file mode 100644
index 000000000..d396dacc0
--- /dev/null
+++ b/packages/builders/src/templates/open-webui/docker-compose.yml
@@ -0,0 +1,26 @@
+version: "3.8"
+services:
+ ollama:
+ volumes:
+ - ollama:/root/.ollama
+ networks:
+ - dokploy-network
+ pull_policy: always
+ tty: true
+ restart: unless-stopped
+ image: ollama/ollama:${OLLAMA_DOCKER_TAG-latest}
+
+ open-webui:
+ image: ghcr.io/open-webui/open-webui:${WEBUI_DOCKER_TAG-main}
+ volumes:
+ - open-webui:/app/backend/data
+ depends_on:
+ - ollama
+ environment:
+ - "OLLAMA_BASE_URL=http://ollama:11434"
+ - "WEBUI_SECRET_KEY="
+ restart: unless-stopped
+
+volumes:
+ ollama: {}
+ open-webui: {}
diff --git a/packages/builders/src/templates/open-webui/index.ts b/packages/builders/src/templates/open-webui/index.ts
new file mode 100644
index 000000000..0431c2a11
--- /dev/null
+++ b/packages/builders/src/templates/open-webui/index.ts
@@ -0,0 +1,24 @@
+import {
+ type DomainSchema,
+ type Schema,
+ type Template,
+ generateRandomDomain,
+} from "../utils";
+
+export function generate(schema: Schema): Template {
+ const randomDomain = generateRandomDomain(schema);
+
+ const domains: DomainSchema[] = [
+ {
+ host: randomDomain,
+ port: 8080,
+ serviceName: "open-webui",
+ },
+ ];
+ const envs = ["OLLAMA_DOCKER_TAG=0.1.47", "WEBUI_DOCKER_TAG=0.3.7"];
+
+ return {
+ envs,
+ domains,
+ };
+}
diff --git a/packages/builders/src/templates/phpmyadmin/docker-compose.yml b/packages/builders/src/templates/phpmyadmin/docker-compose.yml
new file mode 100644
index 000000000..1f775f09a
--- /dev/null
+++ b/packages/builders/src/templates/phpmyadmin/docker-compose.yml
@@ -0,0 +1,28 @@
+version: "3.8"
+
+services:
+ db:
+ image: mysql:5.7
+ environment:
+ MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
+ MYSQL_DATABASE: tu_base_de_datos
+ MYSQL_USER: ${MYSQL_USER}
+ MYSQL_PASSWORD: ${MYSQL_PASSWORD}
+ volumes:
+ - db_data:/var/lib/mysql
+ networks:
+ - dokploy-network
+
+ phpmyadmin:
+ image: phpmyadmin/phpmyadmin:5.2.1
+ environment:
+ PMA_HOST: db
+ PMA_USER: ${MYSQL_USER}
+ PMA_PASSWORD: ${MYSQL_PASSWORD}
+ PMA_ARBITRARY: 1
+ depends_on:
+ - db
+
+volumes:
+ db_data:
+ driver: local
diff --git a/packages/builders/src/templates/phpmyadmin/index.ts b/packages/builders/src/templates/phpmyadmin/index.ts
new file mode 100644
index 000000000..e1c976b9d
--- /dev/null
+++ b/packages/builders/src/templates/phpmyadmin/index.ts
@@ -0,0 +1,32 @@
+import {
+ type DomainSchema,
+ type Schema,
+ type Template,
+ generatePassword,
+ generateRandomDomain,
+} from "../utils";
+
+export function generate(schema: Schema): Template {
+ const mainDomain = generateRandomDomain(schema);
+ const rootPassword = generatePassword(32);
+ const password = generatePassword(32);
+
+ const domains: DomainSchema[] = [
+ {
+ host: mainDomain,
+ port: 80,
+ serviceName: "phpmyadmin",
+ },
+ ];
+ const envs = [
+ `MYSQL_ROOT_PASSWORD=${rootPassword}`,
+ "MYSQL_DATABASE=mysql",
+ "MYSQL_USER=phpmyadmin",
+ `MYSQL_PASSWORD=${password}`,
+ ];
+
+ return {
+ envs,
+ domains,
+ };
+}
diff --git a/packages/builders/src/templates/plausible/docker-compose.yml b/packages/builders/src/templates/plausible/docker-compose.yml
new file mode 100644
index 000000000..62ce5ece4
--- /dev/null
+++ b/packages/builders/src/templates/plausible/docker-compose.yml
@@ -0,0 +1,44 @@
+version: "3.8"
+services:
+ plausible_db:
+ image: postgres:16-alpine
+ restart: always
+ networks:
+ - dokploy-network
+ volumes:
+ - db-data:/var/lib/postgresql/data
+ environment:
+ - POSTGRES_PASSWORD=postgres
+
+ plausible_events_db:
+ image: clickhouse/clickhouse-server:24.3.3.102-alpine
+ restart: always
+ networks:
+ - dokploy-network
+ volumes:
+ - event-data:/var/lib/clickhouse
+ - event-logs:/var/log/clickhouse-server
+ - ../files/clickhouse/clickhouse-config.xml:/etc/clickhouse-server/config.d/logging.xml:ro
+ - ../files/clickhouse/clickhouse-user-config.xml:/etc/clickhouse-server/users.d/logging.xml:ro
+ ulimits:
+ nofile:
+ soft: 262144
+ hard: 262144
+
+ plausible:
+ image: ghcr.io/plausible/community-edition:v2.1.0
+ restart: always
+ command: sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh run"
+ depends_on:
+ - plausible_db
+ - plausible_events_db
+ env_file:
+ - .env
+
+volumes:
+ db-data:
+ driver: local
+ event-data:
+ driver: local
+ event-logs:
+ driver: local
diff --git a/packages/builders/src/templates/plausible/index.ts b/packages/builders/src/templates/plausible/index.ts
new file mode 100644
index 000000000..2bd1212b6
--- /dev/null
+++ b/packages/builders/src/templates/plausible/index.ts
@@ -0,0 +1,71 @@
+import {
+ type DomainSchema,
+ type Schema,
+ type Template,
+ generateBase64,
+ generateRandomDomain,
+} from "../utils";
+
+export function generate(schema: Schema): Template {
+ const mainDomain = generateRandomDomain(schema);
+ const secretBase = generateBase64(64);
+ const toptKeyBase = generateBase64(32);
+
+ const domains: DomainSchema[] = [
+ {
+ host: mainDomain,
+ port: 8000,
+ serviceName: "plausible",
+ },
+ ];
+
+ const envs = [
+ `BASE_URL=http://${mainDomain}`,
+ `SECRET_KEY_BASE=${secretBase}`,
+ `TOTP_VAULT_KEY=${toptKeyBase}`,
+ ];
+
+ const mounts: Template["mounts"] = [
+ {
+ filePath: "/clickhouse/clickhouse-config.xml",
+ content: `
+
+
+ warning
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ },
+ {
+ filePath: "/clickhouse/clickhouse-user-config.xml",
+ content: `
+
+
+
+ 0
+ 0
+
+
+
+ `,
+ },
+ ];
+
+ return {
+ envs,
+ mounts,
+ domains,
+ };
+}
diff --git a/packages/builders/src/templates/pocketbase/docker-compose.yml b/packages/builders/src/templates/pocketbase/docker-compose.yml
new file mode 100644
index 000000000..fa6674afe
--- /dev/null
+++ b/packages/builders/src/templates/pocketbase/docker-compose.yml
@@ -0,0 +1,9 @@
+version: "3.8"
+services:
+ pocketbase:
+ image: spectado/pocketbase:0.22.12
+ restart: unless-stopped
+ volumes:
+ - /etc/dokploy/templates/${HASH}/data:/pb_data
+ - /etc/dokploy/templates/${HASH}/public:/pb_public
+ - /etc/dokploy/templates/${HASH}/migrations:/pb_migrations
diff --git a/packages/builders/src/templates/pocketbase/index.ts b/packages/builders/src/templates/pocketbase/index.ts
new file mode 100644
index 000000000..f9fc7f8f0
--- /dev/null
+++ b/packages/builders/src/templates/pocketbase/index.ts
@@ -0,0 +1,22 @@
+import {
+ type DomainSchema,
+ type Schema,
+ type Template,
+ generateRandomDomain,
+} from "../utils";
+
+export function generate(schema: Schema): Template {
+ const mainDomain = generateRandomDomain(schema);
+
+ const domains: DomainSchema[] = [
+ {
+ host: mainDomain,
+ port: 80,
+ serviceName: "pocketbase",
+ },
+ ];
+
+ return {
+ domains,
+ };
+}
diff --git a/packages/builders/src/templates/rocketchat/docker-compose.yml b/packages/builders/src/templates/rocketchat/docker-compose.yml
new file mode 100644
index 000000000..751bd845c
--- /dev/null
+++ b/packages/builders/src/templates/rocketchat/docker-compose.yml
@@ -0,0 +1,35 @@
+version: "3.8"
+services:
+ rocketchat:
+ image: registry.rocket.chat/rocketchat/rocket.chat:6.9.2
+ restart: always
+ environment:
+ MONGO_URL: "mongodb://mongodb:27017/rocketchat?replicaSet=rs0"
+ MONGO_OPLOG_URL: "mongodb://mongodb:27017/local?replicaSet=rs0"
+ ROOT_URL: ${ROOT_URL:-http://${ROCKETCHAT_HOST}:${ROCKETCHAT_PORT}}
+ PORT: ${ROCKETCHAT_PORT}
+ DEPLOY_METHOD: docker
+ DEPLOY_PLATFORM:
+ REG_TOKEN:
+ depends_on:
+ - mongodb
+
+ mongodb:
+ image: docker.io/bitnami/mongodb:5.0
+ restart: always
+ volumes:
+ - mongodb_data:/bitnami/mongodb
+ environment:
+ MONGODB_REPLICA_SET_MODE: primary
+ MONGODB_REPLICA_SET_NAME: rs0
+ MONGODB_PORT_NUMBER: 27017
+ MONGODB_INITIAL_PRIMARY_HOST: mongodb
+ MONGODB_INITIAL_PRIMARY_PORT_NUMBER: 27017
+ MONGODB_ADVERTISED_HOSTNAME: mongodb
+ MONGODB_ENABLE_JOURNAL: true
+ ALLOW_EMPTY_PASSWORD: yes
+ networks:
+ - dokploy-network
+
+volumes:
+ mongodb_data: { driver: local }
diff --git a/packages/builders/src/templates/rocketchat/index.ts b/packages/builders/src/templates/rocketchat/index.ts
new file mode 100644
index 000000000..0c10307a2
--- /dev/null
+++ b/packages/builders/src/templates/rocketchat/index.ts
@@ -0,0 +1,25 @@
+import {
+ type DomainSchema,
+ type Schema,
+ type Template,
+ generateRandomDomain,
+} from "../utils";
+
+export function generate(schema: Schema): Template {
+ const mainDomain = generateRandomDomain(schema);
+
+ const domains: DomainSchema[] = [
+ {
+ host: mainDomain,
+ port: 3000,
+ serviceName: "rocketchat",
+ },
+ ];
+
+ const envs = [`ROCKETCHAT_HOST=${mainDomain}`, "ROCKETCHAT_PORT=3000"];
+
+ return {
+ envs,
+ domains,
+ };
+}
diff --git a/packages/builders/src/templates/soketi/docker-compose.yml b/packages/builders/src/templates/soketi/docker-compose.yml
new file mode 100644
index 000000000..1784cdc79
--- /dev/null
+++ b/packages/builders/src/templates/soketi/docker-compose.yml
@@ -0,0 +1,12 @@
+version: "3"
+
+services:
+ soketi:
+ image: quay.io/soketi/soketi:1.4-16-debian
+ container_name: soketi
+ environment:
+ SOKETI_DEBUG: "1"
+ SOKETI_HOST: "0.0.0.0"
+ SOKETI_PORT: "6001"
+ SOKETI_METRICS_SERVER_PORT: "9601"
+ restart: unless-stopped
diff --git a/packages/builders/src/templates/soketi/index.ts b/packages/builders/src/templates/soketi/index.ts
new file mode 100644
index 000000000..47aa461df
--- /dev/null
+++ b/packages/builders/src/templates/soketi/index.ts
@@ -0,0 +1,28 @@
+import {
+ type DomainSchema,
+ type Schema,
+ type Template,
+ generateRandomDomain,
+} from "../utils";
+
+export function generate(schema: Schema): Template {
+ const mainDomain = generateRandomDomain(schema);
+ const metricsDomain = generateRandomDomain(schema);
+
+ const domains: DomainSchema[] = [
+ {
+ host: mainDomain,
+ port: 6001,
+ serviceName: "soketi",
+ },
+ {
+ host: metricsDomain,
+ port: 9601,
+ serviceName: "soketi",
+ },
+ ];
+
+ return {
+ domains,
+ };
+}
diff --git a/packages/builders/src/templates/supabase/docker-compose.yml b/packages/builders/src/templates/supabase/docker-compose.yml
new file mode 100644
index 000000000..e1e187fde
--- /dev/null
+++ b/packages/builders/src/templates/supabase/docker-compose.yml
@@ -0,0 +1,460 @@
+# Usage
+# Start: docker compose up
+# With helpers: docker compose -f docker-compose.yml -f ../files/dev/docker-compose.dev.yml up
+# Stop: docker compose down
+# Destroy: docker compose -f docker-compose.yml -f ../files/dev/docker-compose.dev.yml down -v --remove-orphans
+
+name: supabase
+version: "3.8"
+
+services:
+ studio:
+ container_name: supabase-studio
+ image: supabase/studio:20240729-ce42139
+ networks:
+ - dokploy-network
+ restart: unless-stopped
+ healthcheck:
+ test:
+ [
+ "CMD",
+ "node",
+ "-e",
+ "require('http').get('http://localhost:3000/api/profile', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})",
+ ]
+ timeout: 5s
+ interval: 5s
+ retries: 3
+ depends_on:
+ analytics:
+ condition: service_healthy
+ environment:
+ STUDIO_PG_META_URL: http://meta:8080
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
+
+ DEFAULT_ORGANIZATION_NAME: ${STUDIO_DEFAULT_ORGANIZATION}
+ DEFAULT_PROJECT_NAME: ${STUDIO_DEFAULT_PROJECT}
+
+ SUPABASE_URL: http://kong:8000
+ SUPABASE_PUBLIC_URL: http://${SUPABASE_HOST}
+ SUPABASE_ANON_KEY: ${ANON_KEY}
+ SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
+ AUTH_JWT_SECRET: ${JWT_SECRET}
+
+ LOGFLARE_API_KEY: ${LOGFLARE_API_KEY}
+ LOGFLARE_URL: http://analytics:4000
+ NEXT_PUBLIC_ENABLE_LOGS: true
+ # Comment to use Big Query backend for analytics
+ NEXT_ANALYTICS_BACKEND_PROVIDER: postgres
+ # Uncomment to use Big Query backend for analytics
+ # NEXT_ANALYTICS_BACKEND_PROVIDER: bigquery
+
+ kong:
+ container_name: supabase-kong
+ image: kong:2.8.1
+ restart: unless-stopped
+ networks:
+ - dokploy-network
+ # https://unix.stackexchange.com/a/294837
+ entrypoint: bash -c 'eval "echo \"$$(cat ~/temp.yml)\"" > ~/kong.yml && /docker-entrypoint.sh kong docker-start'
+ #ports:
+ # - ${KONG_HTTP_PORT}:8000/tcp
+ # - ${KONG_HTTPS_PORT}:8443/tcp
+ expose:
+ - 8000
+ - 8443
+ depends_on:
+ analytics:
+ condition: service_healthy
+ environment:
+ KONG_DATABASE: "off"
+ KONG_DECLARATIVE_CONFIG: /home/kong/kong.yml
+ # https://github.com/supabase/cli/issues/14
+ KONG_DNS_ORDER: LAST,A,CNAME
+ KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth
+ KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k
+ KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k
+ SUPABASE_ANON_KEY: ${ANON_KEY}
+ SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
+ DASHBOARD_USERNAME: ${DASHBOARD_USERNAME}
+ DASHBOARD_PASSWORD: ${DASHBOARD_PASSWORD}
+ volumes:
+ # https://github.com/supabase/supabase/issues/12661
+ - ../files/volumes/api/kong.yml:/home/kong/temp.yml:ro
+
+ auth:
+ container_name: supabase-auth
+ image: supabase/gotrue:v2.158.1
+ networks:
+ - dokploy-network
+ depends_on:
+ db:
+ # Disable this if you are using an external Postgres database
+ condition: service_healthy
+ analytics:
+ condition: service_healthy
+ healthcheck:
+ test:
+ [
+ "CMD",
+ "wget",
+ "--no-verbose",
+ "--tries=1",
+ "--spider",
+ "http://localhost:9999/health",
+ ]
+ timeout: 5s
+ interval: 5s
+ retries: 3
+ restart: unless-stopped
+ environment:
+ GOTRUE_API_HOST: 0.0.0.0
+ GOTRUE_API_PORT: 9999
+ API_EXTERNAL_URL: http://${SUPABASE_HOST}
+
+ GOTRUE_DB_DRIVER: postgres
+ GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOSTNAME}:${POSTGRES_PORT}/${POSTGRES_DB}
+
+ GOTRUE_SITE_URL: http://${SUPABASE_HOST}
+ GOTRUE_URI_ALLOW_LIST: ${ADDITIONAL_REDIRECT_URLS}
+ GOTRUE_DISABLE_SIGNUP: ${DISABLE_SIGNUP}
+
+ GOTRUE_JWT_ADMIN_ROLES: service_role
+ GOTRUE_JWT_AUD: authenticated
+ GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
+ GOTRUE_JWT_EXP: ${JWT_EXPIRY}
+ GOTRUE_JWT_SECRET: ${JWT_SECRET}
+
+ GOTRUE_EXTERNAL_EMAIL_ENABLED: ${ENABLE_EMAIL_SIGNUP}
+ GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: ${ENABLE_ANONYMOUS_USERS}
+ GOTRUE_MAILER_AUTOCONFIRM: ${ENABLE_EMAIL_AUTOCONFIRM}
+ # GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED: true
+ # GOTRUE_SMTP_MAX_FREQUENCY: 1s
+ GOTRUE_SMTP_ADMIN_EMAIL: ${SMTP_ADMIN_EMAIL}
+ GOTRUE_SMTP_HOST: ${SMTP_HOSTNAME}
+ GOTRUE_SMTP_PORT: ${SMTP_PORT}
+ GOTRUE_SMTP_USER: ${SMTP_USER}
+ GOTRUE_SMTP_PASS: ${SMTP_PASS}
+ GOTRUE_SMTP_SENDER_NAME: ${SMTP_SENDER_NAME}
+ GOTRUE_MAILER_URLPATHS_INVITE: ${MAILER_URLPATHS_INVITE}
+ GOTRUE_MAILER_URLPATHS_CONFIRMATION: ${MAILER_URLPATHS_CONFIRMATION}
+ GOTRUE_MAILER_URLPATHS_RECOVERY: ${MAILER_URLPATHS_RECOVERY}
+ GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: ${MAILER_URLPATHS_EMAIL_CHANGE}
+
+ GOTRUE_EXTERNAL_PHONE_ENABLED: ${ENABLE_PHONE_SIGNUP}
+ GOTRUE_SMS_AUTOCONFIRM: ${ENABLE_PHONE_AUTOCONFIRM}
+ # Uncomment to enable custom access token hook. You'll need to create a public.custom_access_token_hook function and grant necessary permissions.
+ # See: https://supabase.com/docs/guides/auth/auth-hooks#hook-custom-access-token for details
+ # GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED="true"
+ # GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_URI="pg-functions://postgres/public/custom_access_token_hook"
+
+ # GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_ENABLED="true"
+ # GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_URI="pg-functions://postgres/public/mfa_verification_attempt"
+
+ # GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_ENABLED="true"
+ # GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_URI="pg-functions://postgres/public/password_verification_attempt"
+
+ rest:
+ container_name: supabase-rest
+ image: postgrest/postgrest:v12.2.0
+ networks:
+ - dokploy-network
+ depends_on:
+ db:
+ # Disable this if you are using an external Postgres database
+ condition: service_healthy
+ analytics:
+ condition: service_healthy
+ restart: unless-stopped
+ environment:
+ PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@${POSTGRES_HOSTNAME}:${POSTGRES_PORT}/${POSTGRES_DB}
+ PGRST_DB_SCHEMAS: ${PGRST_DB_SCHEMAS}
+ PGRST_DB_ANON_ROLE: anon
+ PGRST_JWT_SECRET: ${JWT_SECRET}
+ PGRST_DB_USE_LEGACY_GUCS: "false"
+ PGRST_APP_SETTINGS_JWT_SECRET: ${JWT_SECRET}
+ PGRST_APP_SETTINGS_JWT_EXP: ${JWT_EXPIRY}
+ command: "postgrest"
+
+ realtime:
+ # This container name looks inconsistent but is correct because realtime constructs tenant id by parsing the subdomain
+ container_name: realtime-dev.supabase-realtime
+ image: supabase/realtime:v2.30.23
+ networks:
+ - dokploy-network
+ depends_on:
+ db:
+ # Disable this if you are using an external Postgres database
+ condition: service_healthy
+ analytics:
+ condition: service_healthy
+ healthcheck:
+ test:
+ [
+ "CMD",
+ "curl",
+ "-sSfL",
+ "--head",
+ "-o",
+ "/dev/null",
+ "-H",
+ "Authorization: Bearer ${ANON_KEY}",
+ "http://localhost:4000/api/tenants/realtime-dev/health",
+ ]
+ timeout: 5s
+ interval: 5s
+ retries: 3
+ restart: unless-stopped
+ environment:
+ PORT: 4000
+ DB_HOST: ${POSTGRES_HOSTNAME}
+ DB_PORT: ${POSTGRES_PORT}
+ DB_USER: supabase_admin
+ DB_PASSWORD: ${POSTGRES_PASSWORD}
+ DB_NAME: ${POSTGRES_DB}
+ DB_AFTER_CONNECT_QUERY: "SET search_path TO _realtime"
+ DB_ENC_KEY: supabaserealtime
+ API_JWT_SECRET: ${JWT_SECRET}
+ SECRET_KEY_BASE: UpNVntn3cDxHJpq99YMc1T1AQgQpc8kfYTuRgBiYa15BLrx8etQoXz3gZv1/u2oq
+ ERL_AFLAGS: -proto_dist inet_tcp
+ DNS_NODES: "''"
+ RLIMIT_NOFILE: "10000"
+ APP_NAME: realtime
+ SEED_SELF_HOST: true
+
+ # To use S3 backed storage: docker compose -f docker-compose.yml -f docker-compose.s3.yml up
+ storage:
+ container_name: supabase-storage
+ image: supabase/storage-api:v1.0.6
+ networks:
+ - dokploy-network
+ depends_on:
+ db:
+ # Disable this if you are using an external Postgres database
+ condition: service_healthy
+ rest:
+ condition: service_started
+ imgproxy:
+ condition: service_started
+ healthcheck:
+ test:
+ [
+ "CMD",
+ "wget",
+ "--no-verbose",
+ "--tries=1",
+ "--spider",
+ "http://localhost:5000/status",
+ ]
+ timeout: 5s
+ interval: 5s
+ retries: 3
+ restart: unless-stopped
+ environment:
+ ANON_KEY: ${ANON_KEY}
+ SERVICE_KEY: ${SERVICE_ROLE_KEY}
+ POSTGREST_URL: http://rest:3000
+ PGRST_JWT_SECRET: ${JWT_SECRET}
+ DATABASE_URL: postgres://supabase_storage_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOSTNAME}:${POSTGRES_PORT}/${POSTGRES_DB}
+ FILE_SIZE_LIMIT: 52428800
+ STORAGE_BACKEND: file
+ FILE_STORAGE_BACKEND_PATH: /var/lib/storage
+ TENANT_ID: stub
+ # TODO: https://github.com/supabase/storage-api/issues/55
+ REGION: stub
+ GLOBAL_S3_BUCKET: stub
+ ENABLE_IMAGE_TRANSFORMATION: "true"
+ IMGPROXY_URL: http://imgproxy:5001
+ volumes:
+ - ../files/volumes/storage:/var/lib/storage:z
+
+ imgproxy:
+ container_name: supabase-imgproxy
+ image: darthsim/imgproxy:v3.8.0
+ networks:
+ - dokploy-network
+ healthcheck:
+ test: ["CMD", "imgproxy", "health"]
+ timeout: 5s
+ interval: 5s
+ retries: 3
+ environment:
+ IMGPROXY_BIND: ":5001"
+ IMGPROXY_LOCAL_FILESYSTEM_ROOT: /
+ IMGPROXY_USE_ETAG: "true"
+ IMGPROXY_ENABLE_WEBP_DETECTION: ${IMGPROXY_ENABLE_WEBP_DETECTION}
+ volumes:
+ - ../files/volumes/storage:/var/lib/storage:z
+
+ meta:
+ container_name: supabase-meta
+ image: supabase/postgres-meta:v0.83.2
+ networks:
+ - dokploy-network
+ depends_on:
+ db:
+ # Disable this if you are using an external Postgres database
+ condition: service_healthy
+ analytics:
+ condition: service_healthy
+ restart: unless-stopped
+ environment:
+ PG_META_PORT: 8080
+ PG_META_DB_HOST: ${POSTGRES_HOSTNAME}
+ PG_META_DB_PORT: ${POSTGRES_PORT}
+ PG_META_DB_NAME: ${POSTGRES_DB}
+ PG_META_DB_USER: supabase_admin
+ PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD}
+
+ functions:
+ container_name: supabase-edge-functions
+ image: supabase/edge-runtime:v1.56.0
+ restart: unless-stopped
+ networks:
+ - dokploy-network
+ depends_on:
+ analytics:
+ condition: service_healthy
+ environment:
+ JWT_SECRET: ${JWT_SECRET}
+ SUPABASE_URL: http://kong:8000
+ SUPABASE_ANON_KEY: ${ANON_KEY}
+ SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY}
+ SUPABASE_DB_URL: postgresql://postgres:${POSTGRES_PASSWORD}@${POSTGRES_HOSTNAME}:${POSTGRES_PORT}/${POSTGRES_DB}
+ # TODO: Allow configuring VERIFY_JWT per function. This PR might help: https://github.com/supabase/cli/pull/786
+ VERIFY_JWT: "${FUNCTIONS_VERIFY_JWT}"
+ volumes:
+ - ../files/volumes/functions:/home/deno/functions:Z
+ command:
+ - start
+ - --main-service
+ - /home/deno/functions/main
+
+ analytics:
+ container_name: supabase-analytics
+ image: supabase/logflare:1.4.0
+ networks:
+ - dokploy-network
+ healthcheck:
+ test: ["CMD", "curl", "http://localhost:4000/health"]
+ timeout: 5s
+ interval: 5s
+ retries: 10
+ restart: unless-stopped
+ depends_on:
+ db:
+ # Disable this if you are using an external Postgres database
+ condition: service_healthy
+ # Uncomment to use Big Query backend for analytics
+ # volumes:
+ # - type: bind
+ # source: ${PWD}/gcloud.json
+ # target: /opt/app/rel/logflare/bin/gcloud.json
+ # read_only: true
+ environment:
+ LOGFLARE_NODE_HOST: 127.0.0.1
+ DB_USERNAME: supabase_admin
+ DB_DATABASE: ${POSTGRES_DB}
+ DB_HOSTNAME: ${POSTGRES_HOSTNAME}
+ DB_PORT: ${POSTGRES_PORT}
+ DB_PASSWORD: ${POSTGRES_PASSWORD}
+ DB_SCHEMA: _analytics
+ LOGFLARE_API_KEY: ${LOGFLARE_API_KEY}
+ LOGFLARE_SINGLE_TENANT: true
+ LOGFLARE_SUPABASE_MODE: true
+ LOGFLARE_MIN_CLUSTER_SIZE: 1
+
+ # Comment variables to use Big Query backend for analytics
+ POSTGRES_BACKEND_URL: postgresql://supabase_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOSTNAME}:${POSTGRES_PORT}/${POSTGRES_DB}
+ POSTGRES_BACKEND_SCHEMA: _analytics
+ LOGFLARE_FEATURE_FLAG_OVERRIDE: multibackend=true
+ # Uncomment to use Big Query backend for analytics
+ # GOOGLE_PROJECT_ID: ${GOOGLE_PROJECT_ID}
+ # GOOGLE_PROJECT_NUMBER: ${GOOGLE_PROJECT_NUMBER}
+ #ports:
+ # - 4000:4000
+ expose:
+ - 4000
+
+ # Comment out everything below this point if you are using an external Postgres database
+ db:
+ container_name: supabase-db
+ image: supabase/postgres:15.1.1.78
+ networks:
+ - dokploy-network
+ healthcheck:
+ test: pg_isready -U postgres -h localhost
+ interval: 5s
+ timeout: 5s
+ retries: 10
+ depends_on:
+ vector:
+ condition: service_healthy
+ command:
+ - postgres
+ - -c
+ - config_file=/etc/postgresql/postgresql.conf
+ - -c
+ - log_min_messages=fatal # prevents Realtime polling queries from appearing in logs
+ restart: unless-stopped
+ #ports:
+ # # Pass down internal port because it's set dynamically by other services
+ # - ${POSTGRES_PORT}:${POSTGRES_PORT}
+ expose:
+ - ${POSTGRES_PORT}
+ environment:
+ POSTGRES_HOST: /var/run/postgresql
+ PGPORT: ${POSTGRES_PORT}
+ POSTGRES_PORT: ${POSTGRES_PORT}
+ PGPASSWORD: ${POSTGRES_PASSWORD}
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
+ PGDATABASE: ${POSTGRES_DB}
+ POSTGRES_DB: ${POSTGRES_DB}
+ JWT_SECRET: ${JWT_SECRET}
+ JWT_EXP: ${JWT_EXPIRY}
+ volumes:
+ - ../files/volumes/db/realtime.sql:/docker-entrypoint-initdb.d/migrations/99-realtime.sql:Z
+ # Must be superuser to create event trigger
+ - ../files/volumes/db/webhooks.sql:/docker-entrypoint-initdb.d/init-scripts/98-webhooks.sql:Z
+ # Must be superuser to alter reserved role
+ - ../files/volumes/db/roles.sql:/docker-entrypoint-initdb.d/init-scripts/99-roles.sql:Z
+ # Initialize the database settings with JWT_SECRET and JWT_EXP
+ - ../files/volumes/db/jwt.sql:/docker-entrypoint-initdb.d/init-scripts/99-jwt.sql:Z
+ # PGDATA directory is persisted between restarts
+ - ../files/volumes/db/data:/var/lib/postgresql/data:Z
+ # Changes required for Analytics support
+ - ../files/volumes/db/logs.sql:/docker-entrypoint-initdb.d/migrations/99-logs.sql:Z
+ # Use named volume to persist pgsodium decryption key between restarts
+ - db-config:/etc/postgresql-custom
+
+ vector:
+ container_name: supabase-vector
+ image: timberio/vector:0.28.1-alpine
+ networks:
+ - dokploy-network
+ healthcheck:
+ test:
+ [
+ "CMD",
+ "wget",
+ "--no-verbose",
+ "--tries=1",
+ "--spider",
+ "http://vector:9001/health",
+ ]
+ timeout: 5s
+ interval: 5s
+ retries: 3
+ volumes:
+ - ../files/volumes/logs/vector.yml:/etc/vector/vector.yml:ro
+ - ${DOCKER_SOCKET_LOCATION}:/var/run/docker.sock:ro
+ environment:
+ LOGFLARE_API_KEY: ${LOGFLARE_API_KEY}
+ command: ["--config", "etc/vector/vector.yml"]
+
+volumes:
+ db-config:
+
+networks:
+ dokploy-network:
+ external: true
diff --git a/packages/builders/src/templates/supabase/index.ts b/packages/builders/src/templates/supabase/index.ts
new file mode 100644
index 000000000..6922c77fb
--- /dev/null
+++ b/packages/builders/src/templates/supabase/index.ts
@@ -0,0 +1,995 @@
+import { createHmac } from "node:crypto";
+import {
+ type DomainSchema,
+ type Schema,
+ type Template,
+ generateBase64,
+ generatePassword,
+ generateRandomDomain,
+} from "../utils";
+
+interface JWTPayload {
+ role: "anon" | "service_role";
+ iss: string;
+ iat: number;
+ exp: number;
+}
+
+function base64UrlEncode(str: string): string {
+ return Buffer.from(str)
+ .toString("base64")
+ .replace(/\+/g, "-")
+ .replace(/\//g, "_")
+ .replace(/=/g, "");
+}
+
+function generateJWT(payload: JWTPayload, secret: string): string {
+ const header = { alg: "HS256", typ: "JWT" };
+
+ const encodedHeader = base64UrlEncode(JSON.stringify(header));
+ const encodedPayload = base64UrlEncode(JSON.stringify(payload));
+
+ const signature = createHmac("sha256", secret)
+ .update(`${encodedHeader}.${encodedPayload}`)
+ .digest("base64url");
+
+ return `${encodedHeader}.${encodedPayload}.${signature}`;
+}
+
+export function generateSupabaseAnonJWT(secret: string): string {
+ const now = Math.floor(Date.now() / 1000);
+ const payload: JWTPayload = {
+ role: "anon",
+ iss: "supabase",
+ iat: now,
+ exp: now + 100 * 365 * 24 * 60 * 60, // 100 years
+ };
+
+ return generateJWT(payload, secret);
+}
+
+export function generateSupabaseServiceJWT(secret: string): string {
+ const now = Math.floor(Date.now() / 1000);
+ const payload: JWTPayload = {
+ role: "service_role",
+ iss: "supabase",
+ iat: now,
+ exp: now + 100 * 365 * 24 * 60 * 60, // 100 years
+ };
+
+ return generateJWT(payload, secret);
+}
+
+export function generate(schema: Schema): Template {
+ const mainDomain = generateRandomDomain(schema);
+
+ const postgresPassword = generatePassword(32);
+ const jwtSecret = generateBase64(32);
+ const dashboardPassword = generatePassword(32);
+ const logflareApiKey = generatePassword(32);
+
+ const annonKey = generateSupabaseAnonJWT(jwtSecret);
+ const serviceRoleKey = generateSupabaseServiceJWT(jwtSecret);
+ const domains: DomainSchema[] = [
+ {
+ serviceName: "kong",
+ host: mainDomain,
+ port: 8000,
+ },
+ ];
+
+ const envs = [
+ `SUPABASE_HOST=${mainDomain}`,
+ `POSTGRES_PASSWORD=${postgresPassword}`,
+ `JWT_SECRET=${jwtSecret}`,
+ `ANON_KEY=${annonKey}`,
+ `SERVICE_ROLE_KEY=${serviceRoleKey}`,
+ "DASHBOARD_USERNAME=supabase",
+ `DASHBOARD_PASSWORD=${dashboardPassword}`,
+ "POSTGRES_HOSTNAME=db",
+ "POSTGRES_DB=postgres",
+ "POSTGRES_PORT=5432",
+ "KONG_HTTP_PORT=8000",
+ "KONG_HTTPS_PORT=8443",
+ "PGRST_DB_SCHEMAS=public,storage,graphql_public",
+ "ADDITIONAL_REDIRECT_URLS=",
+ "JWT_EXPIRY=3600",
+ "DISABLE_SIGNUP=false",
+ `MAILER_URLPATHS_CONFIRMATION=\"/auth/v1/verify\"`,
+ `MAILER_URLPATHS_INVITE=\"/auth/v1/verify\"`,
+ `MAILER_URLPATHS_RECOVERY=\"/auth/v1/verify\"`,
+ `MAILER_URLPATHS_EMAIL_CHANGE=\"/auth/v1/verify\"`,
+ "ENABLE_EMAIL_SIGNUP=true",
+ "ENABLE_EMAIL_AUTOCONFIRM=false",
+ "SMTP_ADMIN_EMAIL=admin@example.com",
+ "SMTP_HOSTNAME=supabase-mail",
+ "SMTP_PORT=2500",
+ "SMTP_USER=fake_mail_user",
+ "SMTP_PASS=fake_mail_password",
+ "SMTP_SENDER_NAME=fake_sender",
+ "ENABLE_ANONYMOUS_USERS=false",
+ "ENABLE_PHONE_SIGNUP=true",
+ "ENABLE_PHONE_AUTOCONFIRM=true",
+ "STUDIO_DEFAULT_ORGANIZATION=Default Organization",
+ "STUDIO_DEFAULT_PROJECT=Default Project",
+ "STUDIO_PORT=3000",
+ "IMGPROXY_ENABLE_WEBP_DETECTION=true",
+ "FUNCTIONS_VERIFY_JWT=false",
+ `LOGFLARE_LOGGER_BACKEND_API_KEY=${logflareApiKey}`,
+ `LOGFLARE_API_KEY=${logflareApiKey}`,
+ "DOCKER_SOCKET_LOCATION=/var/run/docker.sock",
+ "GOOGLE_PROJECT_ID=GOOGLE_PROJECT_ID",
+ "GOOGLE_PROJECT_NUMBER=GOOGLE_PROJECT_NUMBER",
+ ];
+
+ const mounts: Template["mounts"] = [
+ {
+ filePath: "/volumes/api/kong.yml",
+ content: `
+_format_version: '2.1'
+_transform: true
+
+###
+### Consumers / Users
+###
+consumers:
+ - username: DASHBOARD
+ - username: anon
+ keyauth_credentials:
+ - key: $SUPABASE_ANON_KEY
+ - username: service_role
+ keyauth_credentials:
+ - key: $SUPABASE_SERVICE_KEY
+
+###
+### Access Control List
+###
+acls:
+ - consumer: anon
+ group: anon
+ - consumer: service_role
+ group: admin
+
+###
+### Dashboard credentials
+###
+basicauth_credentials:
+ - consumer: DASHBOARD
+ username: $DASHBOARD_USERNAME
+ password: $DASHBOARD_PASSWORD
+
+###
+### API Routes
+###
+services:
+ ## Open Auth routes
+ - name: auth-v1-open
+ url: http://auth:9999/verify
+ routes:
+ - name: auth-v1-open
+ strip_path: true
+ paths:
+ - /auth/v1/verify
+ plugins:
+ - name: cors
+ - name: auth-v1-open-callback
+ url: http://auth:9999/callback
+ routes:
+ - name: auth-v1-open-callback
+ strip_path: true
+ paths:
+ - /auth/v1/callback
+ plugins:
+ - name: cors
+ - name: auth-v1-open-authorize
+ url: http://auth:9999/authorize
+ routes:
+ - name: auth-v1-open-authorize
+ strip_path: true
+ paths:
+ - /auth/v1/authorize
+ plugins:
+ - name: cors
+
+ ## Secure Auth routes
+ - name: auth-v1
+ _comment: 'GoTrue: /auth/v1/* -> http://auth:9999/*'
+ url: http://auth:9999/
+ routes:
+ - name: auth-v1-all
+ strip_path: true
+ paths:
+ - /auth/v1/
+ plugins:
+ - name: cors
+ - name: key-auth
+ config:
+ hide_credentials: false
+ - name: acl
+ config:
+ hide_groups_header: true
+ allow:
+ - admin
+ - anon
+
+ ## Secure REST routes
+ - name: rest-v1
+ _comment: 'PostgREST: /rest/v1/* -> http://rest:3000/*'
+ url: http://rest:3000/
+ routes:
+ - name: rest-v1-all
+ strip_path: true
+ paths:
+ - /rest/v1/
+ plugins:
+ - name: cors
+ - name: key-auth
+ config:
+ hide_credentials: true
+ - name: acl
+ config:
+ hide_groups_header: true
+ allow:
+ - admin
+ - anon
+
+ ## Secure GraphQL routes
+ - name: graphql-v1
+ _comment: 'PostgREST: /graphql/v1/* -> http://rest:3000/rpc/graphql'
+ url: http://rest:3000/rpc/graphql
+ routes:
+ - name: graphql-v1-all
+ strip_path: true
+ paths:
+ - /graphql/v1
+ plugins:
+ - name: cors
+ - name: key-auth
+ config:
+ hide_credentials: true
+ - name: request-transformer
+ config:
+ add:
+ headers:
+ - Content-Profile:graphql_public
+ - name: acl
+ config:
+ hide_groups_header: true
+ allow:
+ - admin
+ - anon
+
+ ## Secure Realtime routes
+ - name: realtime-v1-ws
+ _comment: 'Realtime: /realtime/v1/* -> ws://realtime:4000/socket/*'
+ url: http://realtime-dev.supabase-realtime:4000/socket
+ protocol: ws
+ routes:
+ - name: realtime-v1-ws
+ strip_path: true
+ paths:
+ - /realtime/v1/
+ plugins:
+ - name: cors
+ - name: key-auth
+ config:
+ hide_credentials: false
+ - name: acl
+ config:
+ hide_groups_header: true
+ allow:
+ - admin
+ - anon
+ - name: realtime-v1-rest
+ _comment: 'Realtime: /realtime/v1/* -> ws://realtime:4000/socket/*'
+ url: http://realtime-dev.supabase-realtime:4000/api
+ protocol: http
+ routes:
+ - name: realtime-v1-rest
+ strip_path: true
+ paths:
+ - /realtime/v1/api
+ plugins:
+ - name: cors
+ - name: key-auth
+ config:
+ hide_credentials: false
+ - name: acl
+ config:
+ hide_groups_header: true
+ allow:
+ - admin
+ - anon
+ ## Storage routes: the storage server manages its own auth
+ - name: storage-v1
+ _comment: 'Storage: /storage/v1/* -> http://storage:5000/*'
+ url: http://storage:5000/
+ routes:
+ - name: storage-v1-all
+ strip_path: true
+ paths:
+ - /storage/v1/
+ plugins:
+ - name: cors
+
+ ## Edge Functions routes
+ - name: functions-v1
+ _comment: 'Edge Functions: /functions/v1/* -> http://functions:9000/*'
+ url: http://functions:9000/
+ routes:
+ - name: functions-v1-all
+ strip_path: true
+ paths:
+ - /functions/v1/
+ plugins:
+ - name: cors
+
+ ## Analytics routes
+ - name: analytics-v1
+ _comment: 'Analytics: /analytics/v1/* -> http://logflare:4000/*'
+ url: http://analytics:4000/
+ routes:
+ - name: analytics-v1-all
+ strip_path: true
+ paths:
+ - /analytics/v1/
+
+ ## Secure Database routes
+ - name: meta
+ _comment: 'pg-meta: /pg/* -> http://pg-meta:8080/*'
+ url: http://meta:8080/
+ routes:
+ - name: meta-all
+ strip_path: true
+ paths:
+ - /pg/
+ plugins:
+ - name: key-auth
+ config:
+ hide_credentials: false
+ - name: acl
+ config:
+ hide_groups_header: true
+ allow:
+ - admin
+
+ ## Protected Dashboard - catch all remaining routes
+ - name: dashboard
+ _comment: 'Studio: /* -> http://studio:3000/*'
+ url: http://studio:3000/
+ routes:
+ - name: dashboard-all
+ strip_path: true
+ paths:
+ - /
+ plugins:
+ - name: cors
+ - name: basic-auth
+ config:
+ hide_credentials: true
+ `,
+ },
+ {
+ filePath: "/volumes/db/init/data.sql",
+ content: `
+ `,
+ },
+ {
+ filePath: "/volumes/db/jwt.sql",
+ content: `
+\\set jwt_secret \`echo "$JWT_SECRET"\`
+\\set jwt_exp \`echo "$JWT_EXP"\`
+
+ALTER DATABASE postgres SET "app.settings.jwt_secret" TO :'jwt_secret';
+ALTER DATABASE postgres SET "app.settings.jwt_exp" TO :'jwt_exp';
+ `,
+ },
+ {
+ filePath: "/volumes/db/logs.sql",
+ content: `
+\\set pguser \`echo "$POSTGRES_USER"\`
+
+create schema if not exists _analytics;
+alter schema _analytics owner to :pguser;
+ `,
+ },
+ {
+ filePath: "/volumes/db/realtime.sql",
+ content: `
+\\set pguser \`echo "$POSTGRES_USER"\`
+
+create schema if not exists _realtime;
+alter schema _realtime owner to :pguser;
+ `,
+ },
+ {
+ filePath: "/volumes/db/roles.sql",
+ content: `
+-- NOTE: change to your own passwords for production environments
+\\set pgpass \`echo "$POSTGRES_PASSWORD"\`
+
+ALTER USER authenticator WITH PASSWORD :'pgpass';
+ALTER USER pgbouncer WITH PASSWORD :'pgpass';
+ALTER USER supabase_auth_admin WITH PASSWORD :'pgpass';
+ALTER USER supabase_functions_admin WITH PASSWORD :'pgpass';
+ALTER USER supabase_storage_admin WITH PASSWORD :'pgpass';
+ `,
+ },
+ {
+ filePath: "/volumes/db/webhooks.sql",
+ content: `
+BEGIN;
+ -- Create pg_net extension
+ CREATE EXTENSION IF NOT EXISTS pg_net SCHEMA extensions;
+ -- Create supabase_functions schema
+ CREATE SCHEMA supabase_functions AUTHORIZATION supabase_admin;
+ GRANT USAGE ON SCHEMA supabase_functions TO postgres, anon, authenticated, service_role;
+ ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON TABLES TO postgres, anon, authenticated, service_role;
+ ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON FUNCTIONS TO postgres, anon, authenticated, service_role;
+ ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON SEQUENCES TO postgres, anon, authenticated, service_role;
+ -- supabase_functions.migrations definition
+ CREATE TABLE supabase_functions.migrations (
+ version text PRIMARY KEY,
+ inserted_at timestamptz NOT NULL DEFAULT NOW()
+ );
+ -- Initial supabase_functions migration
+ INSERT INTO supabase_functions.migrations (version) VALUES ('initial');
+ -- supabase_functions.hooks definition
+ CREATE TABLE supabase_functions.hooks (
+ id bigserial PRIMARY KEY,
+ hook_table_id integer NOT NULL,
+ hook_name text NOT NULL,
+ created_at timestamptz NOT NULL DEFAULT NOW(),
+ request_id bigint
+ );
+ CREATE INDEX supabase_functions_hooks_request_id_idx ON supabase_functions.hooks USING btree (request_id);
+ CREATE INDEX supabase_functions_hooks_h_table_id_h_name_idx ON supabase_functions.hooks USING btree (hook_table_id, hook_name);
+ COMMENT ON TABLE supabase_functions.hooks IS 'Supabase Functions Hooks: Audit trail for triggered hooks.';
+ CREATE FUNCTION supabase_functions.http_request()
+ RETURNS trigger
+ LANGUAGE plpgsql
+ AS $function$
+ DECLARE
+ request_id bigint;
+ payload jsonb;
+ url text := TG_ARGV[0]::text;
+ method text := TG_ARGV[1]::text;
+ headers jsonb DEFAULT '{}'::jsonb;
+ params jsonb DEFAULT '{}'::jsonb;
+ timeout_ms integer DEFAULT 1000;
+ BEGIN
+ IF url IS NULL OR url = 'null' THEN
+ RAISE EXCEPTION 'url argument is missing';
+ END IF;
+
+ IF method IS NULL OR method = 'null' THEN
+ RAISE EXCEPTION 'method argument is missing';
+ END IF;
+
+ IF TG_ARGV[2] IS NULL OR TG_ARGV[2] = 'null' THEN
+ headers = '{"Content-Type": "application/json"}'::jsonb;
+ ELSE
+ headers = TG_ARGV[2]::jsonb;
+ END IF;
+
+ IF TG_ARGV[3] IS NULL OR TG_ARGV[3] = 'null' THEN
+ params = '{}'::jsonb;
+ ELSE
+ params = TG_ARGV[3]::jsonb;
+ END IF;
+
+ IF TG_ARGV[4] IS NULL OR TG_ARGV[4] = 'null' THEN
+ timeout_ms = 1000;
+ ELSE
+ timeout_ms = TG_ARGV[4]::integer;
+ END IF;
+
+ CASE
+ WHEN method = 'GET' THEN
+ SELECT http_get INTO request_id FROM net.http_get(
+ url,
+ params,
+ headers,
+ timeout_ms
+ );
+ WHEN method = 'POST' THEN
+ payload = jsonb_build_object(
+ 'old_record', OLD,
+ 'record', NEW,
+ 'type', TG_OP,
+ 'table', TG_TABLE_NAME,
+ 'schema', TG_TABLE_SCHEMA
+ );
+
+ SELECT http_post INTO request_id FROM net.http_post(
+ url,
+ payload,
+ params,
+ headers,
+ timeout_ms
+ );
+ ELSE
+ RAISE EXCEPTION 'method argument % is invalid', method;
+ END CASE;
+
+ INSERT INTO supabase_functions.hooks
+ (hook_table_id, hook_name, request_id)
+ VALUES
+ (TG_RELID, TG_NAME, request_id);
+
+ RETURN NEW;
+ END
+ $function$;
+ -- Supabase super admin
+ DO
+ $$
+ BEGIN
+ IF NOT EXISTS (
+ SELECT 1
+ FROM pg_roles
+ WHERE rolname = 'supabase_functions_admin'
+ )
+ THEN
+ CREATE USER supabase_functions_admin NOINHERIT CREATEROLE LOGIN NOREPLICATION;
+ END IF;
+ END
+ $$;
+ GRANT ALL PRIVILEGES ON SCHEMA supabase_functions TO supabase_functions_admin;
+ GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA supabase_functions TO supabase_functions_admin;
+ GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA supabase_functions TO supabase_functions_admin;
+ ALTER USER supabase_functions_admin SET search_path = "supabase_functions";
+ ALTER table "supabase_functions".migrations OWNER TO supabase_functions_admin;
+ ALTER table "supabase_functions".hooks OWNER TO supabase_functions_admin;
+ ALTER function "supabase_functions".http_request() OWNER TO supabase_functions_admin;
+ GRANT supabase_functions_admin TO postgres;
+ -- Remove unused supabase_pg_net_admin role
+ DO
+ $$
+ BEGIN
+ IF EXISTS (
+ SELECT 1
+ FROM pg_roles
+ WHERE rolname = 'supabase_pg_net_admin'
+ )
+ THEN
+ REASSIGN OWNED BY supabase_pg_net_admin TO supabase_admin;
+ DROP OWNED BY supabase_pg_net_admin;
+ DROP ROLE supabase_pg_net_admin;
+ END IF;
+ END
+ $$;
+ -- pg_net grants when extension is already enabled
+ DO
+ $$
+ BEGIN
+ IF EXISTS (
+ SELECT 1
+ FROM pg_extension
+ WHERE extname = 'pg_net'
+ )
+ THEN
+ GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role;
+ ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
+ ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
+ ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
+ ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
+ REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
+ REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
+ GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
+ GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
+ END IF;
+ END
+ $$;
+ -- Event trigger for pg_net
+ CREATE OR REPLACE FUNCTION extensions.grant_pg_net_access()
+ RETURNS event_trigger
+ LANGUAGE plpgsql
+ AS $$
+ BEGIN
+ IF EXISTS (
+ SELECT 1
+ FROM pg_event_trigger_ddl_commands() AS ev
+ JOIN pg_extension AS ext
+ ON ev.objid = ext.oid
+ WHERE ext.extname = 'pg_net'
+ )
+ THEN
+ GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role;
+ ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
+ ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
+ ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
+ ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
+ REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
+ REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
+ GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
+ GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
+ END IF;
+ END;
+ $$;
+ COMMENT ON FUNCTION extensions.grant_pg_net_access IS 'Grants access to pg_net';
+ DO
+ $$
+ BEGIN
+ IF NOT EXISTS (
+ SELECT 1
+ FROM pg_event_trigger
+ WHERE evtname = 'issue_pg_net_access'
+ ) THEN
+ CREATE EVENT TRIGGER issue_pg_net_access ON ddl_command_end WHEN TAG IN ('CREATE EXTENSION')
+ EXECUTE PROCEDURE extensions.grant_pg_net_access();
+ END IF;
+ END
+ $$;
+ INSERT INTO supabase_functions.migrations (version) VALUES ('20210809183423_update_grants');
+ ALTER function supabase_functions.http_request() SECURITY DEFINER;
+ ALTER function supabase_functions.http_request() SET search_path = supabase_functions;
+ REVOKE ALL ON FUNCTION supabase_functions.http_request() FROM PUBLIC;
+ GRANT EXECUTE ON FUNCTION supabase_functions.http_request() TO postgres, anon, authenticated, service_role;
+COMMIT;
+ `,
+ },
+ {
+ filePath: "/volumes/functions/hello/index.ts",
+ content: `
+// Follow this setup guide to integrate the Deno language server with your editor:
+// https://deno.land/manual/getting_started/setup_your_environment
+// This enables autocomplete, go to definition, etc.
+
+import { serve } from "https://deno.land/std@0.177.1/http/server.ts"
+
+serve(async () => {
+ return new Response(
+ \`"Hello from Edge Functions!"\`,
+ { headers: { "Content-Type": "application/json" } },
+ )
+})
+
+// To invoke:
+// curl 'http://localhost:/functions/v1/hello' \\
+// --header 'Authorization: Bearer '
+ `,
+ },
+ {
+ filePath: "/volumes/functions/main/index.ts",
+ content: `
+import { serve } from 'https://deno.land/std@0.131.0/http/server.ts'
+import * as jose from 'https://deno.land/x/jose@v4.14.4/index.ts'
+
+console.log('main function started')
+
+const JWT_SECRET = Deno.env.get('JWT_SECRET')
+const VERIFY_JWT = Deno.env.get('VERIFY_JWT') === 'true'
+
+function getAuthToken(req: Request) {
+ const authHeader = req.headers.get('authorization')
+ if (!authHeader) {
+ throw new Error('Missing authorization header')
+ }
+ const [bearer, token] = authHeader.split(' ')
+ if (bearer !== 'Bearer') {
+ throw new Error(\`Auth header is not 'Bearer {token}'\`)
+ }
+ return token
+}
+
+async function verifyJWT(jwt: string): Promise {
+ const encoder = new TextEncoder()
+ const secretKey = encoder.encode(JWT_SECRET)
+ try {
+ await jose.jwtVerify(jwt, secretKey)
+ } catch (err) {
+ console.error(err)
+ return false
+ }
+ return true
+}
+
+serve(async (req: Request) => {
+ if (req.method !== 'OPTIONS' && VERIFY_JWT) {
+ try {
+ const token = getAuthToken(req)
+ const isValidJWT = await verifyJWT(token)
+
+ if (!isValidJWT) {
+ return new Response(JSON.stringify({ msg: 'Invalid JWT' }), {
+ status: 401,
+ headers: { 'Content-Type': 'application/json' },
+ })
+ }
+ } catch (e) {
+ console.error(e)
+ return new Response(JSON.stringify({ msg: e.toString() }), {
+ status: 401,
+ headers: { 'Content-Type': 'application/json' },
+ })
+ }
+ }
+
+ const url = new URL(req.url)
+ const { pathname } = url
+ const path_parts = pathname.split('/')
+ const service_name = path_parts[1]
+
+ if (!service_name || service_name === '') {
+ const error = { msg: 'missing function name in request' }
+ return new Response(JSON.stringify(error), {
+ status: 400,
+ headers: { 'Content-Type': 'application/json' },
+ })
+ }
+
+ const servicePath = \`/home/deno/functions/\${service_name}\`
+ console.error(\`serving the request with \${servicePath}\`)
+
+ const memoryLimitMb = 150
+ const workerTimeoutMs = 1 * 60 * 1000
+ const noModuleCache = false
+ const importMapPath = null
+ const envVarsObj = Deno.env.toObject()
+ const envVars = Object.keys(envVarsObj).map((k) => [k, envVarsObj[k]])
+
+ try {
+ const worker = await EdgeRuntime.userWorkers.create({
+ servicePath,
+ memoryLimitMb,
+ workerTimeoutMs,
+ noModuleCache,
+ importMapPath,
+ envVars,
+ })
+ return await worker.fetch(req)
+ } catch (e) {
+ const error = { msg: e.toString() }
+ return new Response(JSON.stringify(error), {
+ status: 500,
+ headers: { 'Content-Type': 'application/json' },
+ })
+ }
+})
+ `,
+ },
+ {
+ filePath: "/volumes/logs/vector.yml",
+ content: `
+api:
+ enabled: true
+ address: 0.0.0.0:9001
+
+sources:
+ docker_host:
+ type: docker_logs
+ exclude_containers:
+ - supabase-vector
+
+transforms:
+ project_logs:
+ type: remap
+ inputs:
+ - docker_host
+ source: |-
+ .project = "default"
+ .event_message = del(.message)
+ .appname = del(.container_name)
+ del(.container_created_at)
+ del(.container_id)
+ del(.source_type)
+ del(.stream)
+ del(.label)
+ del(.image)
+ del(.host)
+ del(.stream)
+ router:
+ type: route
+ inputs:
+ - project_logs
+ route:
+ kong: '.appname == "supabase-kong"'
+ auth: '.appname == "supabase-auth"'
+ rest: '.appname == "supabase-rest"'
+ realtime: '.appname == "supabase-realtime"'
+ storage: '.appname == "supabase-storage"'
+ functions: '.appname == "supabase-functions"'
+ db: '.appname == "supabase-db"'
+ # Ignores non nginx errors since they are related with kong booting up
+ kong_logs:
+ type: remap
+ inputs:
+ - router.kong
+ source: |-
+ req, err = parse_nginx_log(.event_message, "combined")
+ if err == null {
+ .timestamp = req.timestamp
+ .metadata.request.headers.referer = req.referer
+ .metadata.request.headers.user_agent = req.agent
+ .metadata.request.headers.cf_connecting_ip = req.client
+ .metadata.request.method = req.method
+ .metadata.request.path = req.path
+ .metadata.request.protocol = req.protocol
+ .metadata.response.status_code = req.status
+ }
+ if err != null {
+ abort
+ }
+ # Ignores non nginx errors since they are related with kong booting up
+ kong_err:
+ type: remap
+ inputs:
+ - router.kong
+ source: |-
+ .metadata.request.method = "GET"
+ .metadata.response.status_code = 200
+ parsed, err = parse_nginx_log(.event_message, "error")
+ if err == null {
+ .timestamp = parsed.timestamp
+ .severity = parsed.severity
+ .metadata.request.host = parsed.host
+ .metadata.request.headers.cf_connecting_ip = parsed.client
+ url, err = split(parsed.request, " ")
+ if err == null {
+ .metadata.request.method = url[0]
+ .metadata.request.path = url[1]
+ .metadata.request.protocol = url[2]
+ }
+ }
+ if err != null {
+ abort
+ }
+ # Gotrue logs are structured json strings which frontend parses directly. But we keep metadata for consistency.
+ auth_logs:
+ type: remap
+ inputs:
+ - router.auth
+ source: |-
+ parsed, err = parse_json(.event_message)
+ if err == null {
+ .metadata.timestamp = parsed.time
+ .metadata = merge!(.metadata, parsed)
+ }
+ # PostgREST logs are structured so we separate timestamp from message using regex
+ rest_logs:
+ type: remap
+ inputs:
+ - router.rest
+ source: |-
+ parsed, err = parse_regex(.event_message, r'^(?P