Canvas: Generate graph state out of dodo-app config

Restructure code, create shared config lib.

Change-Id: I2cf06d35c486d4557484daf8618a2c215316fa7e
diff --git a/Jenkinsfile b/Jenkinsfile
index ff51d94..b619ff5 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -21,6 +21,11 @@
                 container('node') {
                     sh "apk update"
                     sh "apk add gcc g++ make musl-dev python3 py3-setuptools"
+                    dir('apps/canvas/config') {
+                        sh 'npm install'
+                        sh 'npm run format-check'
+                        sh 'npm run lint'
+                    }
                     dir('apps/canvas/back') {
                         sh 'npm install'
                         sh 'npm run format-check'
diff --git a/apps/canvas/back/.env b/apps/canvas/back/.env
index 6237047..a101621 100644
--- a/apps/canvas/back/.env
+++ b/apps/canvas/back/.env
@@ -1,3 +1,3 @@
-DATABASE_URL=file:${DODO_VOLUME_DATA}/dodo.db
-PUBLIC_ADDR=https://canvas.v1.dodo.cloud
-INTERNAL_API_ADDR=http://canvas-app.hgrz-dodo-app-gry.svc.cluster.local:8081
+DATABASE_URL=file:/home/gio/dodo.db
+PUBLIC_ADDR=https://canvas.p.v1.dodo.cloud
+INTERNAL_API_ADDR=http://canvas.hgrz-dodo-app-jjy.svc.cluster.local:8081
diff --git a/apps/canvas/back/package-lock.json b/apps/canvas/back/package-lock.json
index cd69cf4..03f0e80 100644
--- a/apps/canvas/back/package-lock.json
+++ b/apps/canvas/back/package-lock.json
@@ -12,6 +12,7 @@
         "@loancrate/prisma-schema-parser": "^3.0.0",
         "@prisma/client": "^6.6.0",
         "axios": "^1.8.4",
+        "config": "file:../config",
         "dotenv": "^16.5.0",
         "dotenv-expand": "^12.0.2",
         "express": "^4.21.1",
@@ -40,6 +41,21 @@
         "typescript-eslint": "^8.11.0"
       }
     },
+    "../config": {
+      "version": "1.0.0",
+      "license": "ISC",
+      "dependencies": {
+        "@xyflow/react": "^12.3.3",
+        "uuid": "^11.0.2",
+        "zod": "^3.24.4"
+      },
+      "devDependencies": {
+        "eslint": "^9.13.0",
+        "prettier": "3.5.3",
+        "typescript": "^5.8.3",
+        "typescript-eslint": "^8.11.0"
+      }
+    },
     "node_modules/@ampproject/remapping": {
       "version": "2.3.0",
       "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
@@ -4279,6 +4295,10 @@
       "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
       "devOptional": true
     },
+    "node_modules/config": {
+      "resolved": "../config",
+      "link": true
+    },
     "node_modules/console-control-strings": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
diff --git a/apps/canvas/back/package.json b/apps/canvas/back/package.json
index 5f69e57..b32b801 100644
--- a/apps/canvas/back/package.json
+++ b/apps/canvas/back/package.json
@@ -3,7 +3,7 @@
   "version": "1.0.0",
   "description": "",
   "main": "index.js",
-  "type": "commonjs",
+  "type": "module",
   "scripts": {
     "build": "tsc",
     "test": "jest",
@@ -18,6 +18,7 @@
     "@loancrate/prisma-schema-parser": "^3.0.0",
     "@prisma/client": "^6.6.0",
     "axios": "^1.8.4",
+    "config": "file:../config",
     "dotenv": "^16.5.0",
     "dotenv-expand": "^12.0.2",
     "express": "^4.21.1",
diff --git a/apps/canvas/back/src/app_manager.ts b/apps/canvas/back/src/app_manager.ts
index 803192c..faf64e2 100644
--- a/apps/canvas/back/src/app_manager.ts
+++ b/apps/canvas/back/src/app_manager.ts
@@ -69,7 +69,6 @@
 		if (response.status !== 200) {
 			throw new Error(`Failed to deploy application: ${response.statusText}`);
 		}
-		console.log(response.data);
 		const result = DeployResponseSchema.safeParse(response.data);
 		if (!result.success) {
 			throw new Error(`Invalid deploy response format: ${result.error.message}`);
diff --git a/apps/canvas/back/src/index.ts b/apps/canvas/back/src/index.ts
index a2e8b31..0ba25b0 100644
--- a/apps/canvas/back/src/index.ts
+++ b/apps/canvas/back/src/index.ts
@@ -3,15 +3,16 @@
 import fs from "node:fs";
 import { env } from "node:process";
 import axios from "axios";
-import { GithubClient } from "./github";
-import { AppManager } from "./app_manager";
+import { GithubClient } from "./github.js";
+import { AppManager } from "./app_manager.js";
 import { z } from "zod";
-import { ProjectMonitor, WorkerSchema } from "./project_monitor";
+import { ProjectMonitor, WorkerSchema } from "./project_monitor.js";
 import tmp from "tmp";
-import { NodeJSAnalyzer } from "./lib/nodejs";
+import { NodeJSAnalyzer } from "./lib/nodejs.js";
 import shell from "shelljs";
-import { RealFileSystem } from "./lib/fs";
+import { RealFileSystem } from "./lib/fs.js";
 import path from "node:path";
+import { Env, generateDodoConfig, ConfigSchema, AppNode, ConfigWithInput, configToGraph, Network } from "config";
 
 async function generateKey(root: string): Promise<[string, string]> {
 	const privKeyPath = path.join(root, "key");
@@ -120,31 +121,44 @@
 			}
 			resp.status(200);
 			resp.header("content-type", "application/json");
+			let currentState: Record<string, unknown> | null = null;
 			if (state === "deploy") {
 				if (r.state == null) {
-					resp.send({
+					currentState = {
 						nodes: [],
 						edges: [],
 						viewport: { x: 0, y: 0, zoom: 1 },
-					});
+					};
 				} else {
-					resp.send(JSON.parse(Buffer.from(r.state).toString("utf8")));
+					currentState = JSON.parse(Buffer.from(r.state).toString("utf8"));
 				}
 			} else {
 				if (r.draft == null) {
 					if (r.state == null) {
-						resp.send({
+						currentState = {
 							nodes: [],
 							edges: [],
 							viewport: { x: 0, y: 0, zoom: 1 },
-						});
+						};
 					} else {
-						resp.send(JSON.parse(Buffer.from(r.state).toString("utf8")));
+						currentState = JSON.parse(Buffer.from(r.state).toString("utf8"));
 					}
 				} else {
-					resp.send(JSON.parse(Buffer.from(r.draft).toString("utf8")));
+					currentState = JSON.parse(Buffer.from(r.draft).toString("utf8"));
 				}
 			}
+			const env = await getEnv(Number(req.params["projectId"]), resp.locals.userId, resp.locals.username);
+			if (currentState) {
+				const config = generateDodoConfig(
+					req.params["projectId"].toString(),
+					currentState.nodes as AppNode[],
+					env,
+				);
+				resp.send({
+					state: currentState,
+					config,
+				});
+			}
 		} catch (e) {
 			console.log(e);
 			resp.status(500);
@@ -257,7 +271,6 @@
 	deployKey: string,
 	publicAddr?: string,
 ): Promise<void> {
-	console.log(publicAddr);
 	for (const repoUrl of diff.toDelete ?? []) {
 		try {
 			await github.removeDeployKey(repoUrl, deployKey);
@@ -289,11 +302,10 @@
 const handleDeploy: express.Handler = async (req, resp) => {
 	try {
 		const projectId = Number(req.params["projectId"]);
-		const state = JSON.stringify(req.body.state);
 		const p = await db.project.findUnique({
 			where: {
 				id: projectId,
-				userId: resp.locals.userId,
+				// userId: resp.locals.userId, TODO(gio): validate
 			},
 			select: {
 				instanceId: true,
@@ -307,6 +319,21 @@
 			resp.status(404);
 			return;
 		}
+		const config = ConfigSchema.safeParse(req.body.config);
+		if (!config.success) {
+			resp.status(400);
+			resp.write(JSON.stringify({ error: "Invalid configuration", issues: config.error.format() }));
+			return;
+		}
+		const state = req.body.state
+			? JSON.stringify(req.body.state)
+			: JSON.stringify(
+					configToGraph(
+						config.data,
+						getNetworks(resp.locals.username),
+						p.state ? JSON.parse(Buffer.from(p.state).toString("utf8")) : null,
+					),
+				);
 		await db.project.update({
 			where: {
 				id: projectId,
@@ -325,14 +352,20 @@
 			});
 		}
 		let diff: RepoDiff | null = null;
-		const config = req.body.config;
-		config.input.key = {
-			public: deployKeyPublic,
-			private: deployKey,
+		const cfg: ConfigWithInput = {
+			...config.data,
+			input: {
+				appId: projectId.toString(),
+				managerAddr: env.INTERNAL_API_ADDR!,
+				key: {
+					public: deployKeyPublic!,
+					private: deployKey!,
+				},
+			},
 		};
 		try {
 			if (p.instanceId == null) {
-				const deployResponse = await appManager.deploy(config);
+				const deployResponse = await appManager.deploy(cfg);
 				await db.project.update({
 					where: {
 						id: projectId,
@@ -346,7 +379,7 @@
 				});
 				diff = { toAdd: extractGithubRepos(state) };
 			} else {
-				const deployResponse = await appManager.update(p.instanceId, config);
+				const deployResponse = await appManager.update(p.instanceId, cfg);
 				diff = calculateRepoDiff(extractGithubRepos(p.state), extractGithubRepos(state));
 				await db.project.update({
 					where: {
@@ -413,6 +446,41 @@
 	}
 };
 
+const handleConfigGet: express.Handler = async (req, resp) => {
+	try {
+		const projectId = Number(req.params["projectId"]);
+		const project = await db.project.findUnique({
+			where: {
+				id: projectId,
+			},
+			select: {
+				state: true,
+			},
+		});
+
+		if (!project || !project.state) {
+			resp.status(404).send({ error: "No deployed configuration found." });
+			return;
+		}
+
+		const state = JSON.parse(Buffer.from(project.state).toString("utf8"));
+		const env = await getEnv(projectId, resp.locals.userId, resp.locals.username);
+		const config = generateDodoConfig(projectId.toString(), state.nodes, env);
+
+		if (!config) {
+			resp.status(500).send({ error: "Failed to generate configuration." });
+			return;
+		}
+		resp.status(200).json(config);
+	} catch (e) {
+		console.log(e);
+		resp.status(500).send({ error: "Internal server error" });
+	} finally {
+		console.log("config get done");
+		resp.end();
+	}
+};
+
 const handleRemoveDeployment: express.Handler = async (req, resp) => {
 	try {
 		const projectId = Number(req.params["projectId"]);
@@ -529,80 +597,84 @@
 	}
 };
 
+const getNetworks = (username?: string | undefined): Network[] => {
+	return [
+		{
+			name: "Trial",
+			domain: "trial.dodoapp.xyz",
+			hasAuth: false,
+		},
+		// TODO(gio): Remove
+	].concat(
+		username === "gio" || 1 == 1
+			? [
+					{
+						name: "Public",
+						domain: "v1.dodo.cloud",
+						hasAuth: true,
+					},
+					{
+						name: "Private",
+						domain: "p.v1.dodo.cloud",
+						hasAuth: true,
+					},
+				]
+			: [],
+	);
+};
+
+const getEnv = async (projectId: number, userId: string, username: string): Promise<Env> => {
+	const project = await db.project.findUnique({
+		where: {
+			id: projectId,
+			userId,
+		},
+		select: {
+			deployKeyPublic: true,
+			githubToken: true,
+			access: true,
+			instanceId: true,
+		},
+	});
+	if (!project) {
+		throw new Error("Project not found");
+	}
+	const monitor = projectMonitors.get(projectId);
+	const serviceNames = monitor ? monitor.getAllServiceNames() : [];
+	const services = serviceNames.map((name: string) => ({
+		name,
+		workers: [...(monitor ? monitor.getWorkerStatusesForService(name) : new Map()).entries()].map(
+			([id, status]) => ({
+				...status,
+				id,
+			}),
+		),
+	}));
+	return {
+		managerAddr: env.INTERNAL_API_ADDR,
+		deployKeyPublic: project.deployKeyPublic == null ? undefined : project.deployKeyPublic,
+		instanceId: project.instanceId == null ? undefined : project.instanceId,
+		access: JSON.parse(project.access ?? "[]"),
+		integrations: {
+			github: !!project.githubToken,
+		},
+		networks: getNetworks(username),
+		services,
+		user: {
+			id: userId,
+			username: username,
+		},
+	};
+};
+
 const handleEnv: express.Handler = async (req, resp) => {
 	const projectId = Number(req.params["projectId"]);
 	try {
-		const project = await db.project.findUnique({
-			where: {
-				id: projectId,
-				userId: resp.locals.userId,
-			},
-			select: {
-				deployKeyPublic: true,
-				githubToken: true,
-				access: true,
-				instanceId: true,
-			},
-		});
-		if (!project) {
-			resp.status(404);
-			resp.write(JSON.stringify({ error: "Project not found" }));
-			return;
-		}
-		const monitor = projectMonitors.get(projectId);
-		const serviceNames = monitor ? monitor.getAllServiceNames() : [];
-		const services = serviceNames.map((name) => ({
-			name,
-			workers: [...(monitor ? monitor.getWorkerStatusesForService(name) : new Map()).entries()].map(
-				([id, status]) => ({
-					...status,
-					id,
-				}),
-			),
-		}));
-
+		const env = await getEnv(projectId, resp.locals.userId, resp.locals.username);
 		resp.status(200);
-		resp.write(
-			JSON.stringify({
-				managerAddr: env.INTERNAL_API_ADDR,
-				deployKeyPublic: project.deployKeyPublic == null ? undefined : project.deployKeyPublic,
-				instanceId: project.instanceId == null ? undefined : project.instanceId,
-				access: JSON.parse(project.access ?? "[]"),
-				integrations: {
-					github: !!project.githubToken,
-				},
-				networks: [
-					{
-						name: "Trial",
-						domain: "trial.dodoapp.xyz",
-						hasAuth: false,
-					},
-					// TODO(gio): Remove
-				].concat(
-					resp.locals.username !== "gio"
-						? []
-						: [
-								{
-									name: "Public",
-									domain: "v1.dodo.cloud",
-									hasAuth: true,
-								},
-								{
-									name: "Private",
-									domain: "p.v1.dodo.cloud",
-									hasAuth: true,
-								},
-							],
-				),
-				services,
-				user: {
-					id: resp.locals.userId,
-					username: resp.locals.username,
-				},
-			}),
-		);
+		resp.write(JSON.stringify(env));
 	} catch (error) {
-		console.error("Error checking integrations:", error);
+		console.error("Error getting env:", error);
 		resp.status(500);
 		resp.write(JSON.stringify({ error: "Internal server error" }));
 	} finally {
@@ -692,12 +764,17 @@
 		return true;
 	}
 	const results = await Promise.all(
-		projectWorkers.map(async (workerAddress) => {
-			const resp = await axios.post(`${workerAddress}/update`);
-			return resp.status === 200;
+		projectWorkers.map(async (workerAddress: string) => {
+			try {
+				const { data } = await axios.get(`http://${workerAddress}/reload`);
+				return data.every((s: { status: string }) => s.status === "ok");
+			} catch (error) {
+				console.error(`Failed to reload worker ${workerAddress}:`, error);
+				return false;
+			}
 		}),
 	);
-	return results.reduce((acc, curr) => acc && curr, true);
+	return results.reduce((acc: boolean, curr: boolean) => acc && curr, true);
 }
 
 const handleReload: express.Handler = async (req, resp) => {
@@ -886,9 +963,30 @@
 	}
 };
 
+const handleValidateConfig: express.Handler = async (req, resp) => {
+	try {
+		const validationResult = ConfigSchema.safeParse(req.body);
+		if (!validationResult.success) {
+			resp.status(400);
+			resp.header("Content-Type", "application/json");
+			resp.write(JSON.stringify({ success: false, errors: validationResult.error.flatten() }));
+		} else {
+			resp.status(200);
+			resp.header("Content-Type", "application/json");
+			resp.write(JSON.stringify({ success: true }));
+		}
+	} catch (e) {
+		console.log(e);
+		resp.status(500);
+	} finally {
+		resp.end();
+	}
+};
+
 async function start() {
 	await db.$connect();
 	const app = express();
+	app.set("json spaces", 2);
 	app.use(express.json()); // Global JSON parsing
 
 	// Public webhook route - no auth needed
@@ -903,6 +1001,7 @@
 	projectRouter.get("/:projectId/saved/draft", handleSavedGet("draft"));
 	projectRouter.post("/:projectId/deploy", handleDeploy);
 	projectRouter.get("/:projectId/status", handleStatus);
+	projectRouter.get("/:projectId/config", handleConfigGet);
 	projectRouter.delete("/:projectId", handleProjectDelete);
 	projectRouter.get("/:projectId/repos/github", handleGithubRepos);
 	projectRouter.post("/:projectId/github-token", handleUpdateGithubToken);
@@ -921,6 +1020,9 @@
 	const internalApi = express();
 	internalApi.use(express.json());
 	internalApi.post("/api/project/:projectId/workers", handleRegisterWorker);
+	internalApi.get("/api/project/:projectId/config", handleConfigGet);
+	internalApi.post("/api/project/:projectId/deploy", handleDeploy);
+	internalApi.post("/api/validate-config", handleValidateConfig);
 
 	app.listen(env.DODO_PORT_WEB, () => {
 		console.log("Web server started on port", env.DODO_PORT_WEB);
diff --git a/apps/canvas/back/src/lib/analyze.ts b/apps/canvas/back/src/lib/analyze.ts
index e1fa699..28eecbf 100644
--- a/apps/canvas/back/src/lib/analyze.ts
+++ b/apps/canvas/back/src/lib/analyze.ts
@@ -1,4 +1,4 @@
-import { FileSystem } from "./fs";
+import { FileSystem } from "./fs.js";
 
 export interface ServiceAnalyzer {
 	detect: (fs: FileSystem, root: string) => boolean;
diff --git a/apps/canvas/back/src/lib/nodejs.test.ts b/apps/canvas/back/src/lib/nodejs.test.ts
index 7d406b1..5de8423 100644
--- a/apps/canvas/back/src/lib/nodejs.test.ts
+++ b/apps/canvas/back/src/lib/nodejs.test.ts
@@ -1,8 +1,8 @@
-import { NodeJSAnalyzer } from "./nodejs";
-import { FileSystem, RealFileSystem } from "./fs";
+import { NodeJSAnalyzer } from "./nodejs.js";
+import { FileSystem, RealFileSystem } from "./fs.js";
 import { Volume, IFs, createFsFromVolume } from "memfs";
 import { test, expect } from "@jest/globals";
-import { expandValue } from "./env";
+import { expandValue } from "./env.js";
 import shell from "shelljs";
 
 class InMemoryFileSystem implements FileSystem {
diff --git a/apps/canvas/back/src/lib/nodejs.ts b/apps/canvas/back/src/lib/nodejs.ts
index 07e6c1f..3369b1d 100644
--- a/apps/canvas/back/src/lib/nodejs.ts
+++ b/apps/canvas/back/src/lib/nodejs.ts
@@ -1,10 +1,10 @@
 import path from "path";
-import { FileSystem } from "./fs";
-import { ServiceAnalyzer, ConfigVar, ConfigVarCategory, ConfigVarSemanticType } from "./analyze";
+import { FileSystem } from "./fs.js";
+import { ServiceAnalyzer, ConfigVar, ConfigVarCategory, ConfigVarSemanticType } from "./analyze.js";
 import { parse as parseDotenv } from "dotenv";
 import { parsePrismaSchema } from "@loancrate/prisma-schema-parser";
-import { augmentConfigVar } from "./semantics";
-import { expandValue } from "./env";
+import { augmentConfigVar } from "./semantics.js";
+import { expandValue } from "./env.js";
 import { z } from "zod";
 
 const packageJsonFileName = "package.json";
diff --git a/apps/canvas/back/src/lib/semantics.ts b/apps/canvas/back/src/lib/semantics.ts
index 2cbd1ab..f365f83 100644
--- a/apps/canvas/back/src/lib/semantics.ts
+++ b/apps/canvas/back/src/lib/semantics.ts
@@ -1,4 +1,4 @@
-import { ConfigVar, ConfigVarSemanticType } from "./analyze";
+import { ConfigVar, ConfigVarSemanticType } from "./analyze.js";
 
 export function augmentConfigVar(cv: ConfigVar) {
 	if (cv.semanticType != null) {
diff --git a/apps/canvas/back/tsconfig.json b/apps/canvas/back/tsconfig.json
index c0ce9b7..b92e830 100644
--- a/apps/canvas/back/tsconfig.json
+++ b/apps/canvas/back/tsconfig.json
@@ -13,7 +13,7 @@
 		// "disableSolutionSearching": true,                 /* Opt a project out of multi-project reference checking when editing. */
 		// "disableReferencedProjectLoad": true,             /* Reduce the number of projects loaded automatically by TypeScript. */
 		/* Language and Environment */
-		"target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
+		"target": "es2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
 		// "lib": [],                                        /* Specify a set of bundled library declaration files that describe the target runtime environment. */
 		// "jsx": "preserve",                                /* Specify what JSX code is generated. */
 		// "experimentalDecorators": true,                   /* Enable experimental support for legacy experimental decorators. */
@@ -26,7 +26,10 @@
 		// "useDefineForClassFields": true,                  /* Emit ECMAScript-standard-compliant class fields. */
 		// "moduleDetection": "auto",                        /* Control what method is used to detect module-format JS files. */
 		/* Modules */
-		"module": "commonjs" /* Specify what module code is generated. */,
+		"module": "node16",
+		"moduleResolution": "node16",
+		"allowImportingTsExtensions": false,
+		"noEmit": false,
 		// "rootDir": "./",                                  /* Specify the root folder within your source files. */
 		// "moduleResolution": "node10",                     /* Specify how TypeScript looks up a file from a given module specifier. */
 		// "baseUrl": "./",                                  /* Specify the base directory to resolve non-relative module names. */
@@ -49,7 +52,7 @@
 		// "checkJs": true,                                  /* Enable error reporting in type-checked JavaScript files. */
 		// "maxNodeModuleJsDepth": 1,                        /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
 		/* Emit */
-		// "declaration": true,                              /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
+		"declaration": true,                              /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
 		// "declarationMap": true,                           /* Create sourcemaps for d.ts files. */
 		// "emitDeclarationOnly": true,                      /* Only output d.ts files and not JavaScript files. */
 		// "sourceMap": true,                                /* Create source map files for emitted JavaScript files. */
diff --git a/apps/canvas/config/eslint.config.mjs b/apps/canvas/config/eslint.config.mjs
new file mode 100644
index 0000000..20ef473
--- /dev/null
+++ b/apps/canvas/config/eslint.config.mjs
@@ -0,0 +1,23 @@
+import js from "@eslint/js";
+import tseslint from "typescript-eslint";
+
+export default tseslint.config(
+	{ ignores: ["dist"] },
+	{
+		extends: [js.configs.recommended, ...tseslint.configs.recommended],
+		files: ["**/*.ts"],
+		languageOptions: {
+			ecmaVersion: 2020,
+		},
+		rules: {
+			"@typescript-eslint/no-unused-vars": [
+				"error",
+				{
+					argsIgnorePattern: "^_$",
+					varsIgnorePattern: "^_$",
+					caughtErrorsIgnorePattern: "^_$",
+				},
+			],
+		},
+	},
+);
diff --git a/apps/canvas/config/package-lock.json b/apps/canvas/config/package-lock.json
new file mode 100644
index 0000000..ba8253b
--- /dev/null
+++ b/apps/canvas/config/package-lock.json
@@ -0,0 +1,1952 @@
+{
+  "name": "config",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "config",
+      "version": "1.0.0",
+      "license": "ISC",
+      "dependencies": {
+        "@xyflow/react": "^12.3.3",
+        "uuid": "^11.0.2",
+        "zod": "^3.24.4"
+      },
+      "devDependencies": {
+        "eslint": "^9.13.0",
+        "prettier": "3.5.3",
+        "typescript": "^5.8.3",
+        "typescript-eslint": "^8.11.0"
+      }
+    },
+    "node_modules/@eslint-community/eslint-utils": {
+      "version": "4.7.0",
+      "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
+      "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "eslint-visitor-keys": "^3.4.3"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+      }
+    },
+    "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+      "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/@eslint-community/regexpp": {
+      "version": "4.12.1",
+      "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
+      "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+      }
+    },
+    "node_modules/@eslint/config-array": {
+      "version": "0.20.1",
+      "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.1.tgz",
+      "integrity": "sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@eslint/object-schema": "^2.1.6",
+        "debug": "^4.3.1",
+        "minimatch": "^3.1.2"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/@eslint/config-helpers": {
+      "version": "0.2.3",
+      "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.3.tgz",
+      "integrity": "sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/@eslint/core": {
+      "version": "0.14.0",
+      "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz",
+      "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@types/json-schema": "^7.0.15"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/@eslint/eslintrc": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
+      "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ajv": "^6.12.4",
+        "debug": "^4.3.2",
+        "espree": "^10.0.1",
+        "globals": "^14.0.0",
+        "ignore": "^5.2.0",
+        "import-fresh": "^3.2.1",
+        "js-yaml": "^4.1.0",
+        "minimatch": "^3.1.2",
+        "strip-json-comments": "^3.1.1"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/@eslint/js": {
+      "version": "9.29.0",
+      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.29.0.tgz",
+      "integrity": "sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://eslint.org/donate"
+      }
+    },
+    "node_modules/@eslint/object-schema": {
+      "version": "2.1.6",
+      "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
+      "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/@eslint/plugin-kit": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.2.tgz",
+      "integrity": "sha512-4SaFZCNfJqvk/kenHpI8xvN42DMaoycy4PzKc5otHxRswww1kAt82OlBuwRVLofCACCTZEcla2Ydxv8scMXaTg==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@eslint/core": "^0.15.0",
+        "levn": "^0.4.1"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": {
+      "version": "0.15.0",
+      "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.0.tgz",
+      "integrity": "sha512-b7ePw78tEWWkpgZCDYkbqDOP8dmM6qe+AOC6iuJqlq1R/0ahMAeH3qynpnqKFGkMltrp44ohV4ubGyvLX28tzw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@types/json-schema": "^7.0.15"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/@humanfs/core": {
+      "version": "0.19.1",
+      "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+      "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=18.18.0"
+      }
+    },
+    "node_modules/@humanfs/node": {
+      "version": "0.16.6",
+      "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz",
+      "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@humanfs/core": "^0.19.1",
+        "@humanwhocodes/retry": "^0.3.0"
+      },
+      "engines": {
+        "node": ">=18.18.0"
+      }
+    },
+    "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz",
+      "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=18.18"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/nzakas"
+      }
+    },
+    "node_modules/@humanwhocodes/module-importer": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+      "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=12.22"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/nzakas"
+      }
+    },
+    "node_modules/@humanwhocodes/retry": {
+      "version": "0.4.3",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+      "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=18.18"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/nzakas"
+      }
+    },
+    "node_modules/@nodelib/fs.scandir": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+      "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.stat": "2.0.5",
+        "run-parallel": "^1.1.9"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.stat": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+      "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.walk": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+      "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.scandir": "2.1.5",
+        "fastq": "^1.6.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@types/d3-color": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+      "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-drag": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
+      "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-selection": "*"
+      }
+    },
+    "node_modules/@types/d3-interpolate": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+      "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-color": "*"
+      }
+    },
+    "node_modules/@types/d3-selection": {
+      "version": "3.0.11",
+      "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
+      "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-transition": {
+      "version": "3.0.9",
+      "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
+      "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-selection": "*"
+      }
+    },
+    "node_modules/@types/d3-zoom": {
+      "version": "3.0.8",
+      "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
+      "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-interpolate": "*",
+        "@types/d3-selection": "*"
+      }
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+      "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/json-schema": {
+      "version": "7.0.15",
+      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+      "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@typescript-eslint/eslint-plugin": {
+      "version": "8.34.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.0.tgz",
+      "integrity": "sha512-QXwAlHlbcAwNlEEMKQS2RCgJsgXrTJdjXT08xEgbPFa2yYQgVjBymxP5DrfrE7X7iodSzd9qBUHUycdyVJTW1w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@eslint-community/regexpp": "^4.10.0",
+        "@typescript-eslint/scope-manager": "8.34.0",
+        "@typescript-eslint/type-utils": "8.34.0",
+        "@typescript-eslint/utils": "8.34.0",
+        "@typescript-eslint/visitor-keys": "8.34.0",
+        "graphemer": "^1.4.0",
+        "ignore": "^7.0.0",
+        "natural-compare": "^1.4.0",
+        "ts-api-utils": "^2.1.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "@typescript-eslint/parser": "^8.34.0",
+        "eslint": "^8.57.0 || ^9.0.0",
+        "typescript": ">=4.8.4 <5.9.0"
+      }
+    },
+    "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+      "version": "7.0.5",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+      "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/@typescript-eslint/parser": {
+      "version": "8.34.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.34.0.tgz",
+      "integrity": "sha512-vxXJV1hVFx3IXz/oy2sICsJukaBrtDEQSBiV48/YIV5KWjX1dO+bcIr/kCPrW6weKXvsaGKFNlwH0v2eYdRRbA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/scope-manager": "8.34.0",
+        "@typescript-eslint/types": "8.34.0",
+        "@typescript-eslint/typescript-estree": "8.34.0",
+        "@typescript-eslint/visitor-keys": "8.34.0",
+        "debug": "^4.3.4"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^8.57.0 || ^9.0.0",
+        "typescript": ">=4.8.4 <5.9.0"
+      }
+    },
+    "node_modules/@typescript-eslint/project-service": {
+      "version": "8.34.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.34.0.tgz",
+      "integrity": "sha512-iEgDALRf970/B2YExmtPMPF54NenZUf4xpL3wsCRx/lgjz6ul/l13R81ozP/ZNuXfnLCS+oPmG7JIxfdNYKELw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/tsconfig-utils": "^8.34.0",
+        "@typescript-eslint/types": "^8.34.0",
+        "debug": "^4.3.4"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.8.4 <5.9.0"
+      }
+    },
+    "node_modules/@typescript-eslint/scope-manager": {
+      "version": "8.34.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.34.0.tgz",
+      "integrity": "sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/types": "8.34.0",
+        "@typescript-eslint/visitor-keys": "8.34.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/@typescript-eslint/tsconfig-utils": {
+      "version": "8.34.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.0.tgz",
+      "integrity": "sha512-+W9VYHKFIzA5cBeooqQxqNriAP0QeQ7xTiDuIOr71hzgffm3EL2hxwWBIIj4GuofIbKxGNarpKqIq6Q6YrShOA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.8.4 <5.9.0"
+      }
+    },
+    "node_modules/@typescript-eslint/type-utils": {
+      "version": "8.34.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.34.0.tgz",
+      "integrity": "sha512-n7zSmOcUVhcRYC75W2pnPpbO1iwhJY3NLoHEtbJwJSNlVAZuwqu05zY3f3s2SDWWDSo9FdN5szqc73DCtDObAg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/typescript-estree": "8.34.0",
+        "@typescript-eslint/utils": "8.34.0",
+        "debug": "^4.3.4",
+        "ts-api-utils": "^2.1.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^8.57.0 || ^9.0.0",
+        "typescript": ">=4.8.4 <5.9.0"
+      }
+    },
+    "node_modules/@typescript-eslint/types": {
+      "version": "8.34.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.34.0.tgz",
+      "integrity": "sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree": {
+      "version": "8.34.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.0.tgz",
+      "integrity": "sha512-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/project-service": "8.34.0",
+        "@typescript-eslint/tsconfig-utils": "8.34.0",
+        "@typescript-eslint/types": "8.34.0",
+        "@typescript-eslint/visitor-keys": "8.34.0",
+        "debug": "^4.3.4",
+        "fast-glob": "^3.3.2",
+        "is-glob": "^4.0.3",
+        "minimatch": "^9.0.4",
+        "semver": "^7.6.0",
+        "ts-api-utils": "^2.1.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.8.4 <5.9.0"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+      "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+      "version": "9.0.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+      "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/@typescript-eslint/utils": {
+      "version": "8.34.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.34.0.tgz",
+      "integrity": "sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@eslint-community/eslint-utils": "^4.7.0",
+        "@typescript-eslint/scope-manager": "8.34.0",
+        "@typescript-eslint/types": "8.34.0",
+        "@typescript-eslint/typescript-estree": "8.34.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^8.57.0 || ^9.0.0",
+        "typescript": ">=4.8.4 <5.9.0"
+      }
+    },
+    "node_modules/@typescript-eslint/visitor-keys": {
+      "version": "8.34.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.0.tgz",
+      "integrity": "sha512-qHV7pW7E85A0x6qyrFn+O+q1k1p3tQCsqIZ1KZ5ESLXY57aTvUd3/a4rdPTeXisvhXn2VQG0VSKUqs8KHF2zcA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/types": "8.34.0",
+        "eslint-visitor-keys": "^4.2.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/@xyflow/react": {
+      "version": "12.7.0",
+      "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.7.0.tgz",
+      "integrity": "sha512-U6VMEbYjiCg1byHrR7S+b5ZdHTjgCFX4KpBc634G/WtEBUvBLoMQdlCD6uJHqodnOAxpt3+G2wiDeTmXAFJzgQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@xyflow/system": "0.0.62",
+        "classcat": "^5.0.3",
+        "zustand": "^4.4.0"
+      },
+      "peerDependencies": {
+        "react": ">=17",
+        "react-dom": ">=17"
+      }
+    },
+    "node_modules/@xyflow/system": {
+      "version": "0.0.62",
+      "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.62.tgz",
+      "integrity": "sha512-Z2ufbnvuYxIOCGyzE/8eX8TAEM8Lpzc/JafjD1Tzy6ZJs/E7KGVU17Q1F5WDHVW+dbztJAdyXMG0ejR9bwSUAA==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-drag": "^3.0.7",
+        "@types/d3-interpolate": "^3.0.4",
+        "@types/d3-selection": "^3.0.10",
+        "@types/d3-transition": "^3.0.8",
+        "@types/d3-zoom": "^3.0.8",
+        "d3-drag": "^3.0.0",
+        "d3-interpolate": "^3.0.1",
+        "d3-selection": "^3.0.0",
+        "d3-zoom": "^3.0.0"
+      }
+    },
+    "node_modules/acorn": {
+      "version": "8.15.0",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+      "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "acorn": "bin/acorn"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/acorn-jsx": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+      "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+      }
+    },
+    "node_modules/ajv": {
+      "version": "6.12.6",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+      "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fast-deep-equal": "^3.1.1",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.4.1",
+        "uri-js": "^4.2.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/argparse": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+      "dev": true,
+      "license": "Python-2.0"
+    },
+    "node_modules/balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/brace-expansion": {
+      "version": "1.1.12",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+      "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/braces": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fill-range": "^7.1.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/callsites": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+      "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/chalk": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/classcat": {
+      "version": "5.0.5",
+      "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
+      "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
+      "license": "MIT"
+    },
+    "node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/cross-spawn": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/d3-color": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+      "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-dispatch": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+      "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-drag": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+      "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-dispatch": "1 - 3",
+        "d3-selection": "3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-ease": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+      "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-interpolate": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+      "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-color": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-selection": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+      "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-timer": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+      "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-transition": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+      "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-color": "1 - 3",
+        "d3-dispatch": "1 - 3",
+        "d3-ease": "1 - 3",
+        "d3-interpolate": "1 - 3",
+        "d3-timer": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "peerDependencies": {
+        "d3-selection": "2 - 3"
+      }
+    },
+    "node_modules/d3-zoom": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+      "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-dispatch": "1 - 3",
+        "d3-drag": "2 - 3",
+        "d3-interpolate": "1 - 3",
+        "d3-selection": "2 - 3",
+        "d3-transition": "2 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/debug": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+      "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/deep-is": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+      "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/escape-string-regexp": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+      "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/eslint": {
+      "version": "9.29.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.29.0.tgz",
+      "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@eslint-community/eslint-utils": "^4.2.0",
+        "@eslint-community/regexpp": "^4.12.1",
+        "@eslint/config-array": "^0.20.1",
+        "@eslint/config-helpers": "^0.2.1",
+        "@eslint/core": "^0.14.0",
+        "@eslint/eslintrc": "^3.3.1",
+        "@eslint/js": "9.29.0",
+        "@eslint/plugin-kit": "^0.3.1",
+        "@humanfs/node": "^0.16.6",
+        "@humanwhocodes/module-importer": "^1.0.1",
+        "@humanwhocodes/retry": "^0.4.2",
+        "@types/estree": "^1.0.6",
+        "@types/json-schema": "^7.0.15",
+        "ajv": "^6.12.4",
+        "chalk": "^4.0.0",
+        "cross-spawn": "^7.0.6",
+        "debug": "^4.3.2",
+        "escape-string-regexp": "^4.0.0",
+        "eslint-scope": "^8.4.0",
+        "eslint-visitor-keys": "^4.2.1",
+        "espree": "^10.4.0",
+        "esquery": "^1.5.0",
+        "esutils": "^2.0.2",
+        "fast-deep-equal": "^3.1.3",
+        "file-entry-cache": "^8.0.0",
+        "find-up": "^5.0.0",
+        "glob-parent": "^6.0.2",
+        "ignore": "^5.2.0",
+        "imurmurhash": "^0.1.4",
+        "is-glob": "^4.0.0",
+        "json-stable-stringify-without-jsonify": "^1.0.1",
+        "lodash.merge": "^4.6.2",
+        "minimatch": "^3.1.2",
+        "natural-compare": "^1.4.0",
+        "optionator": "^0.9.3"
+      },
+      "bin": {
+        "eslint": "bin/eslint.js"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://eslint.org/donate"
+      },
+      "peerDependencies": {
+        "jiti": "*"
+      },
+      "peerDependenciesMeta": {
+        "jiti": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/eslint-scope": {
+      "version": "8.4.0",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+      "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "esrecurse": "^4.3.0",
+        "estraverse": "^5.2.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/eslint-visitor-keys": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+      "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/espree": {
+      "version": "10.4.0",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+      "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "acorn": "^8.15.0",
+        "acorn-jsx": "^5.3.2",
+        "eslint-visitor-keys": "^4.2.1"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/esquery": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+      "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "estraverse": "^5.1.0"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "node_modules/esrecurse": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+      "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "estraverse": "^5.2.0"
+      },
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/estraverse": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+      "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/esutils": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+      "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/fast-deep-equal": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/fast-glob": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+      "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.stat": "^2.0.2",
+        "@nodelib/fs.walk": "^1.2.3",
+        "glob-parent": "^5.1.2",
+        "merge2": "^1.3.0",
+        "micromatch": "^4.0.8"
+      },
+      "engines": {
+        "node": ">=8.6.0"
+      }
+    },
+    "node_modules/fast-glob/node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fast-json-stable-stringify": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/fast-levenshtein": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+      "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/fastq": {
+      "version": "1.19.1",
+      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
+      "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "reusify": "^1.0.4"
+      }
+    },
+    "node_modules/file-entry-cache": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+      "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "flat-cache": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "node_modules/fill-range": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "to-regex-range": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/find-up": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+      "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "locate-path": "^6.0.0",
+        "path-exists": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/flat-cache": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+      "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "flatted": "^3.2.9",
+        "keyv": "^4.5.4"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/flatted": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
+      "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/glob-parent": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+      "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/globals": {
+      "version": "14.0.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+      "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/graphemer": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+      "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/ignore": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+      "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/import-fresh": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+      "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "parent-module": "^1.0.0",
+        "resolve-from": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/imurmurhash": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+      "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.8.19"
+      }
+    },
+    "node_modules/is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-glob": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.12.0"
+      }
+    },
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/js-yaml": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+      "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "argparse": "^2.0.1"
+      },
+      "bin": {
+        "js-yaml": "bin/js-yaml.js"
+      }
+    },
+    "node_modules/json-buffer": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+      "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/json-schema-traverse": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/json-stable-stringify-without-jsonify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+      "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/keyv": {
+      "version": "4.5.4",
+      "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+      "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "json-buffer": "3.0.1"
+      }
+    },
+    "node_modules/levn": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+      "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "prelude-ls": "^1.2.1",
+        "type-check": "~0.4.0"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/locate-path": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+      "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-locate": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/lodash.merge": {
+      "version": "4.6.2",
+      "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+      "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/merge2": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+      "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/micromatch": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "braces": "^3.0.3",
+        "picomatch": "^2.3.1"
+      },
+      "engines": {
+        "node": ">=8.6"
+      }
+    },
+    "node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/natural-compare": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+      "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/optionator": {
+      "version": "0.9.4",
+      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+      "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "deep-is": "^0.1.3",
+        "fast-levenshtein": "^2.0.6",
+        "levn": "^0.4.1",
+        "prelude-ls": "^1.2.1",
+        "type-check": "^0.4.0",
+        "word-wrap": "^1.2.5"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/p-limit": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+      "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "yocto-queue": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-locate": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+      "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-limit": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/parent-module": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+      "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "callsites": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/path-exists": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/picomatch": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/prelude-ls": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+      "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/prettier": {
+      "version": "3.5.3",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
+      "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "prettier": "bin/prettier.cjs"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/prettier/prettier?sponsor=1"
+      }
+    },
+    "node_modules/punycode": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+      "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/queue-microtask": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/react": {
+      "version": "19.1.0",
+      "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
+      "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
+      "license": "MIT",
+      "peer": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/react-dom": {
+      "version": "19.1.0",
+      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
+      "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "scheduler": "^0.26.0"
+      },
+      "peerDependencies": {
+        "react": "^19.1.0"
+      }
+    },
+    "node_modules/resolve-from": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+      "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/reusify": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+      "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "iojs": ">=1.0.0",
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/run-parallel": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "queue-microtask": "^1.2.2"
+      }
+    },
+    "node_modules/scheduler": {
+      "version": "0.26.0",
+      "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
+      "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
+      "license": "MIT",
+      "peer": true
+    },
+    "node_modules/semver": {
+      "version": "7.7.2",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+      "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-json-comments": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+      "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/supports-color": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-number": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=8.0"
+      }
+    },
+    "node_modules/ts-api-utils": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
+      "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18.12"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.8.4"
+      }
+    },
+    "node_modules/type-check": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+      "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "prelude-ls": "^1.2.1"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/typescript": {
+      "version": "5.8.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
+      "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/typescript-eslint": {
+      "version": "8.34.0",
+      "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.34.0.tgz",
+      "integrity": "sha512-MRpfN7uYjTrTGigFCt8sRyNqJFhjN0WwZecldaqhWm+wy0gaRt8Edb/3cuUy0zdq2opJWT6iXINKAtewnDOltQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/eslint-plugin": "8.34.0",
+        "@typescript-eslint/parser": "8.34.0",
+        "@typescript-eslint/utils": "8.34.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^8.57.0 || ^9.0.0",
+        "typescript": ">=4.8.4 <5.9.0"
+      }
+    },
+    "node_modules/uri-js": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+      "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "punycode": "^2.1.0"
+      }
+    },
+    "node_modules/use-sync-external-store": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
+      "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
+      "license": "MIT",
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+      }
+    },
+    "node_modules/uuid": {
+      "version": "11.1.0",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
+      "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
+      "funding": [
+        "https://github.com/sponsors/broofa",
+        "https://github.com/sponsors/ctavan"
+      ],
+      "license": "MIT",
+      "bin": {
+        "uuid": "dist/esm/bin/uuid"
+      }
+    },
+    "node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/word-wrap": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+      "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/yocto-queue": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+      "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/zod": {
+      "version": "3.25.64",
+      "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.64.tgz",
+      "integrity": "sha512-hbP9FpSZf7pkS7hRVUrOjhwKJNyampPgtXKc3AN6DsWtoHsg2Sb4SQaS4Tcay380zSwd2VPo9G9180emBACp5g==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/colinhacks"
+      }
+    },
+    "node_modules/zustand": {
+      "version": "4.5.7",
+      "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
+      "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
+      "license": "MIT",
+      "dependencies": {
+        "use-sync-external-store": "^1.2.2"
+      },
+      "engines": {
+        "node": ">=12.7.0"
+      },
+      "peerDependencies": {
+        "@types/react": ">=16.8",
+        "immer": ">=9.0.6",
+        "react": ">=16.8"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "immer": {
+          "optional": true
+        },
+        "react": {
+          "optional": true
+        }
+      }
+    }
+  }
+}
diff --git a/apps/canvas/config/package.json b/apps/canvas/config/package.json
new file mode 100644
index 0000000..c5b04e6
--- /dev/null
+++ b/apps/canvas/config/package.json
@@ -0,0 +1,34 @@
+{
+  "name": "config",
+  "version": "1.0.0",
+  "description": "",
+  "license": "ISC",
+  "author": "",
+  "type": "module",
+  "main": "dist/index.js",
+  "scripts": {
+    "build": "tsc",
+    "format": "prettier --write src/**/*.{js,ts,jsx,tsx} --list-different",
+    "format-check": "prettier --check src/**/*.{js,ts,jsx,tsx}",
+    "lint": "eslint ."
+  },
+  "dependencies": {
+    "zod": "^3.24.4",
+    "@xyflow/react": "^12.3.3",
+    "uuid": "^11.0.2"
+  },
+  "devDependencies": {
+    "eslint": "^9.13.0",
+    "prettier": "3.5.3",
+    "typescript": "^5.8.3",
+    "typescript-eslint": "^8.11.0"
+  },
+  "prettier": {
+    "printWidth": 120,
+    "tabWidth": 4,
+    "useTabs": true,
+    "singleQuote": false,
+    "trailingComma": "all",
+    "arrowParens": "always"
+  }
+}
diff --git a/apps/canvas/config/src/config.ts b/apps/canvas/config/src/config.ts
new file mode 100644
index 0000000..53fd64c
--- /dev/null
+++ b/apps/canvas/config/src/config.ts
@@ -0,0 +1,598 @@
+import {
+	AppNode,
+	BoundEnvVar,
+	Env,
+	GatewayHttpsNode,
+	GatewayTCPNode,
+	MongoDBNode,
+	Network,
+	NetworkNode,
+	Port,
+	PostgreSQLNode,
+	ServiceNode,
+	VolumeNode,
+} from "./graph.js";
+import { Edge } from "@xyflow/react";
+import { v4 as uuidv4 } from "uuid";
+import { ConfigWithInput, Ingress, Service, Volume, PostgreSQL, MongoDB, Config, PortDomain } from "./types.js";
+
+export function generateDodoConfig(appId: string | undefined, nodes: AppNode[], env: Env): ConfigWithInput | null {
+	try {
+		if (appId == null || env.managerAddr == null) {
+			return null;
+		}
+		const networkMap = new Map(env.networks.map((n) => [n.domain, n.name]));
+		const ingressNodes = nodes
+			.filter((n) => n.type === "gateway-https")
+			.filter((n) => n.data.https !== undefined && !n.data.readonly);
+		const tcpNodes = nodes
+			.filter((n) => n.type === "gateway-tcp")
+			.filter((n) => n.data.exposed !== undefined && !n.data.readonly);
+		const findExpose = (n: AppNode): PortDomain[] => {
+			return n.data.ports
+				.map((p) => [n.id, p.id, p.name])
+				.flatMap((sp) => {
+					return tcpNodes.flatMap((i) =>
+						(i.data.exposed || [])
+							.filter((t) => t.serviceId === sp[0] && t.portId === sp[1])
+							.map(() => ({
+								nodeId: i.id,
+								network: networkMap.get(i.data.network!)!,
+								subdomain: i.data.subdomain!,
+								port: { name: sp[2] },
+							})),
+					);
+				});
+		};
+		return {
+			input: {
+				appId: appId,
+				managerAddr: env.managerAddr,
+			},
+			service: nodes
+				.filter((n) => n.type === "app")
+				.map((n): Service => {
+					return {
+						nodeId: n.id,
+						type: n.data.type,
+						name: n.data.label,
+						source: {
+							repository: nodes
+								.filter((i) => i.type === "github")
+								.find((i) => i.id === n.data.repository?.repoNodeId)!.data.repository!.sshURL,
+							branch:
+								n.data.repository != undefined && "branch" in n.data.repository
+									? n.data.repository.branch
+									: "main",
+							rootDir:
+								n.data.repository != undefined && "rootDir" in n.data.repository
+									? n.data.repository.rootDir
+									: "/",
+						},
+						ports: (n.data.ports || [])
+							.filter((p) => !n.data.dev?.enabled || (p.value != 22 && p.value != 9090))
+							.map((p) => ({
+								name: p.name.toLowerCase(),
+								value: p.value,
+								protocol: "TCP", // TODO(gio)
+							})),
+						env: (n.data.envVars || [])
+							.filter((e) => "name" in e)
+							.map((e) => ({
+								name: e.name,
+								alias: "alias" in e ? e.alias : undefined,
+							})),
+						ingress: ingressNodes
+							.filter((i) => i.data.https!.serviceId === n.id)
+							.map(
+								(i): Ingress => ({
+									nodeId: i.id,
+									network: networkMap.get(i.data.network!)!,
+									subdomain: i.data.subdomain!,
+									port: {
+										name: n.data.ports.find((p) => p.id === i.data.https!.portId)!.name,
+									},
+									auth:
+										i.data.auth?.enabled || false
+											? {
+													enabled: true,
+													groups: i.data.auth!.groups,
+													noAuthPathPatterns: i.data.auth!.noAuthPathPatterns,
+												}
+											: {
+													enabled: false,
+												},
+								}),
+							),
+						expose: findExpose(n),
+						preBuildCommands: n.data.preBuildCommands
+							? n.data.preBuildCommands.split("\n").map((cmd) => ({ bin: cmd }))
+							: [],
+						dev: {
+							enabled: n.data.dev ? n.data.dev.enabled : false,
+							username: n.data.dev && n.data.dev.enabled ? env.user.username : undefined,
+							codeServer:
+								n.data.dev?.enabled && n.data.dev.expose != null
+									? {
+											network: networkMap.get(n.data.dev.expose.network)!,
+											subdomain: n.data.dev.expose.subdomain,
+										}
+									: undefined,
+							ssh:
+								n.data.dev?.enabled && n.data.dev.expose != null
+									? {
+											network: networkMap.get(n.data.dev.expose.network)!,
+											subdomain: n.data.dev.expose.subdomain,
+										}
+									: undefined,
+						},
+					};
+				}),
+			volume: nodes
+				.filter((n) => n.type === "volume")
+				.map(
+					(n): Volume => ({
+						nodeId: n.id,
+						name: n.data.label,
+						accessMode: n.data.type,
+						size: n.data.size,
+					}),
+				),
+			postgresql: nodes
+				.filter((n) => n.type === "postgresql")
+				.map(
+					(n): PostgreSQL => ({
+						nodeId: n.id,
+						name: n.data.label,
+						size: "1Gi", // TODO(gio)
+						expose: findExpose(n).map((e) => ({ network: e.network, subdomain: e.subdomain })),
+					}),
+				),
+			mongodb: nodes
+				.filter((n) => n.type === "mongodb")
+				.map(
+					(n): MongoDB => ({
+						nodeId: n.id,
+						name: n.data.label,
+						size: "1Gi", // TODO(gio)
+						expose: findExpose(n).map((e) => ({ network: e.network, subdomain: e.subdomain })),
+					}),
+				),
+		};
+	} catch (e) {
+		console.log(e);
+		return { input: { appId: "qweqwe", managerAddr: "" } };
+	}
+}
+
+export type Graph = {
+	nodes: AppNode[];
+	edges: Edge[];
+};
+
+export function configToGraph(config: Config, networks: Network[], current?: Graph): Graph {
+	if (current == null) {
+		current = { nodes: [], edges: [] };
+	}
+	const ret: Graph = {
+		nodes: [],
+		edges: [],
+	};
+	if (networks.length === 0) {
+		return ret;
+	}
+	const networkNodes = networks.map((n): NetworkNode => {
+		let existing: NetworkNode | undefined = undefined;
+		existing = current.nodes
+			.filter((i): i is NetworkNode => i.type === "network")
+			.find((i) => i.data.domain === n.domain);
+		return {
+			id: n.domain,
+			type: "network",
+			data: {
+				label: n.name,
+				domain: n.domain,
+				envVars: [],
+				ports: [],
+			},
+			position: existing != null ? existing.position : { x: 0, y: 0 },
+		};
+	});
+	const services = config.service?.map((s): ServiceNode => {
+		let existing: ServiceNode | null = null;
+		if (s.nodeId !== undefined) {
+			existing = current.nodes.find((n) => n.id === s.nodeId) as ServiceNode;
+		}
+		return {
+			id: existing != null ? existing.id : uuidv4(),
+			type: "app",
+			data: {
+				label: s.name,
+				type: s.type,
+				env: [],
+				ports: (s.ports || []).map(
+					(p): Port => ({
+						id: uuidv4(),
+						name: p.name,
+						value: p.value,
+					}),
+				),
+				envVars: (s.env || []).map((e): BoundEnvVar => {
+					if (e.alias != null) {
+						return {
+							id: uuidv4(),
+							name: e.name,
+							source: null,
+							alias: e.alias,
+							isEditting: false,
+						};
+					} else {
+						return {
+							id: uuidv4(),
+							name: e.name,
+							source: null,
+							isEditting: false,
+						};
+					}
+				}),
+				volume: s.volume || [],
+				preBuildCommands: s.preBuildCommands?.map((p) => p.bin).join("\n") || "",
+				// TODO(gio): dev
+				isChoosingPortToConnect: false,
+			},
+			// TODO(gio): generate position
+			position:
+				existing != null
+					? existing.position
+					: {
+							x: 0,
+							y: 0,
+						},
+		};
+	});
+	const serviceGateways = config.service?.flatMap((s, index): GatewayHttpsNode[] => {
+		return (s.ingress || []).map((i): GatewayHttpsNode => {
+			let existing: GatewayHttpsNode | null = null;
+			if (i.nodeId !== undefined) {
+				existing = current.nodes.find((n) => n.id === i.nodeId) as GatewayHttpsNode;
+			}
+			console.log("!!!", i.network, networks);
+			return {
+				id: existing != null ? existing.id : uuidv4(),
+				type: "gateway-https",
+				data: {
+					label: i.subdomain,
+					envVars: [],
+					ports: [],
+					network: networks.find((n) => n.name.toLowerCase() === i.network.toLowerCase())!.domain,
+					subdomain: i.subdomain,
+					https: {
+						serviceId: services![index]!.id,
+						portId: services![index]!.data.ports.find((p) => {
+							const port = i.port;
+							if ("name" in port) {
+								return p.name === port.name;
+							} else {
+								return `${p.value}` === port.value;
+							}
+						})!.id,
+					},
+					auth: i.auth.enabled
+						? {
+								enabled: true,
+								groups: i.auth.groups || [],
+								noAuthPathPatterns: i.auth.noAuthPathPatterns || [],
+							}
+						: {
+								enabled: false,
+								groups: [],
+								noAuthPathPatterns: [],
+							},
+				},
+				position: {
+					x: 0,
+					y: 0,
+				},
+			};
+		});
+	});
+	const exposures = new Map<string, GatewayTCPNode>();
+	config.service
+		?.flatMap((s, index): GatewayTCPNode[] => {
+			return (s.expose || []).map((e): GatewayTCPNode => {
+				let existing: GatewayTCPNode | null = null;
+				if (e.nodeId !== undefined) {
+					existing = current.nodes.find((n) => n.id === e.nodeId) as GatewayTCPNode;
+				}
+				return {
+					id: existing != null ? existing.id : uuidv4(),
+					type: "gateway-tcp",
+					data: {
+						label: e.subdomain,
+						envVars: [],
+						ports: [],
+						network: networks.find((n) => n.name.toLowerCase() === e.network.toLowerCase())!.domain,
+						subdomain: e.subdomain,
+						exposed: [
+							{
+								serviceId: services![index]!.id,
+								portId: services![index]!.data.ports.find((p) => {
+									const port = e.port;
+									if ("name" in port) {
+										return p.name === port.name;
+									} else {
+										return p.value === port.value;
+									}
+								})!.id,
+							},
+						],
+					},
+					position: existing != null ? existing.position : { x: 0, y: 0 },
+				};
+			});
+		})
+		.forEach((n) => {
+			const key = `${n.data.network}-${n.data.subdomain}`;
+			if (!exposures.has(key)) {
+				exposures.set(key, n);
+			} else {
+				exposures.get(key)!.data.exposed.push(...n.data.exposed);
+			}
+		});
+	const volumes = config.volume?.map((v): VolumeNode => {
+		let existing: VolumeNode | null = null;
+		if (v.nodeId !== undefined) {
+			existing = current.nodes.find((n) => n.id === v.nodeId) as VolumeNode;
+		}
+		return {
+			id: existing != null ? existing.id : uuidv4(),
+			type: "volume",
+			data: {
+				label: v.name,
+				type: v.accessMode,
+				size: v.size,
+				attachedTo: [],
+				envVars: [],
+				ports: [],
+			},
+			position:
+				existing != null
+					? existing.position
+					: {
+							x: 0,
+							y: 0,
+						},
+		};
+	});
+	const postgresql = config.postgresql?.map((p): PostgreSQLNode => {
+		let existing: PostgreSQLNode | null = null;
+		if (p.nodeId !== undefined) {
+			existing = current.nodes.find((n) => n.id === p.nodeId) as PostgreSQLNode;
+		}
+		return {
+			id: existing != null ? existing.id : uuidv4(),
+			type: "postgresql",
+			data: {
+				label: p.name,
+				volumeId: "", // TODO(gio): volume
+				envVars: [],
+				ports: [
+					{
+						id: "connection",
+						name: "connection",
+						value: 5432,
+					},
+				],
+			},
+			position:
+				existing != null
+					? existing.position
+					: {
+							x: 0,
+							y: 0,
+						},
+		};
+	});
+	config.postgresql
+		?.flatMap((p, index): GatewayTCPNode[] => {
+			return (p.expose || []).map((e): GatewayTCPNode => {
+				let existing: GatewayTCPNode | null = null;
+				if (e.nodeId !== undefined) {
+					existing = current.nodes.find((n) => n.id === e.nodeId) as GatewayTCPNode;
+				}
+				return {
+					id: existing != null ? existing.id : uuidv4(),
+					type: "gateway-tcp",
+					data: {
+						label: e.subdomain,
+						envVars: [],
+						ports: [],
+						network: networks.find((n) => n.name.toLowerCase() === e.network.toLowerCase())!.domain,
+						subdomain: e.subdomain,
+						exposed: [
+							{
+								serviceId: postgresql![index]!.id,
+								portId: "connection",
+							},
+						],
+					},
+					position: existing != null ? existing.position : { x: 0, y: 0 },
+				};
+			});
+		})
+		.forEach((n) => {
+			const key = `${n.data.network}-${n.data.subdomain}`;
+			if (!exposures.has(key)) {
+				exposures.set(key, n);
+			} else {
+				exposures.get(key)!.data.exposed.push(...n.data.exposed);
+			}
+		});
+	const mongodb = config.mongodb?.map((m): MongoDBNode => {
+		let existing: MongoDBNode | null = null;
+		if (m.nodeId !== undefined) {
+			existing = current.nodes.find((n) => n.id === m.nodeId) as MongoDBNode;
+		}
+		return {
+			id: existing != null ? existing.id : uuidv4(),
+			type: "mongodb",
+			data: {
+				label: m.name,
+				volumeId: "", // TODO(gio): volume
+				envVars: [],
+				ports: [
+					{
+						id: "connection",
+						name: "connection",
+						value: 27017,
+					},
+				],
+			},
+			position:
+				existing != null
+					? existing.position
+					: {
+							x: 0,
+							y: 0,
+						},
+		};
+	});
+	config.mongodb
+		?.flatMap((p, index): GatewayTCPNode[] => {
+			return (p.expose || []).map((e): GatewayTCPNode => {
+				let existing: GatewayTCPNode | null = null;
+				if (e.nodeId !== undefined) {
+					existing = current.nodes.find((n) => n.id === e.nodeId) as GatewayTCPNode;
+				}
+				return {
+					id: existing != null ? existing.id : uuidv4(),
+					type: "gateway-tcp",
+					data: {
+						label: e.subdomain,
+						envVars: [],
+						ports: [],
+						network: networks.find((n) => n.name.toLowerCase() === e.network.toLowerCase())!.domain,
+						subdomain: e.subdomain,
+						exposed: [
+							{
+								serviceId: mongodb![index]!.id,
+								portId: "connection",
+							},
+						],
+					},
+					position: existing != null ? existing.position : { x: 0, y: 0 },
+				};
+			});
+		})
+		.forEach((n) => {
+			const key = `${n.data.network}-${n.data.subdomain}`;
+			if (!exposures.has(key)) {
+				exposures.set(key, n);
+			} else {
+				exposures.get(key)!.data.exposed.push(...n.data.exposed);
+			}
+		});
+	ret.nodes = [
+		...networkNodes,
+		...ret.nodes,
+		...(services || []),
+		...(serviceGateways || []),
+		...(volumes || []),
+		...(postgresql || []),
+		...(mongodb || []),
+		...(exposures.values() || []),
+	];
+	services?.forEach((s) => {
+		s.data.envVars.forEach((e) => {
+			if (!("name" in e)) {
+				return;
+			}
+			if (!e.name.startsWith("DODO_")) {
+				return;
+			}
+			let r: {
+				type: string;
+				name: string;
+			} | null = null;
+			if (e.name.startsWith("DODO_PORT_")) {
+				return;
+			} else if (e.name.startsWith("DODO_POSTGRESQL_")) {
+				r = {
+					type: "postgresql",
+					name: e.name.replace("DODO_POSTGRESQL_", "").replace("_URL", "").toLowerCase(),
+				};
+			} else if (e.name.startsWith("DODO_MONGODB_")) {
+				r = {
+					type: "mongodb",
+					name: e.name.replace("DODO_MONGODB_", "").replace("_URL", "").toLowerCase(),
+				};
+			} else if (e.name.startsWith("DODO_VOLUME_")) {
+				r = {
+					type: "volume",
+					name: e.name.replace("DODO_VOLUME_", "").toLowerCase(),
+				};
+			}
+			if (r != null) {
+				e.source = ret.nodes.find((n) => n.type === r.type && n.data.label.toLowerCase() === r.name)!.id;
+			}
+		});
+	});
+	const envVarEdges = [...(services || [])].flatMap((n): Edge[] => {
+		return n.data.envVars.flatMap((e): Edge[] => {
+			if (e.source == null) {
+				return [];
+			}
+			const sn = ret.nodes.find((n) => n.id === e.source!)!;
+			const sourceHandle = sn.type === "app" ? "ports" : sn.type === "volume" ? "volume" : "env_var";
+			return [
+				{
+					id: uuidv4(),
+					source: e.source!,
+					sourceHandle: sourceHandle,
+					target: n.id,
+					targetHandle: "env_var",
+				},
+			];
+		});
+	});
+	const exposureEdges = [...exposures.values()].flatMap((n): Edge[] => {
+		return n.data.exposed.flatMap((e): Edge[] => {
+			return [
+				{
+					id: uuidv4(),
+					source: e.serviceId,
+					sourceHandle: ret.nodes.find((n) => n.id === e.serviceId)!.type === "app" ? "ports" : "env_var",
+					target: n.id,
+					targetHandle: "tcp",
+				},
+				{
+					id: uuidv4(),
+					source: n.id,
+					sourceHandle: "subdomain",
+					target: n.data.network!,
+					targetHandle: "subdomain",
+				},
+			];
+		});
+	});
+	const ingressEdges = [...(serviceGateways || [])].flatMap((n): Edge[] => {
+		return [
+			{
+				id: uuidv4(),
+				source: n.data.https!.serviceId,
+				sourceHandle: "ports",
+				target: n.id,
+				targetHandle: "https",
+			},
+			{
+				id: uuidv4(),
+				source: n.id,
+				sourceHandle: "subdomain",
+				target: n.data.network!,
+				targetHandle: "subdomain",
+			},
+		];
+	});
+	ret.edges = [...envVarEdges, ...exposureEdges, ...ingressEdges];
+	return ret;
+}
diff --git a/apps/canvas/config/src/graph.ts b/apps/canvas/config/src/graph.ts
new file mode 100644
index 0000000..e8741f9
--- /dev/null
+++ b/apps/canvas/config/src/graph.ts
@@ -0,0 +1,320 @@
+import { z } from "zod";
+import { Node } from "@xyflow/react";
+import { Domain, ServiceType, VolumeType } from "./types.js";
+
+export const serviceAnalyzisSchema = z.object({
+	name: z.string(),
+	location: z.string(),
+	configVars: z.array(
+		z.object({
+			name: z.string(),
+			category: z.enum(["CommandLineFlag", "EnvironmentVariable"]),
+			type: z.optional(z.enum(["String", "Number", "Boolean"])),
+			semanticType: z.optional(
+				z.enum([
+					"EXPANDED_ENV_VAR",
+					"PORT",
+					"FILESYSTEM_PATH",
+					"DATABASE_URL",
+					"SQLITE_PATH",
+					"POSTGRES_URL",
+					"POSTGRES_PASSWORD",
+					"POSTGRES_USER",
+					"POSTGRES_DB",
+					"POSTGRES_PORT",
+					"POSTGRES_HOST",
+					"POSTGRES_SSL",
+					"MONGO_URL",
+					"MONGO_PASSWORD",
+					"MONGO_USER",
+					"MONGO_DB",
+					"MONGO_PORT",
+					"MONGO_HOST",
+					"MONGO_SSL",
+				]),
+			),
+		}),
+	),
+});
+
+export type BoundEnvVar =
+	| {
+			id: string;
+			source: string | null;
+	  }
+	| {
+			id: string;
+			source: string | null;
+			name: string;
+			isEditting: boolean;
+	  }
+	| {
+			id: string;
+			source: string | null;
+			name: string;
+			alias: string;
+			isEditting: boolean;
+	  }
+	| {
+			id: string;
+			source: string | null;
+			portId: string;
+			name: string;
+			alias: string;
+			isEditting: boolean;
+	  };
+
+export type EnvVar = {
+	name: string;
+	value: string;
+};
+
+export type InitData = {
+	label: string;
+	envVars: BoundEnvVar[];
+	ports: Port[];
+};
+
+export type NodeData = InitData & {
+	activeField?: string | undefined;
+	state?: string | null;
+};
+
+export type PortConnectedTo = {
+	serviceId: string;
+	portId: string;
+};
+
+export type NetworkData = NodeData & {
+	domain: string;
+};
+
+export type NetworkNode = Node<NetworkData> & {
+	type: "network";
+};
+
+export type GatewayHttpsData = NodeData & {
+	readonly?: boolean;
+	network?: string;
+	subdomain?: string;
+	https?: PortConnectedTo;
+	auth?: {
+		enabled: boolean;
+		groups: string[];
+		noAuthPathPatterns: string[];
+	};
+};
+
+export type GatewayHttpsNode = Node<GatewayHttpsData> & {
+	type: "gateway-https";
+};
+
+export type GatewayTCPData = NodeData & {
+	readonly?: boolean;
+	network?: string;
+	subdomain?: string;
+	exposed: PortConnectedTo[];
+	selected?: {
+		serviceId?: string;
+		portId?: string;
+	};
+};
+
+export type GatewayTCPNode = Node<GatewayTCPData> & {
+	type: "gateway-tcp";
+};
+
+export type Port = {
+	id: string;
+	name: string;
+	value: number;
+};
+
+export type ServiceData = NodeData & {
+	type: ServiceType;
+	repository?:
+		| {
+				id: number;
+				repoNodeId: string;
+		  }
+		| {
+				id: number;
+				repoNodeId: string;
+				branch: string;
+		  }
+		| {
+				id: number;
+				repoNodeId: string;
+				branch: string;
+				rootDir: string;
+		  };
+	env: string[];
+	volume: string[];
+	preBuildCommands: string;
+	isChoosingPortToConnect: boolean;
+	dev?:
+		| {
+				enabled: false;
+				expose?: Domain;
+		  }
+		| {
+				enabled: true;
+				expose?: Domain;
+				codeServerNodeId: string;
+				sshNodeId: string;
+		  };
+	info?: z.infer<typeof serviceAnalyzisSchema>;
+};
+
+export type ServiceNode = Node<ServiceData> & {
+	type: "app";
+};
+
+export type VolumeData = NodeData & {
+	type: VolumeType;
+	size: string;
+	attachedTo: string[];
+};
+
+export type VolumeNode = Node<VolumeData> & {
+	type: "volume";
+};
+
+export type PostgreSQLData = NodeData & {
+	volumeId: string;
+};
+
+export type PostgreSQLNode = Node<PostgreSQLData> & {
+	type: "postgresql";
+};
+
+export type MongoDBData = NodeData & {
+	volumeId: string;
+};
+
+export type MongoDBNode = Node<MongoDBData> & {
+	type: "mongodb";
+};
+
+export type GithubData = NodeData & {
+	repository?: {
+		id: number;
+		sshURL: string;
+		fullName: string;
+	};
+};
+
+export type GithubNode = Node<GithubData> & {
+	type: "github";
+};
+
+export type NANode = Node<NodeData> & {
+	type: undefined;
+};
+
+export type AppNode =
+	| NetworkNode
+	| GatewayHttpsNode
+	| GatewayTCPNode
+	| ServiceNode
+	| VolumeNode
+	| PostgreSQLNode
+	| MongoDBNode
+	| GithubNode
+	| NANode;
+
+export type NodeType = Exclude<Pick<AppNode, "type">["type"], undefined>;
+
+export const networkSchema = z.object({
+	name: z.string().min(1),
+	domain: z.string().min(1),
+	hasAuth: z.boolean(),
+});
+
+export type Network = z.infer<typeof networkSchema>;
+
+export const accessSchema = z.discriminatedUnion("type", [
+	z.object({
+		type: z.literal("https"),
+		name: z.string(),
+		address: z.string(),
+	}),
+	z.object({
+		type: z.literal("ssh"),
+		name: z.string(),
+		host: z.string(),
+		port: z.number(),
+	}),
+	z.object({
+		type: z.literal("tcp"),
+		name: z.string(),
+		host: z.string(),
+		port: z.number(),
+	}),
+	z.object({
+		type: z.literal("udp"),
+		name: z.string(),
+		host: z.string(),
+		port: z.number(),
+	}),
+	z.object({
+		type: z.literal("postgresql"),
+		name: z.string(),
+		host: z.string(),
+		port: z.number(),
+		database: z.string(),
+		username: z.string(),
+		password: z.string(),
+	}),
+	z.object({
+		type: z.literal("mongodb"),
+		name: z.string(),
+		host: z.string(),
+		port: z.number(),
+		database: z.string(),
+		username: z.string(),
+		password: z.string(),
+	}),
+]);
+
+export const serviceInfoSchema = z.object({
+	name: z.string(),
+	workers: z.array(
+		z.object({
+			id: z.string(),
+			commit: z.optional(
+				z.object({
+					hash: z.string(),
+					message: z.string(),
+				}),
+			),
+			commands: z.optional(
+				z.array(
+					z.object({
+						command: z.string(),
+						state: z.string(),
+					}),
+				),
+			),
+		}),
+	),
+});
+
+export const envSchema = z.object({
+	managerAddr: z.optional(z.string().min(1)),
+	instanceId: z.optional(z.string().min(1)),
+	deployKeyPublic: z.optional(z.nullable(z.string().min(1))),
+	networks: z.array(networkSchema).default([]),
+	integrations: z.object({
+		github: z.boolean(),
+	}),
+	services: z.array(serviceInfoSchema),
+	user: z.object({
+		id: z.string(),
+		username: z.string(),
+	}),
+	access: z.array(accessSchema),
+});
+
+export type ServiceInfo = z.infer<typeof serviceInfoSchema>;
+export type Env = z.infer<typeof envSchema>;
diff --git a/apps/canvas/config/src/index.ts b/apps/canvas/config/src/index.ts
new file mode 100644
index 0000000..d064f1c
--- /dev/null
+++ b/apps/canvas/config/src/index.ts
@@ -0,0 +1,51 @@
+export {
+	Auth,
+	AuthDisabled,
+	AuthEnabled,
+	Config,
+	ConfigSchema,
+	ConfigWithInputSchema,
+	Domain,
+	Ingress,
+	MongoDB,
+	PortDomain,
+	PortValue,
+	PostgreSQL,
+	Service,
+	ServiceTypes,
+	Volume,
+	ConfigWithInput,
+	VolumeType,
+} from "./types.js";
+
+export {
+	AppNode,
+	NodeType,
+	Network,
+	ServiceNode,
+	BoundEnvVar,
+	GatewayTCPNode,
+	GatewayHttpsNode,
+	GithubNode,
+	serviceAnalyzisSchema,
+	ServiceData,
+	VolumeNode,
+	PostgreSQLNode,
+	MongoDBNode,
+	Port,
+	EnvVar,
+	NodeData,
+	InitData,
+	NetworkData,
+	GatewayHttpsData,
+	GatewayTCPData,
+	ServiceInfo,
+	Env,
+	VolumeData,
+	PostgreSQLData,
+	MongoDBData,
+	GithubData,
+	envSchema,
+} from "./graph.js";
+
+export { generateDodoConfig, configToGraph } from "./config.js";
diff --git a/apps/canvas/config/src/types.ts b/apps/canvas/config/src/types.ts
new file mode 100644
index 0000000..fe417b8
--- /dev/null
+++ b/apps/canvas/config/src/types.ts
@@ -0,0 +1,155 @@
+import { z } from "zod";
+
+const AuthDisabledSchema = z.object({
+	enabled: z.literal(false),
+});
+
+const AuthEnabledSchema = z.object({
+	enabled: z.literal(true),
+	groups: z.array(z.string()),
+	noAuthPathPatterns: z.array(z.string()),
+});
+
+const AuthSchema = z.union([AuthDisabledSchema, AuthEnabledSchema]);
+
+const IngressSchema = z.object({
+	nodeId: z.string().optional(),
+	network: z.string(),
+	subdomain: z.string(),
+	port: z.union([z.object({ name: z.string() }), z.object({ value: z.string() })]),
+	auth: AuthSchema,
+});
+
+const DomainSchema = z.object({
+	nodeId: z.string().optional(),
+	network: z.string(),
+	subdomain: z.string(),
+});
+
+const PortValueSchema = z.union([
+	z.object({
+		name: z.string(),
+	}),
+	z.object({
+		value: z.number(),
+	}),
+]);
+
+const PortDomainSchema = DomainSchema.extend({
+	port: PortValueSchema,
+});
+
+export const ServiceTypes = [
+	"deno:2.2.0",
+	"golang:1.20.0",
+	"golang:1.22.0",
+	"golang:1.24.0",
+	"hugo:latest",
+	"php:8.2-apache",
+	"nextjs:deno-2.0.0",
+	"nodejs:23.1.0",
+	"nodejs:24.0.2",
+] as const;
+
+const ServiceTypeSchema = z.enum(ServiceTypes);
+
+const ServiceSchema = z.object({
+	nodeId: z.string().optional(),
+	type: ServiceTypeSchema,
+	name: z.string(),
+	source: z.object({
+		repository: z.string(),
+		branch: z.string(),
+		rootDir: z.string(),
+	}),
+	ports: z
+		.array(
+			z.object({
+				name: z.string(),
+				value: z.number(),
+				protocol: z.enum(["TCP", "UDP"]),
+			}),
+		)
+		.optional(),
+	env: z
+		.array(
+			z.object({
+				name: z.string(),
+				alias: z.string().optional(),
+			}),
+		)
+		.optional(),
+	ingress: z.array(IngressSchema).optional(),
+	expose: z.array(PortDomainSchema).optional(),
+	volume: z.array(z.string()).optional(),
+	preBuildCommands: z.array(z.object({ bin: z.string() })).optional(),
+	dev: z
+		.object({
+			enabled: z.boolean(),
+			username: z.string().optional(),
+			ssh: DomainSchema.optional(),
+			codeServer: DomainSchema.optional(),
+		})
+		.optional(),
+});
+
+const VolumeTypeSchema = z.enum(["ReadWriteOnce", "ReadOnlyMany", "ReadWriteMany", "ReadWriteOncePod"]);
+
+const VolumeSchema = z.object({
+	nodeId: z.string().optional(),
+	name: z.string(),
+	size: z.string(),
+	accessMode: VolumeTypeSchema,
+});
+
+const PostgreSQLSchema = z.object({
+	nodeId: z.string().optional(),
+	name: z.string(),
+	size: z.string(),
+	expose: z.array(DomainSchema).optional(),
+});
+
+const MongoDBSchema = z.object({
+	nodeId: z.string().optional(),
+	name: z.string(),
+	size: z.string(),
+	expose: z.array(DomainSchema).optional(),
+});
+
+export const ConfigSchema = z.object({
+	service: z.array(ServiceSchema).optional(),
+	volume: z.array(VolumeSchema).optional(),
+	postgresql: z.array(PostgreSQLSchema).optional(),
+	mongodb: z.array(MongoDBSchema).optional(),
+});
+
+export const InputSchema = z.object({
+	appId: z.string(),
+	managerAddr: z.string(),
+	key: z
+		.object({
+			public: z.string(),
+			private: z.string(),
+		})
+		.optional(),
+});
+
+export const ConfigWithInputSchema = ConfigSchema.extend({
+	input: InputSchema,
+});
+
+export type AuthDisabled = z.infer<typeof AuthDisabledSchema>;
+export type AuthEnabled = z.infer<typeof AuthEnabledSchema>;
+export type Auth = z.infer<typeof AuthSchema>;
+export type Ingress = z.infer<typeof IngressSchema>;
+export type Domain = z.infer<typeof DomainSchema>;
+export type PortValue = z.infer<typeof PortValueSchema>;
+export type PortDomain = z.infer<typeof PortDomainSchema>;
+export type ServiceType = z.infer<typeof ServiceTypeSchema>;
+export type Service = z.infer<typeof ServiceSchema>;
+export type VolumeType = z.infer<typeof VolumeTypeSchema>;
+export type Volume = z.infer<typeof VolumeSchema>;
+export type PostgreSQL = z.infer<typeof PostgreSQLSchema>;
+export type MongoDB = z.infer<typeof MongoDBSchema>;
+export type Config = z.infer<typeof ConfigSchema>;
+export type ConfigWithInput = z.infer<typeof ConfigWithInputSchema>;
diff --git a/apps/canvas/config/tsconfig.json b/apps/canvas/config/tsconfig.json
new file mode 100644
index 0000000..6d6881e
--- /dev/null
+++ b/apps/canvas/config/tsconfig.json
@@ -0,0 +1,105 @@
+{
+	"include": [
+		"src/**/*.ts"
+	],
+	"compilerOptions": {
+		/* Visit https://aka.ms/tsconfig to read more about this file */
+		"outDir": "dist",
+		/* Projects */
+		// "incremental": true,                              /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
+		// "composite": true,                                /* Enable constraints that allow a TypeScript project to be used with project references. */
+		// "tsBuildInfoFile": "./.tsbuildinfo",              /* Specify the path to .tsbuildinfo incremental compilation file. */
+		// "disableSourceOfProjectReferenceRedirect": true,  /* Disable preferring source files instead of declaration files when referencing composite projects. */
+		// "disableSolutionSearching": true,                 /* Opt a project out of multi-project reference checking when editing. */
+		// "disableReferencedProjectLoad": true,             /* Reduce the number of projects loaded automatically by TypeScript. */
+		/* Language and Environment */
+		"target": "es2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
+		// "lib": [],                                        /* Specify a set of bundled library declaration files that describe the target runtime environment. */
+		// "jsx": "preserve",                                /* Specify what JSX code is generated. */
+		// "experimentalDecorators": true,                   /* Enable experimental support for legacy experimental decorators. */
+		// "emitDecoratorMetadata": true,                    /* Emit design-type metadata for decorated declarations in source files. */
+		// "jsxFactory": "",                                 /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
+		// "jsxFragmentFactory": "",                         /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
+		// "jsxImportSource": "",                            /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
+		// "reactNamespace": "",                             /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
+		// "noLib": true,                                    /* Disable including any library files, including the default lib.d.ts. */
+		// "useDefineForClassFields": true,                  /* Emit ECMAScript-standard-compliant class fields. */
+		// "moduleDetection": "auto",                        /* Control what method is used to detect module-format JS files. */
+		/* Modules */
+		"module": "node16" /* Specify what module code is generated. */,
+		// "rootDir": "./",                                  /* Specify the root folder within your source files. */
+		"moduleResolution": "node16",                     /* Specify how TypeScript looks up a file from a given module specifier. */
+		"baseUrl": ".",                                  /* Specify the base directory to resolve non-relative module names. */
+		// "rootDirs": [],                                   /* Allow multiple folders to be treated as one when resolving modules. */
+		// "typeRoots": [],                                  /* Specify multiple folders that act like './node_modules/@types'. */
+		// "types": [],                                      /* Specify type package names to be included without being referenced in a source file. */
+		// "allowUmdGlobalAccess": true,                     /* Allow accessing UMD globals from modules. */
+		// "moduleSuffixes": [],                             /* List of file name suffixes to search when resolving a module. */
+		// "allowImportingTsExtensions": true,               /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
+		// "resolvePackageJsonExports": true,                /* Use the package.json 'exports' field when resolving package imports. */
+		// "resolvePackageJsonImports": true,                /* Use the package.json 'imports' field when resolving imports. */
+		// "customConditions": [],                           /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
+		// "noUncheckedSideEffectImports": true,             /* Check side effect imports. */
+		// "resolveJsonModule": true,                        /* Enable importing .json files. */
+		// "allowArbitraryExtensions": true,                 /* Enable importing files with any extension, provided a declaration file is present. */
+		// "noResolve": true,                                /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
+		/* JavaScript Support */
+		// "allowJs": true,                                  /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
+		// "checkJs": true,                                  /* Enable error reporting in type-checked JavaScript files. */
+		// "maxNodeModuleJsDepth": 1,                        /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
+		/* Emit */
+		"declaration": true,                              /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
+		// "declarationMap": true,                           /* Create sourcemaps for d.ts files. */
+		// "emitDeclarationOnly": true,                      /* Only output d.ts files and not JavaScript files. */
+		// "sourceMap": true,                                /* Create source map files for emitted JavaScript files. */
+		// "inlineSourceMap": true,                          /* Include sourcemap files inside the emitted JavaScript. */
+		// "noEmit": true,                                   /* Disable emitting files from a compilation. */
+		// "outFile": "./",                                  /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
+		// "outDir": "./",                                   /* Specify an output folder for all emitted files. */
+		// "removeComments": true,                           /* Disable emitting comments. */
+		// "importHelpers": true,                            /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
+		// "downlevelIteration": true,                       /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
+		// "sourceRoot": "",                                 /* Specify the root path for debuggers to find the reference source code. */
+		// "mapRoot": "",                                    /* Specify the location where debugger should locate map files instead of generated locations. */
+		// "inlineSources": true,                            /* Include source code in the sourcemaps inside the emitted JavaScript. */
+		// "emitBOM": true,                                  /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
+		// "newLine": "crlf",                                /* Set the newline character for emitting files. */
+		// "stripInternal": true,                            /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
+		// "noEmitHelpers": true,                            /* Disable generating custom helper functions like '__extends' in compiled output. */
+		// "noEmitOnError": true,                            /* Disable emitting files if any type checking errors are reported. */
+		// "preserveConstEnums": true,                       /* Disable erasing 'const enum' declarations in generated code. */
+		// "declarationDir": "./",                           /* Specify the output directory for generated declaration files. */
+		/* Interop Constraints */
+		// "isolatedModules": true,                          /* Ensure that each file can be safely transpiled without relying on other imports. */
+		// "verbatimModuleSyntax": true,                     /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
+		// "isolatedDeclarations": true,                     /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
+		// "allowSyntheticDefaultImports": true,             /* Allow 'import x from y' when a module doesn't have a default export. */
+		"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
+		// "preserveSymlinks": true,                         /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
+		"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
+		/* Type Checking */
+		"strict": true /* Enable all strict type-checking options. */,
+		// "noImplicitAny": true,                            /* Enable error reporting for expressions and declarations with an implied 'any' type. */
+		// "strictNullChecks": true,                         /* When type checking, take into account 'null' and 'undefined'. */
+		// "strictFunctionTypes": true,                      /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
+		// "strictBindCallApply": true,                      /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
+		// "strictPropertyInitialization": true,             /* Check for class properties that are declared but not set in the constructor. */
+		// "strictBuiltinIteratorReturn": true,              /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
+		// "noImplicitThis": true,                           /* Enable error reporting when 'this' is given the type 'any'. */
+		// "useUnknownInCatchVariables": true,               /* Default catch clause variables as 'unknown' instead of 'any'. */
+		// "alwaysStrict": true,                             /* Ensure 'use strict' is always emitted. */
+		// "noUnusedLocals": true,                           /* Enable error reporting when local variables aren't read. */
+		// "noUnusedParameters": true,                       /* Raise an error when a function parameter isn't read. */
+		// "exactOptionalPropertyTypes": true,               /* Interpret optional property types as written, rather than adding 'undefined'. */
+		// "noImplicitReturns": true,                        /* Enable error reporting for codepaths that do not explicitly return in a function. */
+		// "noFallthroughCasesInSwitch": true,               /* Enable error reporting for fallthrough cases in switch statements. */
+		// "noUncheckedIndexedAccess": true,                 /* Add 'undefined' to a type when accessed using an index. */
+		// "noImplicitOverride": true,                       /* Ensure overriding members in derived classes are marked with an override modifier. */
+		// "noPropertyAccessFromIndexSignature": true,       /* Enforces using indexed accessors for keys declared using an indexed type. */
+		// "allowUnusedLabels": true,                        /* Disable error reporting for unused labels. */
+		// "allowUnreachableCode": true,                     /* Disable error reporting for unreachable code. */
+		/* Completeness */
+		// "skipDefaultLibCheck": true,                      /* Skip type checking .d.ts files that are included with TypeScript. */
+		"skipLibCheck": true /* Skip type checking all .d.ts files. */
+	}
+}
\ No newline at end of file
diff --git a/apps/canvas/front/package-lock.json b/apps/canvas/front/package-lock.json
index cb422f6..0855cc5 100644
--- a/apps/canvas/front/package-lock.json
+++ b/apps/canvas/front/package-lock.json
@@ -29,6 +29,7 @@
 				"@xyflow/react": "^12.3.3",
 				"class-variance-authority": "^0.7.0",
 				"clsx": "^2.1.1",
+				"config": "file:../config",
 				"lucide-react": "^0.454.0",
 				"react": "^18.3.1",
 				"react-dom": "^18.3.1",
@@ -61,6 +62,20 @@
 				"watch": "^1.0.2"
 			}
 		},
+		"../config": {
+			"version": "1.0.0",
+			"license": "ISC",
+			"dependencies": {
+				"@xyflow/react": "^12.3.3",
+				"zod": "^3.24.4"
+			},
+			"devDependencies": {
+				"eslint": "^9.13.0",
+				"prettier": "3.5.3",
+				"typescript": "^5.8.3",
+				"typescript-eslint": "^8.11.0"
+			}
+		},
 		"node_modules/@alloc/quick-lru": {
 			"version": "5.2.0",
 			"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
@@ -4656,6 +4671,10 @@
 			"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
 			"dev": true
 		},
+		"node_modules/config": {
+			"resolved": "../config",
+			"link": true
+		},
 		"node_modules/convert-source-map": {
 			"version": "2.0.0",
 			"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
diff --git a/apps/canvas/front/package.json b/apps/canvas/front/package.json
index 43d1232..0b31f98 100644
--- a/apps/canvas/front/package.json
+++ b/apps/canvas/front/package.json
@@ -37,6 +37,7 @@
 		"@xyflow/react": "^12.3.3",
 		"class-variance-authority": "^0.7.0",
 		"clsx": "^2.1.1",
+		"config": "file:../config",
 		"lucide-react": "^0.454.0",
 		"react": "^18.3.1",
 		"react-dom": "^18.3.1",
diff --git a/apps/canvas/front/src/Config.tsx b/apps/canvas/front/src/Config.tsx
index bdad346..eb024ea 100644
--- a/apps/canvas/front/src/Config.tsx
+++ b/apps/canvas/front/src/Config.tsx
@@ -1,5 +1,5 @@
 import { useStateStore } from "./lib/state";
-import { generateDodoConfig } from "./lib/config";
+import { generateDodoConfig } from "../../config/src/config";
 import JSONView from "@microlink/react-json-view";
 import { useMemo } from "react";
 
diff --git a/apps/canvas/front/src/components/actions.tsx b/apps/canvas/front/src/components/actions.tsx
index d4bd5ea..1442b9a 100644
--- a/apps/canvas/front/src/components/actions.tsx
+++ b/apps/canvas/front/src/components/actions.tsx
@@ -1,7 +1,7 @@
-import { AppNode, nodeLabelFull, useEnv, useMessages, useProjectId, useStateStore } from "@/lib/state";
+import { nodeLabelFull, useEnv, useMessages, useProjectId, useStateStore } from "@/lib/state";
 import { Button } from "./ui/button";
 import { useCallback, useEffect, useState } from "react";
-import { generateDodoConfig } from "@/lib/config";
+import { generateDodoConfig, AppNode } from "config";
 import { useNodes, useReactFlow } from "@xyflow/react";
 import { useToast } from "@/hooks/use-toast";
 import {
@@ -148,9 +148,9 @@
 			method: "GET",
 		});
 		const inst = await resp.json();
-		const { x = 0, y = 0, zoom = 1 } = inst.viewport;
-		store.setNodes(inst.nodes || []);
-		store.setEdges(inst.edges || []);
+		const { x = 0, y = 0, zoom = 1 } = inst.state.viewport;
+		store.setNodes(inst.state.nodes || []);
+		store.setEdges(inst.state.edges || []);
 		instance.setViewport({ x, y, zoom });
 	}, [projectId, instance, store]);
 	const clear = useCallback(() => {
diff --git a/apps/canvas/front/src/components/import-modal.tsx b/apps/canvas/front/src/components/import-modal.tsx
index ea9a06c..44f66d5 100644
--- a/apps/canvas/front/src/components/import-modal.tsx
+++ b/apps/canvas/front/src/components/import-modal.tsx
@@ -11,9 +11,6 @@
 	useGithubRepositoriesLoading,
 	useGithubRepositoriesError,
 	useFetchGithubRepositories,
-	serviceAnalyzisSchema,
-	ServiceType,
-	ServiceData,
 	useStateStore,
 } from "@/lib/state";
 import { Alert, AlertDescription } from "./ui/alert";
@@ -24,6 +21,7 @@
 import { Switch } from "./ui/switch";
 import { Label } from "./ui/label";
 import { useToast } from "@/hooks/use-toast";
+import { serviceAnalyzisSchema, ServiceType, ServiceData } from "config";
 
 const schema = z.object({
 	repositoryId: z.number().optional(),
diff --git a/apps/canvas/front/src/components/node-app.tsx b/apps/canvas/front/src/components/node-app.tsx
index d9eea6d..7eb632c 100644
--- a/apps/canvas/front/src/components/node-app.tsx
+++ b/apps/canvas/front/src/components/node-app.tsx
@@ -1,20 +1,7 @@
 import { v4 as uuidv4 } from "uuid";
 import { NodeRect } from "./node-rect";
-import {
-	useStateStore,
-	ServiceNode,
-	ServiceTypes,
-	nodeLabel,
-	BoundEnvVar,
-	AppState,
-	nodeIsConnectable,
-	GatewayTCPNode,
-	GatewayHttpsNode,
-	AppNode,
-	GithubNode,
-	useEnv,
-	useGithubRepositories,
-} from "@/lib/state";
+import { useStateStore, nodeLabel, AppState, nodeIsConnectable, useEnv, useGithubRepositories } from "@/lib/state";
+import { ServiceNode, ServiceTypes } from "config";
 import { KeyboardEvent, FocusEvent, useCallback, useEffect, useMemo, useState } from "react";
 import { z } from "zod";
 import { useForm, EventType, DeepPartial } from "react-hook-form";
diff --git a/apps/canvas/front/src/lib/config.ts b/apps/canvas/front/src/lib/config.ts
index 513f1f0..39db5b4 100644
--- a/apps/canvas/front/src/lib/config.ts
+++ b/apps/canvas/front/src/lib/config.ts
@@ -1,235 +1,5 @@
-import { AppNode, Env, Message, MessageType, NodeType, ServiceType, VolumeType } from "./state";
-
-export type AuthDisabled = {
-	enabled: false;
-};
-
-export type AuthEnabled = {
-	enabled: true;
-	groups: string[];
-	noAuthPathPatterns: string[];
-};
-
-export type Auth = AuthDisabled | AuthEnabled;
-
-export type Ingress = {
-	network: string;
-	subdomain: string;
-	port: { name: string } | { value: string };
-	auth: Auth;
-};
-
-export type Domain = {
-	network: string;
-	subdomain: string;
-};
-
-export type PortValue =
-	| {
-			name: string;
-	  }
-	| {
-			value: number;
-	  };
-
-export type PortDomain = Domain & {
-	port: PortValue;
-};
-
-export type Service = {
-	type: ServiceType;
-	name: string;
-	source: {
-		repository: string;
-		branch: string;
-		rootDir: string;
-	};
-	ports?: {
-		name: string;
-		value: number;
-		protocol: "TCP" | "UDP";
-	}[];
-	env?: {
-		name: string;
-		alias?: string;
-	}[];
-	ingress?: Ingress[];
-	expose?: PortDomain[];
-	volume?: string[];
-	preBuildCommands?: { bin: string }[];
-	dev?: {
-		enabled: boolean;
-		username?: string;
-		ssh?: Domain;
-		codeServer?: Domain;
-	};
-};
-
-export type Volume = {
-	name: string;
-	accessMode: VolumeType;
-	size: string;
-};
-
-export type PostgreSQL = {
-	name: string;
-	size: string;
-	expose?: Domain[];
-};
-
-export type MongoDB = {
-	name: string;
-	size: string;
-	expose?: Domain[];
-};
-
-export type Config = {
-	input: {
-		appId: string;
-		managerAddr: string;
-	};
-	service?: Service[];
-	volume?: Volume[];
-	postgresql?: PostgreSQL[];
-	mongodb?: MongoDB[];
-};
-
-export function generateDodoConfig(appId: string | undefined, nodes: AppNode[], env: Env): Config | null {
-	try {
-		if (appId == null || env.managerAddr == null) {
-			return null;
-		}
-		const networkMap = new Map(env.networks.map((n) => [n.domain, n.name]));
-		const ingressNodes = nodes
-			.filter((n) => n.type === "gateway-https")
-			.filter((n) => n.data.https !== undefined && !n.data.readonly);
-		const tcpNodes = nodes
-			.filter((n) => n.type === "gateway-tcp")
-			.filter((n) => n.data.exposed !== undefined && !n.data.readonly);
-		const findExpose = (n: AppNode): PortDomain[] => {
-			return n.data.ports
-				.map((p) => [n.id, p.id, p.name])
-				.flatMap((sp) => {
-					return tcpNodes.flatMap((i) =>
-						(i.data.exposed || [])
-							.filter((t) => t.serviceId === sp[0] && t.portId === sp[1])
-							.map(() => ({
-								network: networkMap.get(i.data.network!)!,
-								subdomain: i.data.subdomain!,
-								port: { name: sp[2] },
-							})),
-					);
-				});
-		};
-		return {
-			input: {
-				appId: appId,
-				managerAddr: env.managerAddr,
-			},
-			service: nodes
-				.filter((n) => n.type === "app")
-				.map((n): Service => {
-					return {
-						type: n.data.type,
-						name: n.data.label,
-						source: {
-							repository: nodes
-								.filter((i) => i.type === "github")
-								.find((i) => i.id === n.data.repository.id)!.data.repository!.sshURL,
-							branch: n.data.repository.branch,
-							rootDir: n.data.repository.rootDir,
-						},
-						ports: (n.data.ports || [])
-							.filter((p) => !n.data.dev?.enabled || (p.value != 22 && p.value != 9090))
-							.map((p) => ({
-								name: p.name,
-								value: p.value,
-								protocol: "TCP", // TODO(gio)
-							})),
-						env: (n.data.envVars || [])
-							.filter((e) => "name" in e)
-							.map((e) => ({
-								name: e.name,
-								alias: "alias" in e ? e.alias : undefined,
-							})),
-						ingress: ingressNodes
-							.filter((i) => i.data.https!.serviceId === n.id)
-							.map(
-								(i): Ingress => ({
-									network: networkMap.get(i.data.network!)!,
-									subdomain: i.data.subdomain!,
-									port: {
-										name: n.data.ports.find((p) => p.id === i.data.https!.portId)!.name,
-									},
-									auth:
-										i.data.auth?.enabled || false
-											? {
-													enabled: true,
-													groups: i.data.auth!.groups,
-													noAuthPathPatterns: i.data.auth!.noAuthPathPatterns,
-												}
-											: {
-													enabled: false,
-												},
-								}),
-							),
-						expose: findExpose(n),
-						preBuildCommands: n.data.preBuildCommands
-							? n.data.preBuildCommands.split("\n").map((cmd) => ({ bin: cmd }))
-							: [],
-						dev: {
-							enabled: n.data.dev ? n.data.dev.enabled : false,
-							username: n.data.dev && n.data.dev.enabled ? env.user.username : undefined,
-							codeServer:
-								n.data.dev?.enabled && n.data.dev.expose != null
-									? {
-											network: networkMap.get(n.data.dev.expose.network)!,
-											subdomain: n.data.dev.expose.subdomain,
-										}
-									: undefined,
-							ssh:
-								n.data.dev?.enabled && n.data.dev.expose != null
-									? {
-											network: networkMap.get(n.data.dev.expose.network)!,
-											subdomain: n.data.dev.expose.subdomain,
-										}
-									: undefined,
-						},
-					};
-				}),
-			volume: nodes
-				.filter((n) => n.type === "volume")
-				.map(
-					(n): Volume => ({
-						name: n.data.label,
-						accessMode: n.data.type,
-						size: n.data.size,
-					}),
-				),
-			postgresql: nodes
-				.filter((n) => n.type === "postgresql")
-				.map(
-					(n): PostgreSQL => ({
-						name: n.data.label,
-						size: "1Gi", // TODO(gio)
-						expose: findExpose(n).map((e) => ({ network: e.network, subdomain: e.subdomain })),
-					}),
-				),
-			mongodb: nodes
-				.filter((n) => n.type === "mongodb")
-				.map(
-					(n): MongoDB => ({
-						name: n.data.label,
-						size: "1Gi", // TODO(gio)
-						expose: findExpose(n).map((e) => ({ network: e.network, subdomain: e.subdomain })),
-					}),
-				),
-		};
-	} catch (e) {
-		console.log(e);
-		return null;
-	}
-}
+import { AppNode, NodeType } from "config";
+import { Message, MessageType } from "./state";
 
 export interface Validator {
 	(nodes: AppNode[]): Message[];
@@ -347,7 +117,7 @@
 				}) satisfies Message,
 		);
 	const noApp = git
-		.filter((n) => !nodes.some((i) => i.type === "app" && i.data?.repository?.id === n.id))
+		.filter((n) => !nodes.some((i) => i.type === "app" && i.data?.repository?.repoNodeId === n.id))
 		.map(
 			(n) =>
 				({
@@ -383,7 +153,7 @@
 			}),
 		);
 	const noSource = apps
-		.filter((n) => n.data == null || n.data.repository == null || n.data.repository.id === "")
+		.filter((n) => n.data == null || n.data.repository == null || n.data.repository.repoNodeId === "")
 		.map(
 			(n): Message => ({
 				id: `${n.id}-no-repo`,
diff --git a/apps/canvas/front/src/lib/state.ts b/apps/canvas/front/src/lib/state.ts
index a0e4e91..8ada938 100644
--- a/apps/canvas/front/src/lib/state.ts
+++ b/apps/canvas/front/src/lib/state.ts
@@ -1,7 +1,7 @@
 import { Category, defaultCategories } from "./categories";
 import { CreateValidators, Validator } from "./config";
 import { GitHubService, GitHubServiceImpl, GitHubRepository } from "./github";
-import type { Edge, Node, OnConnect, OnEdgesChange, OnNodesChange, Viewport as ReactFlowViewport } from "@xyflow/react";
+import type { Edge, OnConnect, OnEdgesChange, OnNodesChange, Viewport as ReactFlowViewport } from "@xyflow/react";
 import {
 	addEdge,
 	applyEdgeChanges,
@@ -13,217 +13,8 @@
 } from "@xyflow/react";
 import type { DeepPartial } from "react-hook-form";
 import { v4 as uuidv4 } from "uuid";
-import { z } from "zod";
 import { create } from "zustand";
-
-export const serviceAnalyzisSchema = z.object({
-	name: z.string(),
-	location: z.string(),
-	configVars: z.array(
-		z.object({
-			name: z.string(),
-			category: z.enum(["CommandLineFlag", "EnvironmentVariable"]),
-			type: z.optional(z.enum(["String", "Number", "Boolean"])),
-			semanticType: z.optional(
-				z.enum([
-					"EXPANDED_ENV_VAR",
-					"PORT",
-					"FILESYSTEM_PATH",
-					"DATABASE_URL",
-					"SQLITE_PATH",
-					"POSTGRES_URL",
-					"POSTGRES_PASSWORD",
-					"POSTGRES_USER",
-					"POSTGRES_DB",
-					"POSTGRES_PORT",
-					"POSTGRES_HOST",
-					"POSTGRES_SSL",
-					"MONGO_URL",
-					"MONGO_PASSWORD",
-					"MONGO_USER",
-					"MONGO_DB",
-					"MONGO_PORT",
-					"MONGO_HOST",
-					"MONGO_SSL",
-				]),
-			),
-		}),
-	),
-});
-
-export type InitData = {
-	label: string;
-	envVars: BoundEnvVar[];
-	ports: Port[];
-};
-
-export type NodeData = InitData & {
-	activeField?: string | undefined;
-	state?: string | null;
-};
-
-export type PortConnectedTo = {
-	serviceId: string;
-	portId: string;
-};
-
-export type NetworkData = NodeData & {
-	domain: string;
-};
-
-export type NetworkNode = Node<NetworkData> & {
-	type: "network";
-};
-
-export type GatewayHttpsData = NodeData & {
-	readonly?: boolean;
-	network?: string;
-	subdomain?: string;
-	https?: PortConnectedTo;
-	auth?: {
-		enabled: boolean;
-		groups: string[];
-		noAuthPathPatterns: string[];
-	};
-};
-
-export type GatewayHttpsNode = Node<GatewayHttpsData> & {
-	type: "gateway-https";
-};
-
-export type GatewayTCPData = NodeData & {
-	readonly?: boolean;
-	network?: string;
-	subdomain?: string;
-	exposed: PortConnectedTo[];
-	selected?: {
-		serviceId?: string;
-		portId?: string;
-	};
-};
-
-export type GatewayTCPNode = Node<GatewayTCPData> & {
-	type: "gateway-tcp";
-};
-
-export type Port = {
-	id: string;
-	name: string;
-	value: number;
-};
-
-export const ServiceTypes = [
-	"deno:2.2.0",
-	"golang:1.20.0",
-	"golang:1.22.0",
-	"golang:1.24.0",
-	"hugo:latest",
-	"php:8.2-apache",
-	"nextjs:deno-2.0.0",
-	"nodejs:23.1.0",
-	"nodejs:24.0.2",
-] as const;
-export type ServiceType = (typeof ServiceTypes)[number];
-
-export type Domain = {
-	network: string;
-	subdomain: string;
-};
-
-export type ServiceData = NodeData & {
-	type: ServiceType;
-	repository?:
-		| {
-				id: number;
-				repoNodeId: string;
-		  }
-		| {
-				id: number;
-				repoNodeId: string;
-				branch: string;
-		  }
-		| {
-				id: number;
-				repoNodeId: string;
-				branch: string;
-				rootDir: string;
-		  };
-	env: string[];
-	volume: string[];
-	preBuildCommands: string;
-	isChoosingPortToConnect: boolean;
-	dev?:
-		| {
-				enabled: false;
-				expose?: Domain;
-		  }
-		| {
-				enabled: true;
-				expose?: Domain;
-				codeServerNodeId: string;
-				sshNodeId: string;
-		  };
-	info?: z.infer<typeof serviceAnalyzisSchema>;
-};
-
-export type ServiceNode = Node<ServiceData> & {
-	type: "app";
-};
-
-export type VolumeType = "ReadWriteOnce" | "ReadOnlyMany" | "ReadWriteMany" | "ReadWriteOncePod";
-
-export type VolumeData = NodeData & {
-	type: VolumeType;
-	size: string;
-	attachedTo: string[];
-};
-
-export type VolumeNode = Node<VolumeData> & {
-	type: "volume";
-};
-
-export type PostgreSQLData = NodeData & {
-	volumeId: string;
-};
-
-export type PostgreSQLNode = Node<PostgreSQLData> & {
-	type: "postgresql";
-};
-
-export type MongoDBData = NodeData & {
-	volumeId: string;
-};
-
-export type MongoDBNode = Node<MongoDBData> & {
-	type: "mongodb";
-};
-
-export type GithubData = NodeData & {
-	repository?: {
-		id: number;
-		sshURL: string;
-		fullName: string;
-	};
-};
-
-export type GithubNode = Node<GithubData> & {
-	type: "github";
-};
-
-export type NANode = Node<NodeData> & {
-	type: undefined;
-};
-
-export type AppNode =
-	| NetworkNode
-	| GatewayHttpsNode
-	| GatewayTCPNode
-	| ServiceNode
-	| VolumeNode
-	| PostgreSQLNode
-	| MongoDBNode
-	| GithubNode
-	| NANode;
+import { AppNode, Env, NodeType, VolumeNode, GatewayTCPData, envSchema } from "config";
 
 export function nodeLabel(n: AppNode): string {
 	try {
@@ -319,38 +110,6 @@
 	}
 }
 
-export type BoundEnvVar =
-	| {
-			id: string;
-			source: string | null;
-	  }
-	| {
-			id: string;
-			source: string | null;
-			name: string;
-			isEditting: boolean;
-	  }
-	| {
-			id: string;
-			source: string | null;
-			name: string;
-			alias: string;
-			isEditting: boolean;
-	  }
-	| {
-			id: string;
-			source: string | null;
-			portId: string;
-			name: string;
-			alias: string;
-			isEditting: boolean;
-	  };
-
-export type EnvVar = {
-	name: string;
-	value: string;
-};
-
 export function nodeEnvVarNamePort(n: AppNode, portName: string): string {
 	return `DODO_SERVICE_${n.data.label.toUpperCase()}_ADDRESS_${portName.toUpperCase()}`;
 }
@@ -381,8 +140,6 @@
 	}
 }
 
-export type NodeType = Exclude<Pick<AppNode, "type">["type"], undefined>;
-
 export type MessageType = "INFO" | "WARNING" | "FATAL";
 
 export type Message = {
@@ -395,100 +152,6 @@
 	onClick?: (state: AppState) => void;
 };
 
-export const accessSchema = z.discriminatedUnion("type", [
-	z.object({
-		type: z.literal("https"),
-		name: z.string(),
-		address: z.string(),
-	}),
-	z.object({
-		type: z.literal("ssh"),
-		name: z.string(),
-		host: z.string(),
-		port: z.number(),
-	}),
-	z.object({
-		type: z.literal("tcp"),
-		name: z.string(),
-		host: z.string(),
-		port: z.number(),
-	}),
-	z.object({
-		type: z.literal("udp"),
-		name: z.string(),
-		host: z.string(),
-		port: z.number(),
-	}),
-	z.object({
-		type: z.literal("postgresql"),
-		name: z.string(),
-		host: z.string(),
-		port: z.number(),
-		database: z.string(),
-		username: z.string(),
-		password: z.string(),
-	}),
-	z.object({
-		type: z.literal("mongodb"),
-		name: z.string(),
-		host: z.string(),
-		port: z.number(),
-		database: z.string(),
-		username: z.string(),
-		password: z.string(),
-	}),
-]);
-
-export const serviceInfoSchema = z.object({
-	name: z.string(),
-	workers: z.array(
-		z.object({
-			id: z.string(),
-			commit: z.optional(
-				z.object({
-					hash: z.string(),
-					message: z.string(),
-				}),
-			),
-			commands: z.optional(
-				z.array(
-					z.object({
-						command: z.string(),
-						state: z.string(),
-					}),
-				),
-			),
-		}),
-	),
-});
-
-export const envSchema = z.object({
-	managerAddr: z.optional(z.string().min(1)),
-	instanceId: z.optional(z.string().min(1)),
-	deployKeyPublic: z.optional(z.nullable(z.string().min(1))),
-	networks: z
-		.array(
-			z.object({
-				name: z.string().min(1),
-				domain: z.string().min(1),
-				hasAuth: z.boolean(),
-			}),
-		)
-		.default([]),
-	integrations: z.object({
-		github: z.boolean(),
-	}),
-	services: z.array(serviceInfoSchema),
-	user: z.object({
-		id: z.string(),
-		username: z.string(),
-	}),
-	access: z.array(accessSchema),
-});
-
-export type ServiceInfo = z.infer<typeof serviceInfoSchema>;
-export type Env = z.infer<typeof envSchema>;
-
 const defaultEnv: Env = {
 	managerAddr: undefined,
 	deployKeyPublic: undefined,
@@ -729,15 +392,15 @@
 			method: "GET",
 		});
 		const inst = await resp.json();
-		setN(inst.nodes);
-		set({ edges: inst.edges });
+		setN(inst.state.nodes);
+		set({ edges: inst.state.edges });
 		injectNetworkNodes();
 		if (
-			get().zoom.x !== inst.viewport.x ||
-			get().zoom.y !== inst.viewport.y ||
-			get().zoom.zoom !== inst.viewport.zoom
+			get().zoom.x !== inst.state.viewport.x ||
+			get().zoom.y !== inst.state.viewport.y ||
+			get().zoom.zoom !== inst.state.viewport.zoom
 		) {
-			set({ zoom: inst.viewport });
+			set({ zoom: inst.state.viewport });
 		}
 	};
 
diff --git a/apps/canvas/front/tsconfig.app.json b/apps/canvas/front/tsconfig.app.json
index 6c2d8de..803c90a 100644
--- a/apps/canvas/front/tsconfig.app.json
+++ b/apps/canvas/front/tsconfig.app.json
@@ -27,5 +27,5 @@
 			"@/*": ["./src/*"]
 		}
 	},
-	"include": ["src"]
+	"include": ["src", "../config/src/config.ts"]
 }