diff --git a/.gitignore b/.gitignore index 921cc58..2e4d176 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ logs/ *.db *.db-* .DS_Store +packages/web/dist diff --git a/README.md b/README.md new file mode 100644 index 0000000..8d9605d --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +# scrunner + +Workflow runner with a sandboxed script engine, SQLite run history, and an admin UI. + +## Packages + +- `packages/server` — Fastify runner, HTTP/cron triggers, admin REST API +- `packages/web` — React admin UI (Vite, DaisyUI, React Query) + +## Setup + +```bash +pnpm install +pnpm dev +``` + +- API: http://localhost:9000 +- UI (dev): http://localhost:5173 + +The first account created becomes **admin**. Later accounts are created from Users. + +## Scripts + +| Command | Description | +|---|---| +| `pnpm dev` | Server + Vite together | +| `pnpm dev:server` | API/runner only | +| `pnpm dev:web` | UI only (proxies `/api` to :9000) | +| `pnpm build` | Production UI build | +| `pnpm start` | Serve API and built UI from :9000 | +| `pnpm migrate` | Apply SQLite migrations | + +## Environment + +| Variable | Default | Notes | +|---|---|---| +| `SCRUNNER_JWT_SECRET` | `scrunner-dev-secret` (dev only) | **Required in production** | +| `SCRUNNER_DB_PATH` | `packages/server/data/scrunner.db` | SQLite file | +| `SCRUNNER_LOG_LEVEL` | `debug` | Pino level | +| `SCRUNNER_RETENTION_DAYS` | `30` | Run history prune | +| `SCRUNNER_CORS_ORIGIN` | `http://localhost:5173` | Vite origin in dev | +| `PORT` | `9000` | HTTP port | +| `NODE_ENV` | — | Set `production` for secure cookies | + +## Production + +```bash +pnpm install +pnpm build +SCRUNNER_JWT_SECRET=... NODE_ENV=production pnpm start +``` + +The server serves `packages/web/dist` when that folder exists. diff --git a/package.json b/package.json index b9e7b28..dacf15f 100644 --- a/package.json +++ b/package.json @@ -1,30 +1,22 @@ { "name": "scrunner", "version": "1.0.0", - "description": "", - "main": "index.js", + "private": true, + "description": "Script/workflow runner with admin UI", "type": "module", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "dev": "pnpm --filter @scrunner/server --filter @scrunner/web --parallel dev", + "dev:server": "pnpm --filter @scrunner/server dev", + "dev:web": "pnpm --filter @scrunner/web dev", + "build": "pnpm --filter @scrunner/web build", + "start": "pnpm --filter @scrunner/server start", + "migrate": "pnpm --filter @scrunner/server migrate" }, - "keywords": [], - "author": "", - "license": "ISC", "packageManager": "pnpm@10.25.0", "pnpm": { "onlyBuiltDependencies": [ - "better-sqlite3" + "better-sqlite3", + "esbuild" ] - }, - "dependencies": { - "axios": "^1.19.0", - "better-sqlite3": "^13.0.3", - "fastify": "^5.12.0", - "jsonata": "^2.2.2", - "knex": "^3.3.0", - "node-cron": "^4.6.0", - "pino": "^10.3.1", - "pino-roll": "^4.0.0", - "yaml": "^2.9.0" } -} \ No newline at end of file +} diff --git a/packages/server/docs/mt.http b/packages/server/docs/mt.http index eed9934..92f846c 100644 --- a/packages/server/docs/mt.http +++ b/packages/server/docs/mt.http @@ -4,32 +4,38 @@ Content-Type: application/json 0 -HTTP/1.1 200 - OK -content-type: application/json; charset=utf-8 -content-length: 73 -date: Fri, 14 Aug 2026 04:34:56 GMT -connection: close ### POST http://localhost:9000/u/default/time-to-ntfy Content-Type: application/json {} -HTTP/1.1 500 - Internal Server Error -content-type: application/json; charset=utf-8 -content-length: 97 -date: Fri, 14 Aug 2026 04:11:52 GMT -connection: close -### -GET http://localhost:9000/admin/runs?owner=default&limit=20 -### -POST http://localhost:9000/admin/workflows/reregister +### Auth bootstrap +GET http://localhost:9000/api/auth/bootstrap + +### Login +POST http://localhost:9000/api/auth/login +Content-Type: application/json + +{ + "username": "admin", + "password": "changeme1" +} + +### Dashboard +GET http://localhost:9000/api/dashboard + +### Runs +GET http://localhost:9000/api/runs?owner=default&limit=20 + +### Reregister +POST http://localhost:9000/api/workflows/reregister Content-Type: application/json {} -HTTP/1.1 200 - OK -content-type: application/json; charset=utf-8 -content-length: 33 -date: Fri, 14 Aug 2026 04:34:22 GMT -connection: close \ No newline at end of file +### Run workflow manually +POST http://localhost:9000/api/workflows/default/manual-trigger.yaml/run +Content-Type: application/json + +{} diff --git a/packages/server/fs-store.js b/packages/server/fs-store.js new file mode 100644 index 0000000..23061d1 --- /dev/null +++ b/packages/server/fs-store.js @@ -0,0 +1,128 @@ +import fs from "fs"; +import path from "path"; +import yaml from "yaml"; +import { SCRIPTS_DIR, WORKFLOWS_DIR } from "./paths.js"; + +export function assertScriptName(name) { + if (typeof name !== "string" || !/^[A-Za-z0-9._-]+\.js$/.test(name)) { + const err = new Error("invalid script name"); + err.statusCode = 400; + throw err; + } + return name; +} + +export function assertOwner(name) { + if (typeof name !== "string" || !/^[A-Za-z0-9_-]+$/.test(name)) { + const err = new Error("invalid owner"); + err.statusCode = 400; + throw err; + } + return name; +} + +export function assertWorkflowFile(name) { + if ( + typeof name !== "string" || + !/^[A-Za-z0-9._-]+\.ya?ml$/.test(name) || + name === "registers.yaml" + ) { + const err = new Error("invalid workflow file"); + err.statusCode = 400; + throw err; + } + return name; +} + +export function listScriptFiles() { + if (!fs.existsSync(SCRIPTS_DIR)) return []; + return fs + .readdirSync(SCRIPTS_DIR) + .filter((f) => f.endsWith(".js")) + .sort(); +} + +export function readScript(name) { + assertScriptName(name); + const filePath = path.join(SCRIPTS_DIR, name); + if (!fs.existsSync(filePath)) return null; + return fs.readFileSync(filePath, "utf8"); +} + +export function writeScript(name, content) { + assertScriptName(name); + fs.mkdirSync(SCRIPTS_DIR, { recursive: true }); + fs.writeFileSync(path.join(SCRIPTS_DIR, name), content, "utf8"); +} + +export function deleteScript(name) { + assertScriptName(name); + const filePath = path.join(SCRIPTS_DIR, name); + if (!fs.existsSync(filePath)) return false; + fs.unlinkSync(filePath); + return true; +} + +export function listOwners() { + if (!fs.existsSync(WORKFLOWS_DIR)) return []; + return fs + .readdirSync(WORKFLOWS_DIR, { withFileTypes: true }) + .filter((d) => d.isDirectory()) + .map((d) => d.name) + .sort(); +} + +function registersPath(owner) { + return path.join(WORKFLOWS_DIR, owner, "registers.yaml"); +} + +export function readRegisters(owner) { + const filePath = registersPath(owner); + if (!fs.existsSync(filePath)) return []; + const parsed = yaml.parse(fs.readFileSync(filePath, "utf8")) ?? {}; + return Array.isArray(parsed.scripts) ? parsed.scripts : []; +} + +export function writeRegisters(owner, files) { + const ownerDir = path.join(WORKFLOWS_DIR, owner); + fs.mkdirSync(ownerDir, { recursive: true }); + fs.writeFileSync( + registersPath(owner), + yaml.stringify({ scripts: files }), + "utf8", + ); +} + +export function readWorkflowYaml(owner, file) { + assertOwner(owner); + assertWorkflowFile(file); + const filePath = path.join(WORKFLOWS_DIR, owner, file); + if (!fs.existsSync(filePath)) return null; + return fs.readFileSync(filePath, "utf8"); +} + +export function writeWorkflowYaml(owner, file, content) { + assertOwner(owner); + assertWorkflowFile(file); + const ownerDir = path.join(WORKFLOWS_DIR, owner); + fs.mkdirSync(ownerDir, { recursive: true }); + fs.writeFileSync(path.join(ownerDir, file), content, "utf8"); +} + +export function deleteWorkflowYaml(owner, file) { + assertOwner(owner); + assertWorkflowFile(file); + const filePath = path.join(WORKFLOWS_DIR, owner, file); + if (!fs.existsSync(filePath)) return false; + fs.unlinkSync(filePath); + return true; +} + +export function listOwnerYamlFiles(owner) { + const ownerDir = path.join(WORKFLOWS_DIR, owner); + if (!fs.existsSync(ownerDir)) return []; + return fs + .readdirSync(ownerDir) + .filter((f) => f.endsWith(".yaml") && f !== "registers.yaml") + .sort(); +} diff --git a/packages/server/knexfile.js b/packages/server/knexfile.js index 6016e81..eed0814 100644 --- a/packages/server/knexfile.js +++ b/packages/server/knexfile.js @@ -1,7 +1,8 @@ import fs from "fs"; import path from "path"; +import { DATA_DIR, SERVER_ROOT } from "./paths.js"; -const dbPath = process.env.SCRUNNER_DB_PATH ?? path.resolve("data/scrunner.db"); +const dbPath = process.env.SCRUNNER_DB_PATH ?? path.join(DATA_DIR, "scrunner.db"); fs.mkdirSync(path.dirname(dbPath), { recursive: true }); /** @type {import("knex").Knex.Config} */ @@ -12,7 +13,7 @@ const config = { }, useNullAsDefault: true, migrations: { - directory: "./migrations", + directory: path.join(SERVER_ROOT, "migrations"), extension: "js", loadExtensions: [".js"], }, diff --git a/packages/server/logger.js b/packages/server/logger.js index 5c79427..01a4ee5 100644 --- a/packages/server/logger.js +++ b/packages/server/logger.js @@ -2,6 +2,7 @@ import fs from "fs"; import path from "path"; import pino from "pino"; import * as store from "./store.js"; +import { LOGS_DIR } from "./paths.js"; const LEVEL_TO_NUM = { trace: 10, @@ -87,12 +88,12 @@ const sqliteStream = { }, }; -fs.mkdirSync("logs", { recursive: true }); +fs.mkdirSync(LOGS_DIR, { recursive: true }); const rollingFile = pino.transport({ target: "pino-roll", options: { - file: path.resolve("logs/scrunner.log"), + file: path.join(LOGS_DIR, "scrunner.log"), size: "10m", mkdir: true, limit: { count: 5 }, diff --git a/packages/server/migrations/20260814120000_users.js b/packages/server/migrations/20260814120000_users.js new file mode 100644 index 0000000..c30e307 --- /dev/null +++ b/packages/server/migrations/20260814120000_users.js @@ -0,0 +1,20 @@ +/** + * @param {import("knex").Knex} knex + */ +export async function up(knex) { + await knex.schema.createTable("users", (t) => { + t.text("id").primary(); + t.text("username").notNullable().unique(); + t.text("password_hash").notNullable(); + t.text("role").notNullable(); + t.text("created_at").notNullable(); + t.text("updated_at").notNullable(); + }); +} + +/** + * @param {import("knex").Knex} knex + */ +export async function down(knex) { + await knex.schema.dropTableIfExists("users"); +} diff --git a/packages/server/package.json b/packages/server/package.json new file mode 100644 index 0000000..1ac21fa --- /dev/null +++ b/packages/server/package.json @@ -0,0 +1,28 @@ +{ + "name": "@scrunner/server", + "version": "1.0.0", + "private": true, + "type": "module", + "main": "runner.js", + "scripts": { + "dev": "node --watch runner.js", + "start": "node runner.js", + "migrate": "node -e \"import('./db.js').then((m) => m.migrate().then(() => process.exit(0)))\"" + }, + "dependencies": { + "@fastify/cookie": "^11.0.2", + "@fastify/cors": "^11.1.0", + "@fastify/jwt": "^9.1.0", + "@fastify/static": "^8.2.0", + "axios": "^1.19.0", + "bcryptjs": "^3.0.2", + "better-sqlite3": "^13.0.3", + "fastify": "^5.12.0", + "jsonata": "^2.2.2", + "knex": "^3.3.0", + "node-cron": "^4.6.0", + "pino": "^10.3.1", + "pino-roll": "^4.0.0", + "yaml": "^2.9.0" + } +} diff --git a/packages/server/paths.js b/packages/server/paths.js new file mode 100644 index 0000000..f84f6a5 --- /dev/null +++ b/packages/server/paths.js @@ -0,0 +1,9 @@ +import path from "path"; +import { fileURLToPath } from "url"; + +export const SERVER_ROOT = path.dirname(fileURLToPath(import.meta.url)); +export const SCRIPTS_DIR = path.join(SERVER_ROOT, "scripts"); +export const WORKFLOWS_DIR = path.join(SERVER_ROOT, "workflows"); +export const DATA_DIR = path.join(SERVER_ROOT, "data"); +export const LOGS_DIR = path.join(SERVER_ROOT, "logs"); +export const WEB_DIST = path.resolve(SERVER_ROOT, "../web/dist"); diff --git a/packages/server/registry.js b/packages/server/registry.js new file mode 100644 index 0000000..11e972b --- /dev/null +++ b/packages/server/registry.js @@ -0,0 +1,288 @@ +import fs from "fs"; +import path from "path"; +import yaml from "yaml"; +import cron from "node-cron"; +import { WORKFLOWS_DIR } from "./paths.js"; +import { log } from "./logger.js"; +import * as store from "./store.js"; +import { clearScriptCache, runScript } from "./script-sandbox.js"; +import { namespacedPath, parseScriptStep } from "./workflow-parse.js"; +import * as fsStore from "./fs-store.js"; + +/** + * @typedef {{ owner: string, file: string, workflow: any }} WorkflowEntry + */ + +/** + * @param {import("fastify").FastifyInstance} server + */ +export function createRegistry(server) { + /** @type {Map} */ + const workflows = new Map(); + /** @type {Map} */ + const loadErrors = new Map(); + /** @type {import("node-cron").ScheduledTask[]} */ + const cronTasks = []; + /** @type {import("node-cron").ScheduledTask | null} */ + let pruneTask = null; + /** @type {Set} */ + const registeredHttpRoutes = new Set(); + + function registerWorkflows() { + workflows.clear(); + loadErrors.clear(); + clearScriptCache(); + + if (!fs.existsSync(WORKFLOWS_DIR)) { + log.warn("workflows directory missing"); + return; + } + + const owners = fsStore.listOwners(); + + for (const owner of owners) { + const registersPath = path.join(WORKFLOWS_DIR, owner, "registers.yaml"); + if (!fs.existsSync(registersPath)) { + log.warn(`Skipping owner "${owner}": no registers.yaml`); + continue; + } + + let workflowFiles = []; + try { + workflowFiles = fsStore.readRegisters(owner); + } catch (err) { + log.error({ err, owner }, "failed to parse registers.yaml"); + continue; + } + + const onDisk = fsStore.listOwnerYamlFiles(owner); + for (const file of onDisk) { + if (!workflowFiles.includes(file)) { + log.warn(`Workflow file not in registers.yaml: ${owner}/${file}`); + } + } + + for (const file of workflowFiles) { + const key = `${owner}/${file}`; + const filePath = path.join(WORKFLOWS_DIR, owner, file); + try { + const workflowData = fs.readFileSync(filePath, "utf8"); + const workflow = yaml.parse(workflowData); + workflows.set(key, { owner, file, workflow }); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + loadErrors.set(key, message); + log.error({ err, workflow: key }, "failed to load workflow; skipping"); + } + } + } + + log.debug({ count: workflows.size }, "workflows loaded"); + } + + function registerHttpTriggers() { + const seen = new Set(); + + for (const [key, { owner, workflow }] of workflows) { + if (workflow.enabled === false) { + log.debug(`Skipping disabled workflow HTTP triggers (${key})`); + continue; + } + + for (const trigger of workflow.triggers ?? []) { + if (trigger.type !== "HTTP") continue; + + const method = String(trigger.method ?? "POST").toUpperCase(); + const url = namespacedPath(owner, trigger.path); + const routeKey = `${method} ${url}`; + + if (seen.has(routeKey)) { + log.warn(`Skipping duplicate HTTP trigger ${routeKey} (${key})`); + continue; + } + seen.add(routeKey); + + if (registeredHttpRoutes.has(routeKey)) { + continue; + } + registeredHttpRoutes.add(routeKey); + + server.route({ + method, + url, + handler: async (req, reply) => { + const result = await runWorkflow( + key, + { data: req.body }, + { type: "http", detail: `${method} ${url}` }, + ); + if (result.status === "failed") { + return reply.code(500).send({ + runId: result.runId, + error: result.error, + }); + } + return reply.send({ + runId: result.runId, + result: result.result, + }); + }, + }); + log.debug(`Registered HTTP trigger ${routeKey} (${key})`); + } + } + } + + function registerCronTriggers() { + for (const task of cronTasks) { + task.destroy(); + } + cronTasks.length = 0; + + for (const [key, { workflow }] of workflows) { + if (workflow.enabled === false) { + log.debug(`Skipping disabled workflow cron triggers (${key})`); + continue; + } + + for (const trigger of workflow.triggers ?? []) { + if (trigger.type !== "cron") continue; + + const schedule = trigger.schedule; + if (!schedule || !cron.validate(schedule)) { + log.warn(`Skipping invalid cron schedule "${schedule}" (${key})`); + continue; + } + + const task = cron.schedule( + schedule, + () => { + log.debug(`cron firing ${key} (${schedule})`); + return runWorkflow( + key, + { data: workflow.data ?? null }, + { type: "cron", detail: schedule }, + ); + }, + { name: `${key}:${schedule}`, noOverlap: true }, + ); + cronTasks.push(task); + log.debug(`Registered cron trigger ${schedule} (${key})`); + } + } + } + + function registerPruneJob() { + if (pruneTask) { + pruneTask.destroy(); + pruneTask = null; + } + const days = Number(process.env.SCRUNNER_RETENTION_DAYS ?? 30); + pruneTask = cron.schedule( + "0 0 * * *", + async () => { + try { + const deleted = await store.pruneOlderThan(days); + log.info({ deleted, days }, "pruned old workflow runs"); + } catch (err) { + log.error({ err }, "failed to prune old workflow runs"); + } + }, + { name: "prune-runs" }, + ); + } + + /** + * @param {string} key + * @param {{ data?: unknown }} context + * @param {{ type: string, detail?: string | null }} trigger + */ + async function runWorkflow(key, context, trigger) { + const entry = workflows.get(key); + if (!entry) { + log.error({ workflow: key }, "workflow not found"); + return { runId: null, status: "failed", error: "workflow not found" }; + } + + const { owner, workflow } = entry; + const run = await store.startRun({ + owner, + workflow: key, + workflowName: workflow?.name, + trigger, + input: context.data, + }); + const runLog = log.child({ runId: run.id, owner, workflow: key }); + runLog.debug("running workflow"); + + let ctx = { + ...context, + data: context.data ?? workflow.data ?? null, + }; + + try { + for (const [index, rawStep] of (workflow.scripts ?? []).entries()) { + const { script, config } = parseScriptStep(rawStep); + const step = await store.startStep({ + runId: run.id, + index, + script, + config, + }); + const stepLog = runLog.child({ stepId: step.id, script }); + try { + ctx = await runScript(script, { ...ctx, config }, { + log: stepLog, + workflowName: key, + }); + await store.finishStep(step.id, "success", ctx); + } catch (err) { + await store.finishStep(step.id, "failed", null, err); + throw err; + } + } + await store.finishRun(run.id, "success", ctx); + return { runId: run.id, status: "success", result: ctx }; + } catch (err) { + runLog.error({ err }, "workflow failed"); + await store.finishRun(run.id, "failed", null, err); + return { + runId: run.id, + status: "failed", + error: err instanceof Error ? err.message : String(err), + }; + } + } + + function reregister() { + registerWorkflows(); + registerHttpTriggers(); + registerCronTriggers(); + } + + function referencedScripts() { + const refs = new Set(); + for (const { workflow } of workflows.values()) { + for (const raw of workflow.scripts ?? []) { + try { + refs.add(parseScriptStep(raw).script); + } catch { + // skip invalid steps + } + } + } + return refs; + } + + return { + workflows, + loadErrors, + registerWorkflows, + registerHttpTriggers, + registerCronTriggers, + registerPruneJob, + reregister, + runWorkflow, + referencedScripts, + }; +} diff --git a/packages/server/runner.js b/packages/server/runner.js index eab615f..541d89e 100644 --- a/packages/server/runner.js +++ b/packages/server/runner.js @@ -1,328 +1,148 @@ import fs from "fs"; -import path from "path"; -import yaml from "yaml"; import fastify from "fastify"; -import cron from "node-cron"; +import cookie from "@fastify/cookie"; +import cors from "@fastify/cors"; +import jwt from "@fastify/jwt"; +import fastifyStatic from "@fastify/static"; import { migrate, db } from "./db.js"; import { log, enableLogPersistence, flushLogs } from "./logger.js"; import * as store from "./store.js"; -import { clearScriptCache, runScript } from "./script-sandbox.js"; +import { createRegistry } from "./registry.js"; +import { COOKIE, OPEN_API_ROUTES } from "./src/api/auth.js"; +import authPlugin from "./src/api/auth.js"; +import usersPlugin from "./src/api/users.js"; +import scriptsPluginFactory from "./src/api/scripts.js"; +import workflowsPluginFactory from "./src/api/workflows.js"; +import runsPlugin from "./src/api/runs.js"; +import dashboardPluginFactory from "./src/api/dashboard.js"; +import { WEB_DIST } from "./paths.js"; await migrate(); enableLogPersistence(); -/** - * @typedef {{ owner: string, file: string, workflow: any }} WorkflowEntry - */ +const jwtSecret = + process.env.SCRUNNER_JWT_SECRET ?? + (process.env.NODE_ENV === "production" ? "" : "scrunner-dev-secret"); -/** - * @type {Map} - */ -const workflows = new Map(); - -/** - * @type {import("node-cron").ScheduledTask[]} - */ -const cronTasks = []; - -/** - * @type {import("node-cron").ScheduledTask | null} - */ -let pruneTask = null; - -/** - * @type {Set} - */ -const registeredHttpRoutes = new Set(); - -function parseScriptStep(step) { - if (typeof step === "string") return { script: step, config: null }; - if (step?.script) return { script: step.script, config: step.config ?? null }; - throw new Error(`Invalid script step: ${JSON.stringify(step)}`); +if (!jwtSecret) { + log.error("SCRUNNER_JWT_SECRET is required in production"); + process.exit(1); } -/** - * Resolve an HTTP path under the owner namespace: /notify -> /u/alice/notify - * @param {string} owner - * @param {string} triggerPath - */ -function namespacedPath(owner, triggerPath) { - const cleaned = String(triggerPath).replace(/^\/+/, ""); - return `/u/${owner}/${cleaned}`; -} - -function registerWorkflows() { - workflows.clear(); - clearScriptCache(); - - const workflowsRoot = "workflows"; - if (!fs.existsSync(workflowsRoot)) { - log.warn("workflows directory missing"); - return; - } - - const owners = fs - .readdirSync(workflowsRoot, { withFileTypes: true }) - .filter((d) => d.isDirectory()) - .map((d) => d.name); - - for (const owner of owners) { - const registersPath = path.join(workflowsRoot, owner, "registers.yaml"); - if (!fs.existsSync(registersPath)) { - log.warn(`Skipping owner "${owner}": no registers.yaml`); - continue; - } - - let workflowFiles = []; - try { - const registerData = fs.readFileSync(registersPath, "utf8"); - const parsed = yaml.parse(registerData) ?? {}; - workflowFiles = parsed.scripts ?? []; - } catch (err) { - log.error({ err, owner }, "failed to parse registers.yaml"); - continue; - } - - const ownerDir = path.join(workflowsRoot, owner); - const onDisk = fs - .readdirSync(ownerDir) - .filter((f) => f.endsWith(".yaml") && f !== "registers.yaml"); - for (const file of onDisk) { - if (!workflowFiles.includes(file)) { - log.warn( - `Workflow file not in registers.yaml: ${owner}/${file}`, - ); - } - } - - for (const file of workflowFiles) { - const key = `${owner}/${file}`; - const filePath = path.join(ownerDir, file); - try { - const workflowData = fs.readFileSync(filePath, "utf8"); - const workflow = yaml.parse(workflowData); - workflows.set(key, { owner, file, workflow }); - } catch (err) { - log.error({ err, workflow: key }, "failed to load workflow; skipping"); - } - } - } - - log.debug({ count: workflows.size }, "workflows loaded"); -} - -function registerHttpTriggers() { - const seen = new Set(); - - for (const [key, { owner, workflow }] of workflows) { - if (workflow.enabled === false) { - log.debug(`Skipping disabled workflow HTTP triggers (${key})`); - continue; - } - - for (const trigger of workflow.triggers ?? []) { - if (trigger.type !== "HTTP") continue; - - const method = String(trigger.method ?? "POST").toUpperCase(); - const url = namespacedPath(owner, trigger.path); - const routeKey = `${method} ${url}`; - - if (seen.has(routeKey)) { - log.warn(`Skipping duplicate HTTP trigger ${routeKey} (${key})`); - continue; - } - seen.add(routeKey); - - if (registeredHttpRoutes.has(routeKey)) { - continue; - } - registeredHttpRoutes.add(routeKey); - - server.route({ - method, - url, - handler: async (req, reply) => { - const result = await runWorkflow( - key, - { data: req.body }, - { type: "http", detail: `${method} ${url}` }, - ); - if (result.status === "failed") { - return reply.code(500).send({ - runId: result.runId, - error: result.error, - }); - } - return reply.send({ - runId: result.runId, - result: result.result, - }); - }, - }); - log.debug(`Registered HTTP trigger ${routeKey} (${key})`); - } - } -} - -function registerCronTriggers() { - for (const task of cronTasks) { - task.destroy(); - } - cronTasks.length = 0; - - for (const [key, { workflow }] of workflows) { - if (workflow.enabled === false) { - log.debug(`Skipping disabled workflow cron triggers (${key})`); - continue; - } - - for (const trigger of workflow.triggers ?? []) { - if (trigger.type !== "cron") continue; - - const schedule = trigger.schedule; - if (!schedule || !cron.validate(schedule)) { - log.warn(`Skipping invalid cron schedule "${schedule}" (${key})`); - continue; - } - - const task = cron.schedule( - schedule, - () => { - log.debug(`cron firing ${key} (${schedule})`); - return runWorkflow( - key, - { data: workflow.data ?? null }, - { type: "cron", detail: schedule }, - ); - }, - { name: `${key}:${schedule}`, noOverlap: true }, - ); - cronTasks.push(task); - log.debug(`Registered cron trigger ${schedule} (${key})`); - } - } -} - -function registerPruneJob() { - if (pruneTask) { - pruneTask.destroy(); - pruneTask = null; - } - const days = Number(process.env.SCRUNNER_RETENTION_DAYS ?? 30); - pruneTask = cron.schedule( - "0 0 * * *", - async () => { - try { - const deleted = await store.pruneOlderThan(days); - log.info({ deleted, days }, "pruned old workflow runs"); - } catch (err) { - log.error({ err }, "failed to prune old workflow runs"); - } - }, - { name: "prune-runs" }, - ); -} - -/** - * @param {string} key - * @param {{ data?: unknown }} context - * @param {{ type: string, detail?: string | null }} trigger - */ -async function runWorkflow(key, context, trigger) { - const entry = workflows.get(key); - if (!entry) { - log.error({ workflow: key }, "workflow not found"); - return { runId: null, status: "failed", error: "workflow not found" }; - } - - const { owner, workflow } = entry; - const run = await store.startRun({ - owner, - workflow: key, - workflowName: workflow?.name, - trigger, - input: context.data, - }); - const runLog = log.child({ runId: run.id, owner, workflow: key }); - runLog.debug("running workflow"); - - let ctx = { - ...context, - data: context.data ?? workflow.data ?? null, - }; - - try { - for (const [index, rawStep] of (workflow.scripts ?? []).entries()) { - const { script, config } = parseScriptStep(rawStep); - const step = await store.startStep({ - runId: run.id, - index, - script, - config, - }); - const stepLog = runLog.child({ stepId: step.id, script }); - try { - ctx = await runScript(script, { ...ctx, config }, { - log: stepLog, - workflowName: key, - }); - await store.finishStep(step.id, "success", ctx); - } catch (err) { - await store.finishStep(step.id, "failed", null, err); - throw err; - } - } - await store.finishRun(run.id, "success", ctx); - return { runId: run.id, status: "success", result: ctx }; - } catch (err) { - runLog.error({ err }, "workflow failed"); - await store.finishRun(run.id, "failed", null, err); - return { - runId: run.id, - status: "failed", - error: err instanceof Error ? err.message : String(err), - }; - } -} - -registerWorkflows(); - const server = fastify({ loggerInstance: log }); -registerHttpTriggers(); -registerCronTriggers(); -registerPruneJob(); - -server.post("/admin/workflows/reregister", async (_req, reply) => { - registerWorkflows(); - // Fastify cannot remove routes; new routes are added, cron is rebuilt. - registerHttpTriggers(); - registerCronTriggers(); - return reply.send({ message: "Workflows refreshed" }); +await server.register(cookie); +await server.register(jwt, { + secret: jwtSecret, + cookie: { + cookieName: COOKIE, + signed: false, + }, +}); +await server.register(cors, { + origin: process.env.SCRUNNER_CORS_ORIGIN ?? "http://localhost:5173", + credentials: true, }); -server.get("/admin/runs", async (req, reply) => { - const q = /** @type {Record} */ (req.query); - const limit = q.limit ? Number(q.limit) : undefined; - const runs = await store.listRuns({ - owner: q.owner, - workflow: q.workflow, - status: q.status, - limit: Number.isFinite(limit) ? limit : undefined, - before: q.before, - }); - return reply.send({ runs }); -}); - -server.get("/admin/runs/:id", async (req, reply) => { - const { id } = /** @type {{ id: string }} */ (req.params); - const run = await store.getRun(id); - if (!run) { - return reply.code(404).send({ error: "run not found" }); +server.decorate("authenticate", async function authenticate(req, reply) { + try { + await req.jwtVerify(); + } catch { + return reply.code(401).send({ error: "unauthorized" }); } - return reply.send(run); }); +server.decorate("requireAdmin", async function requireAdmin(req, reply) { + if (req.user?.role !== "admin") { + return reply.code(403).send({ error: "forbidden" }); + } +}); + +const registry = createRegistry(server); +registry.registerWorkflows(); +registry.registerHttpTriggers(); +registry.registerCronTriggers(); +registry.registerPruneJob(); + +await server.register( + async (api) => { + api.addHook("onRequest", async (req, reply) => { + const raw = (req.url || "").split("?")[0]; + const stripped = raw.replace(/^\/api/, "") || "/"; + const routeUrl = req.routeOptions?.url || stripped; + const open = + OPEN_API_ROUTES.has(`${req.method} ${routeUrl}`) || + OPEN_API_ROUTES.has(`${req.method} ${stripped}`); + if (open) return; + await server.authenticate(req, reply); + }); + await api.register(authPlugin); + await api.register(usersPlugin); + await api.register(scriptsPluginFactory(registry)); + await api.register(workflowsPluginFactory(registry)); + await api.register(runsPlugin); + await api.register(dashboardPluginFactory(registry)); + }, + { prefix: "/api" }, +); + +server.post( + "/admin/workflows/reregister", + { onRequest: [server.authenticate] }, + async (_req, reply) => { + registry.reregister(); + return reply.send({ message: "Workflows refreshed" }); + }, +); + +server.get( + "/admin/runs", + { onRequest: [server.authenticate] }, + async (req, reply) => { + const q = /** @type {Record} */ (req.query); + const limit = q.limit ? Number(q.limit) : undefined; + const runs = await store.listRuns({ + owner: q.owner, + workflow: q.workflow, + status: q.status, + limit: Number.isFinite(limit) ? limit : undefined, + before: q.before, + }); + return reply.send({ runs }); + }, +); + +server.get( + "/admin/runs/:id", + { onRequest: [server.authenticate] }, + async (req, reply) => { + const { id } = /** @type {{ id: string }} */ (req.params); + const run = await store.getRun(id); + if (!run) { + return reply.code(404).send({ error: "run not found" }); + } + return reply.send(run); + }, +); + +if (fs.existsSync(WEB_DIST)) { + await server.register(fastifyStatic, { + root: WEB_DIST, + wildcard: false, + }); + server.setNotFoundHandler((req, reply) => { + const url = req.raw.url ?? ""; + if ( + url.startsWith("/api") || + url.startsWith("/u/") || + url.startsWith("/admin") + ) { + return reply.code(404).send({ error: "not found" }); + } + return reply.sendFile("index.html"); + }); +} + async function shutdown() { try { await flushLogs(); @@ -336,13 +156,15 @@ async function shutdown() { process.on("SIGINT", shutdown); process.on("SIGTERM", shutdown); +const port = Number(process.env.PORT ?? 9000); + server .listen({ host: "0.0.0.0", - port: 9000, + port, }) .then(() => { - log.info("Server is running on port 9000"); + log.info(`Server is running on port ${port}`); }) .catch((err) => { log.error({ err }, "failed to start server"); diff --git a/packages/server/script-sandbox.js b/packages/server/script-sandbox.js index e565967..9573bbc 100644 --- a/packages/server/script-sandbox.js +++ b/packages/server/script-sandbox.js @@ -1,8 +1,9 @@ import fs from "fs"; +import path from "path"; import vm from "node:vm"; -import { fileURLToPath } from "node:url"; import { createRequire } from "node:module"; import axios from "axios"; +import { SCRIPTS_DIR } from "./paths.js"; const hostRequire = createRequire(import.meta.url); @@ -291,7 +292,7 @@ function createScriptSandbox({ log, script, workflowName }) { } function loadCompiledScript(script) { - const filePath = fileURLToPath(new URL(`./scripts/${script}`, import.meta.url)); + const filePath = path.join(SCRIPTS_DIR, script); const { mtimeMs } = fs.statSync(filePath); const cached = scriptCache.get(script); if (cached && cached.mtimeMs === mtimeMs) { diff --git a/packages/server/scripts/jsonata.js b/packages/server/scripts/jsonata.js index b0177e5..0f6506e 100644 --- a/packages/server/scripts/jsonata.js +++ b/packages/server/scripts/jsonata.js @@ -1,6 +1,10 @@ import jsonata from "jsonata"; -export default function jsonataFn(context) { - const expression = jsonata(context.config.expression); - return expression.evaluate(context.data) +export default function jsonataFn(ctx) { + log.info({ctx}, "jsonata: context") + log.info("jsonata: evaluating expression %s", ctx.config.expression); + const expression = jsonata(ctx.config.expression); + const result = expression.evaluate(ctx.data); + log.info({result}, "jsonata: expression result"); + return result; } \ No newline at end of file diff --git a/packages/server/scripts/ntfy.js b/packages/server/scripts/ntfy.js index 5e50d57..065c4fe 100644 --- a/packages/server/scripts/ntfy.js +++ b/packages/server/scripts/ntfy.js @@ -1,12 +1,19 @@ export default async function ntfy(ctx) { - log.info({ ctx }, "ntfy"); + log.info({ ctx }, "ntfy incoming context"); const headers = {} if(ctx.data?.title) { + log.info("ntfy: setting title %s", ctx.data.title); headers.Title = ctx.data.title } - await $axios.post(ctx.config?.url || "https://ntfy.sh/scrunner", ctx.data?.message || "Hello from scrunner", { + const ntfyUrl = ctx.config?.url || "https://ntfy.sh/scrunner"; + + log.info("ntfy sending message to %s", ntfyUrl); + const truncatedMessage = ctx.data?.message?.substring(0, 100); + log.info("ntfy messsage: %s", truncatedMessage); + + await $axios.post(ntfyUrl, ctx.data?.message || "Hello from scrunner", { headers: headers }) return {sent: "true"} diff --git a/packages/server/src/api/auth.js b/packages/server/src/api/auth.js new file mode 100644 index 0000000..d877c3d --- /dev/null +++ b/packages/server/src/api/auth.js @@ -0,0 +1,103 @@ +import bcrypt from "bcryptjs"; +import * as store from "../../store.js"; + +export const COOKIE = "scrunner_token"; +export const OPEN_API_ROUTES = new Set([ + "GET /auth/bootstrap", + "POST /auth/register", + "POST /auth/login", +]); + +export function cookieOpts() { + return { + httpOnly: true, + path: "/", + sameSite: "lax", + secure: process.env.NODE_ENV === "production", + maxAge: 7 * 24 * 60 * 60, + }; +} + +export function validateCredentials(username, password) { + if (!/^[A-Za-z0-9_]{3,32}$/.test(username)) { + return "username must be 3-32 letters, numbers, or underscore"; + } + if (password.length < 8) { + return "password must be at least 8 characters"; + } + return null; +} + +/** + * @param {import("fastify").FastifyInstance} fastify + */ +export default async function authPlugin(fastify) { + fastify.get("/auth/bootstrap", async () => { + const count = await store.countUsers(); + return { needsSetup: count === 0 }; + }); + + fastify.post("/auth/register", async (req, reply) => { + const count = await store.countUsers(); + if (count > 0) { + return reply.code(403).send({ error: "setup already complete" }); + } + const body = /** @type {{ username?: string, password?: string }} */ (req.body ?? {}); + const username = String(body.username ?? "").trim(); + const password = String(body.password ?? ""); + const err = validateCredentials(username, password); + if (err) return reply.code(400).send({ error: err }); + + const passwordHash = await bcrypt.hash(password, 10); + const user = await store.createUser({ + username, + passwordHash, + role: "admin", + }); + const token = await reply.jwtSign({ + id: user.id, + username: user.username, + role: user.role, + }); + reply.setCookie(COOKIE, token, cookieOpts()); + return { user }; + }); + + fastify.post("/auth/login", async (req, reply) => { + const body = /** @type {{ username?: string, password?: string }} */ (req.body ?? {}); + const username = String(body.username ?? "").trim(); + const password = String(body.password ?? ""); + const row = await store.getUserAuthByUsername(username); + if (!row || !(await bcrypt.compare(password, row.password_hash))) { + return reply.code(401).send({ error: "invalid credentials" }); + } + const token = await reply.jwtSign({ + id: row.id, + username: row.username, + role: row.role, + }); + reply.setCookie(COOKIE, token, cookieOpts()); + return { + user: { + id: row.id, + username: row.username, + role: row.role, + created_at: row.created_at, + updated_at: row.updated_at, + }, + }; + }); + + fastify.post("/auth/logout", async (_req, reply) => { + reply.clearCookie(COOKIE, { path: "/" }); + return { ok: true }; + }); + + fastify.get("/auth/me", async (req, reply) => { + const user = await store.getUserById(req.user.id); + if (!user) { + return reply.code(401).send({ error: "unauthorized" }); + } + return { user }; + }); +} diff --git a/packages/server/src/api/dashboard.js b/packages/server/src/api/dashboard.js new file mode 100644 index 0000000..fe3831e --- /dev/null +++ b/packages/server/src/api/dashboard.js @@ -0,0 +1,86 @@ +import yaml from "yaml"; +import * as store from "../../store.js"; +import * as fsStore from "../../fs-store.js"; + +/** + * @param {{ workflows: Map, loadErrors: Map }} registry + */ +export default function dashboardPluginFactory(registry) { + /** + * @param {import("fastify").FastifyInstance} fastify + */ + return async function dashboardPlugin(fastify) { + fastify.get("/dashboard", async () => { + const owners = fsStore.listOwners(); + let workflowCount = 0; + let enabledCount = 0; + let brokenCount = registry.loadErrors.size; + const brokenWorkflows = []; + + for (const [key, message] of registry.loadErrors) { + const [owner, ...rest] = key.split("/"); + brokenWorkflows.push({ + key, + owner, + file: rest.join("/"), + loadError: message, + }); + } + + for (const owner of owners) { + let registered = []; + try { + registered = fsStore.readRegisters(owner); + } catch { + registered = []; + } + const files = [ + ...new Set([...registered, ...fsStore.listOwnerYamlFiles(owner)]), + ]; + for (const file of files) { + workflowCount += 1; + const key = `${owner}/${file}`; + const loaded = registry.workflows.get(key); + if (loaded?.workflow && loaded.workflow.enabled !== false) { + enabledCount += 1; + } else if (!loaded && !registry.loadErrors.has(key)) { + const raw = fsStore.readWorkflowYaml(owner, file); + if (raw) { + try { + const parsed = yaml.parse(raw); + if (parsed?.enabled !== false) enabledCount += 1; + } catch (err) { + brokenCount += 1; + brokenWorkflows.push({ + key, + owner, + file, + loadError: err instanceof Error ? err.message : String(err), + }); + } + } + } + } + } + + const [running, failed, recent] = await Promise.all([ + store.listRuns({ status: "running", limit: 10 }), + store.listRuns({ status: "failed", limit: 20 }), + store.listRuns({ limit: 10 }), + ]); + + return { + workflowCount, + scriptCount: fsStore.listScriptFiles().length, + enabledCount, + brokenCount, + running, + needsAttention: { + failed, + brokenWorkflows, + }, + recent, + }; + }); + }; +} diff --git a/packages/server/src/api/runs.js b/packages/server/src/api/runs.js new file mode 100644 index 0000000..570e707 --- /dev/null +++ b/packages/server/src/api/runs.js @@ -0,0 +1,26 @@ +import * as store from "../../store.js"; + +/** + * @param {import("fastify").FastifyInstance} fastify + */ +export default async function runsPlugin(fastify) { + fastify.get("/runs", async (req) => { + const q = /** @type {Record} */ (req.query ?? {}); + const limit = q.limit ? Number(q.limit) : undefined; + const runs = await store.listRuns({ + owner: q.owner, + workflow: q.workflow, + status: q.status, + limit: Number.isFinite(limit) ? limit : undefined, + before: q.before, + }); + return { runs }; + }); + + fastify.get("/runs/:id", async (req, reply) => { + const { id } = /** @type {{ id: string }} */ (req.params); + const run = await store.getRun(id); + if (!run) return reply.code(404).send({ error: "run not found" }); + return run; + }); +} diff --git a/packages/server/src/api/scripts.js b/packages/server/src/api/scripts.js new file mode 100644 index 0000000..4c06684 --- /dev/null +++ b/packages/server/src/api/scripts.js @@ -0,0 +1,64 @@ +import { clearScriptCache } from "../../script-sandbox.js"; +import * as fsStore from "../../fs-store.js"; + +/** + * @param {{ referencedScripts: () => Set }} registry + */ +export default function scriptsPluginFactory(registry) { + /** + * @param {import("fastify").FastifyInstance} fastify + */ + return async function scriptsPlugin(fastify) { + fastify.get("/scripts", async () => { + return { scripts: fsStore.listScriptFiles() }; + }); + + fastify.get("/scripts/:name", async (req, reply) => { + const { name } = /** @type {{ name: string }} */ (req.params); + try { + fsStore.assertScriptName(name); + } catch (err) { + return reply.code(err.statusCode ?? 400).send({ error: err.message }); + } + const content = fsStore.readScript(name); + if (content == null) return reply.code(404).send({ error: "script not found" }); + return { name, content }; + }); + + fastify.put("/scripts/:name", async (req, reply) => { + const { name } = /** @type {{ name: string }} */ (req.params); + try { + fsStore.assertScriptName(name); + } catch (err) { + return reply.code(err.statusCode ?? 400).send({ error: err.message }); + } + const body = /** @type {{ content?: string }} */ (req.body ?? {}); + if (typeof body.content !== "string") { + return reply.code(400).send({ error: "content is required" }); + } + const existed = fsStore.readScript(name) != null; + fsStore.writeScript(name, body.content); + clearScriptCache(); + return reply.code(existed ? 200 : 201).send({ name }); + }); + + fastify.delete("/scripts/:name", async (req, reply) => { + const { name } = /** @type {{ name: string }} */ (req.params); + try { + fsStore.assertScriptName(name); + } catch (err) { + return reply.code(err.statusCode ?? 400).send({ error: err.message }); + } + if (registry.referencedScripts().has(name)) { + return reply + .code(409) + .send({ error: "script is referenced by a workflow" }); + } + if (!fsStore.deleteScript(name)) { + return reply.code(404).send({ error: "script not found" }); + } + clearScriptCache(); + return { ok: true }; + }); + }; +} diff --git a/packages/server/src/api/users.js b/packages/server/src/api/users.js new file mode 100644 index 0000000..1255d48 --- /dev/null +++ b/packages/server/src/api/users.js @@ -0,0 +1,85 @@ +import bcrypt from "bcryptjs"; +import * as store from "../../store.js"; +import { validateCredentials } from "./auth.js"; + +/** + * @param {import("fastify").FastifyInstance} fastify + */ +export default async function usersPlugin(fastify) { + fastify.addHook("onRequest", fastify.requireAdmin); + + fastify.get("/users", async () => { + const users = await store.listUsers(); + return { users }; + }); + + fastify.post("/users", async (req, reply) => { + const body = /** @type {{ username?: string, password?: string, role?: string }} */ ( + req.body ?? {} + ); + const username = String(body.username ?? "").trim(); + const password = String(body.password ?? ""); + const role = body.role === "admin" ? "admin" : "operator"; + const err = validateCredentials(username, password); + if (err) return reply.code(400).send({ error: err }); + + const existing = await store.getUserAuthByUsername(username); + if (existing) { + return reply.code(409).send({ error: "username taken" }); + } + + const user = await store.createUser({ + username, + passwordHash: await bcrypt.hash(password, 10), + role, + }); + return reply.code(201).send({ user }); + }); + + fastify.patch("/users/:id", async (req, reply) => { + const { id } = /** @type {{ id: string }} */ (req.params); + const existing = await store.getUserAuthById(id); + if (!existing) return reply.code(404).send({ error: "user not found" }); + + const body = /** @type {{ password?: string, role?: string }} */ (req.body ?? {}); + /** @type {{ passwordHash?: string, role?: string }} */ + const patch = {}; + + if (body.role) { + if (body.role !== "admin" && body.role !== "operator") { + return reply.code(400).send({ error: "invalid role" }); + } + if (existing.role === "admin" && body.role !== "admin") { + const admins = await store.countAdmins(); + if (admins <= 1) { + return reply.code(400).send({ error: "cannot demote last admin" }); + } + } + patch.role = body.role; + } + + if (body.password) { + if (body.password.length < 8) { + return reply.code(400).send({ error: "password must be at least 8 characters" }); + } + patch.passwordHash = await bcrypt.hash(body.password, 10); + } + + const user = await store.updateUser(id, patch); + return { user }; + }); + + fastify.delete("/users/:id", async (req, reply) => { + const { id } = /** @type {{ id: string }} */ (req.params); + const existing = await store.getUserAuthById(id); + if (!existing) return reply.code(404).send({ error: "user not found" }); + if (existing.role === "admin") { + const admins = await store.countAdmins(); + if (admins <= 1) { + return reply.code(400).send({ error: "cannot delete last admin" }); + } + } + await store.deleteUser(id); + return { ok: true }; + }); +} diff --git a/packages/server/src/api/workflows.js b/packages/server/src/api/workflows.js new file mode 100644 index 0000000..adc66ab --- /dev/null +++ b/packages/server/src/api/workflows.js @@ -0,0 +1,240 @@ +import yaml from "yaml"; +import * as store from "../../store.js"; +import * as fsStore from "../../fs-store.js"; +import { namespacedPath, parseScriptStep } from "../../workflow-parse.js"; + +function triggerSummary(owner, workflow) { + if (!workflow || typeof workflow !== "object") return []; + return (workflow.triggers ?? []).map((t) => { + const type = t?.type ?? "unknown"; + const isHttp = String(type).toLowerCase() === "http"; + return { + type, + method: isHttp ? String(t?.method ?? "POST").toUpperCase() : t?.method ?? null, + path: isHttp && t?.path != null ? namespacedPath(owner, t.path) : t?.path ?? null, + schedule: t?.schedule ?? null, + }; + }); +} + +function scriptNames(workflow) { + if (!workflow || typeof workflow !== "object") return []; + const names = []; + for (const raw of workflow.scripts ?? []) { + try { + names.push(parseScriptStep(raw).script); + } catch { + names.push(null); + } + } + return names; +} + +/** + * @param {{ workflows: Map, loadErrors: Map, reregister: () => void }} registry + */ +export default function workflowsPluginFactory(registry) { + /** + * @param {import("fastify").FastifyInstance} fastify + */ + return async function workflowsPlugin(fastify) { + fastify.get("/owners", async () => { + return { owners: fsStore.listOwners() }; + }); + + fastify.get("/workflows", async (req) => { + const q = /** @type {{ owner?: string }} */ (req.query ?? {}); + const stats = await store.workflowStats(); + const owners = q.owner + ? [fsStore.assertOwner(q.owner)] + : fsStore.listOwners(); + + const items = []; + for (const owner of owners) { + let registered = []; + try { + registered = fsStore.readRegisters(owner); + } catch { + registered = []; + } + const onDisk = fsStore.listOwnerYamlFiles(owner); + const files = [...new Set([...registered, ...onDisk])]; + + for (const file of files) { + const key = `${owner}/${file}`; + const loaded = registry.workflows.get(key); + const loadError = registry.loadErrors.get(key) ?? null; + let parsed = loaded?.workflow ?? null; + if (!parsed) { + const raw = fsStore.readWorkflowYaml(owner, file); + if (raw != null) { + try { + parsed = yaml.parse(raw); + } catch (err) { + // keep loadError + if (!loadError) { + // file on disk but unparseable and not in registers + } + } + } + } + const st = stats[key] ?? { + invocationCount: 0, + lastInvokedAt: null, + lastStatus: null, + }; + items.push({ + owner, + file, + key, + name: parsed?.name ?? file, + description: parsed?.description ?? null, + enabled: parsed ? parsed.enabled !== false : false, + registered: registered.includes(file), + loadError: + loadError ?? + (parsed ? null : "unreadable"), + lastInvokedAt: st.lastInvokedAt, + lastStatus: st.lastStatus ?? null, + invocationCount: st.invocationCount, + triggers: triggerSummary(owner, parsed), + scripts: scriptNames(parsed), + }); + } + } + return { workflows: items }; + }); + + fastify.get("/workflows/:owner/:file", async (req, reply) => { + const { owner, file } = /** @type {{ owner: string, file: string }} */ ( + req.params + ); + try { + fsStore.assertOwner(owner); + fsStore.assertWorkflowFile(file); + } catch (err) { + return reply.code(err.statusCode ?? 400).send({ error: err.message }); + } + const content = fsStore.readWorkflowYaml(owner, file); + if (content == null) { + return reply.code(404).send({ error: "workflow not found" }); + } + const key = `${owner}/${file}`; + let parsed = null; + let parseError = null; + try { + parsed = yaml.parse(content); + } catch (err) { + parseError = err instanceof Error ? err.message : String(err); + } + const stats = (await store.workflowStats())[key] ?? { + invocationCount: 0, + lastInvokedAt: null, + lastStatus: null, + }; + return { + owner, + file, + key, + content, + parsed, + parseError, + loadError: registry.loadErrors.get(key) ?? parseError, + lastInvokedAt: stats.lastInvokedAt, + lastStatus: stats.lastStatus ?? null, + invocationCount: stats.invocationCount, + }; + }); + + fastify.put("/workflows/:owner/:file", async (req, reply) => { + const { owner, file } = /** @type {{ owner: string, file: string }} */ ( + req.params + ); + try { + fsStore.assertOwner(owner); + fsStore.assertWorkflowFile(file); + } catch (err) { + return reply.code(err.statusCode ?? 400).send({ error: err.message }); + } + const body = /** @type {{ content?: string }} */ (req.body ?? {}); + if (typeof body.content !== "string") { + return reply.code(400).send({ error: "content is required" }); + } + try { + yaml.parse(body.content); + } catch (err) { + return reply.code(400).send({ + error: `invalid yaml: ${err instanceof Error ? err.message : String(err)}`, + }); + } + const existed = fsStore.readWorkflowYaml(owner, file) != null; + fsStore.writeWorkflowYaml(owner, file, body.content); + const registered = fsStore.readRegisters(owner); + if (!registered.includes(file)) { + registered.push(file); + fsStore.writeRegisters(owner, registered); + } + registry.reregister(); + return reply.code(existed ? 200 : 201).send({ owner, file }); + }); + + fastify.delete("/workflows/:owner/:file", async (req, reply) => { + const { owner, file } = /** @type {{ owner: string, file: string }} */ ( + req.params + ); + try { + fsStore.assertOwner(owner); + fsStore.assertWorkflowFile(file); + } catch (err) { + return reply.code(err.statusCode ?? 400).send({ error: err.message }); + } + if (!fsStore.deleteWorkflowYaml(owner, file)) { + return reply.code(404).send({ error: "workflow not found" }); + } + const registered = fsStore.readRegisters(owner).filter((f) => f !== file); + fsStore.writeRegisters(owner, registered); + registry.reregister(); + return { ok: true }; + }); + + fastify.post("/workflows/:owner/:file/run", async (req, reply) => { + const { owner, file } = /** @type {{ owner: string, file: string }} */ ( + req.params + ); + try { + fsStore.assertOwner(owner); + fsStore.assertWorkflowFile(file); + } catch (err) { + return reply.code(err.statusCode ?? 400).send({ error: err.message }); + } + const key = `${owner}/${file}`; + if (!registry.workflows.has(key)) { + return reply.code(404).send({ + error: registry.loadErrors.get(key) ?? "workflow not loaded", + }); + } + const body = /** @type {{ data?: unknown }} */ (req.body ?? {}); + const result = await registry.runWorkflow( + key, + { data: body.data ?? null }, + { type: "manual", detail: "ui" }, + ); + if (result.status === "failed") { + return reply.code(result.runId ? 500 : 404).send({ + runId: result.runId, + error: result.error, + }); + } + return { + runId: result.runId, + status: result.status, + result: result.result, + }; + }); + + fastify.post("/workflows/reregister", async () => { + registry.reregister(); + return { message: "Workflows refreshed" }; + }); + }; +} diff --git a/packages/server/store.js b/packages/server/store.js index 38a7bea..6c76017 100644 --- a/packages/server/store.js +++ b/packages/server/store.js @@ -235,3 +235,102 @@ export async function pruneOlderThan(days) { const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString(); return db("workflow_runs").where("started_at", "<", cutoff).del(); } + +/** + * @returns {Promise>} + */ +export async function workflowStats() { + const rows = await db("workflow_runs") + .select("workflow", "status", "started_at") + .orderBy("started_at", "desc"); + + /** @type {Record} */ + const out = {}; + for (const row of rows) { + const existing = out[row.workflow]; + if (!existing) { + out[row.workflow] = { + invocationCount: 1, + lastInvokedAt: row.started_at ?? null, + lastStatus: row.status ?? null, + }; + } else { + existing.invocationCount += 1; + } + } + return out; +} + +export async function countUsers() { + const row = await db("users").count({ n: "*" }).first(); + return Number(row?.n ?? 0); +} + +export async function countAdmins() { + const row = await db("users").where({ role: "admin" }).count({ n: "*" }).first(); + return Number(row?.n ?? 0); +} + +/** + * @param {{ username: string, passwordHash: string, role: string }} opts + */ +export async function createUser({ username, passwordHash, role }) { + const id = randomUUID(); + const now = nowIso(); + await db("users").insert({ + id, + username, + password_hash: passwordHash, + role, + created_at: now, + updated_at: now, + }); + return getUserById(id); +} + +export async function getUserById(id) { + const row = await db("users").where({ id }).first(); + return row ? publicUser(row) : null; +} + +export async function getUserAuthByUsername(username) { + return db("users").where({ username }).first(); +} + +export async function getUserAuthById(id) { + return db("users").where({ id }).first(); +} + +export async function listUsers() { + const rows = await db("users") + .select("id", "username", "role", "created_at", "updated_at") + .orderBy("username", "asc"); + return rows; +} + +/** + * @param {string} id + * @param {{ passwordHash?: string, role?: string }} patch + */ +export async function updateUser(id, patch) { + const update = { updated_at: nowIso() }; + if (patch.passwordHash) update.password_hash = patch.passwordHash; + if (patch.role) update.role = patch.role; + await db("users").where({ id }).update(update); + return getUserById(id); +} + +export async function deleteUser(id) { + return db("users").where({ id }).del(); +} + +function publicUser(row) { + return { + id: row.id, + username: row.username, + role: row.role, + created_at: row.created_at, + updated_at: row.updated_at, + }; +} + diff --git a/packages/server/workflow-parse.js b/packages/server/workflow-parse.js new file mode 100644 index 0000000..b144c62 --- /dev/null +++ b/packages/server/workflow-parse.js @@ -0,0 +1,15 @@ +export function parseScriptStep(step) { + if (typeof step === "string") return { script: step, config: null }; + if (step?.script) return { script: step.script, config: step.config ?? null }; + throw new Error(`Invalid script step: ${JSON.stringify(step)}`); +} + +/** + * Resolve an HTTP path under the owner namespace: /notify -> /u/alice/notify + * @param {string} owner + * @param {string} triggerPath + */ +export function namespacedPath(owner, triggerPath) { + const cleaned = String(triggerPath).replace(/^\/+/, ""); + return `/u/${owner}/${cleaned}`; +} diff --git a/packages/server/workflows/default/manual-trigger.yaml b/packages/server/workflows/default/manual-trigger.yaml index 781cdbc..fd5307d 100644 --- a/packages/server/workflows/default/manual-trigger.yaml +++ b/packages/server/workflows/default/manual-trigger.yaml @@ -6,4 +6,5 @@ triggers: - type: HTTP method: POST path: /mt + - type: manual diff --git a/packages/server/workflows/default/test.yaml b/packages/server/workflows/default/test.yaml index b7b273e..871e1b7 100644 --- a/packages/server/workflows/default/test.yaml +++ b/packages/server/workflows/default/test.yaml @@ -1,4 +1,4 @@ -name: manual trigger +name: jsonata scripts: - get-current-time.js - script: jsonata.js diff --git a/packages/web/index.html b/packages/web/index.html new file mode 100644 index 0000000..41012af --- /dev/null +++ b/packages/web/index.html @@ -0,0 +1,12 @@ + + + + + + scrunner + + +
+ + + diff --git a/packages/web/package.json b/packages/web/package.json new file mode 100644 index 0000000..b22974e --- /dev/null +++ b/packages/web/package.json @@ -0,0 +1,29 @@ +{ + "name": "@scrunner/web", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@monaco-editor/react": "^4.7.0", + "@tanstack/react-query": "^5.84.0", + "axios": "^1.11.0", + "mermaid": "^11.9.0", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-icons": "^5.5.0", + "react-router-dom": "^7.8.0", + "yaml": "^2.8.1" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.1.11", + "@vitejs/plugin-react": "^5.0.0", + "daisyui": "^5.0.50", + "tailwindcss": "^4.1.11", + "vite": "^7.1.2" + } +} diff --git a/packages/web/src/App.jsx b/packages/web/src/App.jsx new file mode 100644 index 0000000..1ac8ed1 --- /dev/null +++ b/packages/web/src/App.jsx @@ -0,0 +1,60 @@ +import { useEffect } from "react"; +import { Navigate, Route, Routes } from "react-router-dom"; +import { useQueryClient } from "@tanstack/react-query"; +import { useBootstrap, useMe } from "./api/hooks.js"; +import { Layout } from "./components/Layout.jsx"; +import { AuthPage } from "./pages/AuthPage.jsx"; +import { HomePage } from "./pages/HomePage.jsx"; +import { ScriptsPage } from "./pages/ScriptsPage.jsx"; +import { WorkflowsPage } from "./pages/WorkflowsPage.jsx"; +import { EventsPage } from "./pages/EventsPage.jsx"; +import { EventDetailPage } from "./pages/EventDetailPage.jsx"; +import { UsersPage } from "./pages/UsersPage.jsx"; + +export function App() { + const qc = useQueryClient(); + const bootstrap = useBootstrap(); + const me = useMe(); + + useEffect(() => { + function onUnauthorized() { + qc.setQueryData(["me"], null); + } + window.addEventListener("scrunner:unauthorized", onUnauthorized); + return () => window.removeEventListener("scrunner:unauthorized", onUnauthorized); + }, [qc]); + + const user = me.data?.user; + const loading = + bootstrap.isLoading || (me.isLoading && !me.isError && me.data === undefined); + + if (loading) { + return ( +
+ +
+ ); + } + + if (!user) { + return ; + } + + return ( + + + } /> + } /> + } /> + } /> + } /> + {user.role === "admin" ? ( + } /> + ) : ( + } /> + )} + } /> + + + ); +} diff --git a/packages/web/src/api/client.js b/packages/web/src/api/client.js new file mode 100644 index 0000000..14ab21e --- /dev/null +++ b/packages/web/src/api/client.js @@ -0,0 +1,22 @@ +import axios from "axios"; + +export const api = axios.create({ + baseURL: "/api", + withCredentials: true, +}); + +api.interceptors.response.use( + (res) => res, + (err) => { + const url = err.config?.url ?? ""; + const isAuthCall = url.includes("/auth/login") || url.includes("/auth/register") || url.includes("/auth/me") || url.includes("/auth/bootstrap"); + if (err.response?.status === 401 && !isAuthCall) { + window.dispatchEvent(new Event("scrunner:unauthorized")); + } + return Promise.reject(err); + }, +); + +export function errorMessage(err, fallback = "request failed") { + return err?.response?.data?.error ?? err?.message ?? fallback; +} diff --git a/packages/web/src/api/hooks.js b/packages/web/src/api/hooks.js new file mode 100644 index 0000000..44517cd --- /dev/null +++ b/packages/web/src/api/hooks.js @@ -0,0 +1,256 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { api } from "./client.js"; + +export function useBootstrap() { + return useQuery({ + queryKey: ["bootstrap"], + queryFn: async () => (await api.get("/auth/bootstrap")).data, + }); +} + +export function useMe() { + return useQuery({ + queryKey: ["me"], + queryFn: async () => { + try { + return (await api.get("/auth/me")).data; + } catch (err) { + if (err.response?.status === 401) return { user: null }; + throw err; + } + }, + retry: false, + }); +} + +export function useLogin() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (body) => (await api.post("/auth/login", body)).data, + onSuccess: (data) => { + qc.setQueryData(["me"], data); + qc.invalidateQueries({ queryKey: ["bootstrap"] }); + }, + }); +} + +export function useRegister() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (body) => (await api.post("/auth/register", body)).data, + onSuccess: (data) => { + qc.setQueryData(["me"], data); + qc.invalidateQueries({ queryKey: ["bootstrap"] }); + }, + }); +} + +export function useLogout() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async () => (await api.post("/auth/logout")).data, + onSuccess: () => { + qc.setQueryData(["me"], null); + qc.clear(); + }, + }); +} + +export function useDashboard() { + return useQuery({ + queryKey: ["dashboard"], + queryFn: async () => (await api.get("/dashboard")).data, + refetchInterval: (query) => + query.state.data?.running?.length ? 4000 : 15000, + }); +} + +export function useScripts() { + return useQuery({ + queryKey: ["scripts"], + queryFn: async () => (await api.get("/scripts")).data.scripts, + }); +} + +export function useScript(name, enabled = true) { + return useQuery({ + queryKey: ["scripts", name], + queryFn: async () => (await api.get(`/scripts/${encodeURIComponent(name)}`)).data, + enabled: Boolean(name) && enabled, + }); +} + +export function useSaveScript() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async ({ name, content }) => + (await api.put(`/scripts/${encodeURIComponent(name)}`, { content })).data, + onSuccess: (_data, vars) => { + qc.invalidateQueries({ queryKey: ["scripts"] }); + qc.invalidateQueries({ queryKey: ["scripts", vars.name] }); + qc.invalidateQueries({ queryKey: ["dashboard"] }); + }, + }); +} + +export function useDeleteScript() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (name) => + (await api.delete(`/scripts/${encodeURIComponent(name)}`)).data, + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["scripts"] }); + qc.invalidateQueries({ queryKey: ["dashboard"] }); + }, + }); +} + +export function useWorkflows(owner) { + return useQuery({ + queryKey: ["workflows", owner ?? "all"], + queryFn: async () => { + const params = owner ? { owner } : {}; + return (await api.get("/workflows", { params })).data.workflows; + }, + }); +} + +export function useWorkflow(owner, file, enabled = true) { + return useQuery({ + queryKey: ["workflows", owner, file], + queryFn: async () => + (await api.get(`/workflows/${encodeURIComponent(owner)}/${encodeURIComponent(file)}`)) + .data, + enabled: Boolean(owner && file) && enabled, + }); +} + +export function useOwners() { + return useQuery({ + queryKey: ["owners"], + queryFn: async () => (await api.get("/owners")).data.owners, + }); +} + +export function useSaveWorkflow() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async ({ owner, file, content }) => + ( + await api.put( + `/workflows/${encodeURIComponent(owner)}/${encodeURIComponent(file)}`, + { content }, + ) + ).data, + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["workflows"] }); + qc.invalidateQueries({ queryKey: ["owners"] }); + qc.invalidateQueries({ queryKey: ["dashboard"] }); + }, + }); +} + +export function useDeleteWorkflow() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async ({ owner, file }) => + ( + await api.delete( + `/workflows/${encodeURIComponent(owner)}/${encodeURIComponent(file)}`, + ) + ).data, + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["workflows"] }); + qc.invalidateQueries({ queryKey: ["dashboard"] }); + }, + }); +} + +export function useRunWorkflow() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async ({ owner, file, data }) => + ( + await api.post( + `/workflows/${encodeURIComponent(owner)}/${encodeURIComponent(file)}/run`, + data !== undefined ? { data } : {}, + ) + ).data, + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["workflows"] }); + qc.invalidateQueries({ queryKey: ["runs"] }); + qc.invalidateQueries({ queryKey: ["dashboard"] }); + }, + }); +} + +export function useReregisterWorkflows() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async () => (await api.post("/workflows/reregister")).data, + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["workflows"] }); + qc.invalidateQueries({ queryKey: ["scripts"] }); + qc.invalidateQueries({ queryKey: ["owners"] }); + qc.invalidateQueries({ queryKey: ["dashboard"] }); + }, + }); +} + +export function useRuns(filters = {}) { + const { owner, workflow, status, limit } = filters; + return useQuery({ + queryKey: ["runs", { owner, workflow, status, limit }], + queryFn: async () => { + const params = {}; + if (owner) params.owner = owner; + if (workflow) params.workflow = workflow; + if (status) params.status = status; + if (limit) params.limit = limit; + return (await api.get("/runs", { params })).data.runs; + }, + }); +} + +export function useRun(id) { + return useQuery({ + queryKey: ["runs", id], + queryFn: async () => (await api.get(`/runs/${encodeURIComponent(id)}`)).data, + enabled: Boolean(id), + refetchInterval: (query) => + query.state.data?.status === "running" ? 2000 : false, + }); +} + +export function useUsers() { + return useQuery({ + queryKey: ["users"], + queryFn: async () => (await api.get("/users")).data.users, + }); +} + +export function useCreateUser() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (body) => (await api.post("/users", body)).data, + onSuccess: () => qc.invalidateQueries({ queryKey: ["users"] }), + }); +} + +export function useUpdateUser() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async ({ id, ...body }) => + (await api.patch(`/users/${encodeURIComponent(id)}`, body)).data, + onSuccess: () => qc.invalidateQueries({ queryKey: ["users"] }), + }); +} + +export function useDeleteUser() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (id) => + (await api.delete(`/users/${encodeURIComponent(id)}`)).data, + onSuccess: () => qc.invalidateQueries({ queryKey: ["users"] }), + }); +} diff --git a/packages/web/src/components/CodeEditor.jsx b/packages/web/src/components/CodeEditor.jsx new file mode 100644 index 0000000..234e771 --- /dev/null +++ b/packages/web/src/components/CodeEditor.jsx @@ -0,0 +1,30 @@ +import Editor from "@monaco-editor/react"; +import { useTheme } from "../theme.jsx"; + +export function CodeEditor({ language, value, onChange, height = "50vh" }) { + const { theme } = useTheme(); + const isMobile = typeof window !== "undefined" && window.innerWidth < 768; + + return ( +
+ onChange(v ?? "")} + theme={theme === "dark" ? "vs-dark" : "light"} + options={{ + minimap: { enabled: !isMobile }, + lineNumbers: "on", + tabSize: 2, + insertSpaces: true, + autoIndent: "full", + automaticLayout: true, + wordWrap: "on", + scrollBeyondLastLine: false, + fontSize: 13, + }} + /> +
+ ); +} diff --git a/packages/web/src/components/Layout.jsx b/packages/web/src/components/Layout.jsx new file mode 100644 index 0000000..b77e334 --- /dev/null +++ b/packages/web/src/components/Layout.jsx @@ -0,0 +1,93 @@ +import { NavLink, useNavigate } from "react-router-dom"; +import { + LuActivity, + LuCode, + LuGitBranch, + LuHouse, + LuLogOut, + LuMenu, + LuMoon, + LuSun, + LuUsers, +} from "react-icons/lu"; +import { useLogout } from "../api/hooks.js"; +import { useTheme } from "../theme.jsx"; + +const links = [ + { to: "/", label: "Home", icon: LuHouse, end: true }, + { to: "/scripts", label: "Scripts", icon: LuCode }, + { to: "/workflows", label: "Workflows", icon: LuGitBranch }, + { to: "/events", label: "Events", icon: LuActivity }, +]; + +export function Layout({ user, children }) { + const { theme, toggle } = useTheme(); + const logout = useLogout(); + const navigate = useNavigate(); + + const navItems = [ + ...links, + ...(user.role === "admin" + ? [{ to: "/users", label: "Users", icon: LuUsers }] + : []), + ]; + + function closeDrawer() { + const el = document.getElementById("nav-drawer"); + if (el) el.checked = false; + } + + return ( +
+ +
+
+
+ +
+
scrunner
+
+ + {user.username} + +
+
+
{children}
+
+
+
+
+ ); +} diff --git a/packages/web/src/components/MermaidDiagram.jsx b/packages/web/src/components/MermaidDiagram.jsx new file mode 100644 index 0000000..93f748f --- /dev/null +++ b/packages/web/src/components/MermaidDiagram.jsx @@ -0,0 +1,75 @@ +import { useEffect, useId, useRef } from "react"; +import { useNavigate } from "react-router-dom"; +import mermaid from "mermaid"; +import { useTheme } from "../theme.jsx"; + +function findNode(root, id) { + return ( + root.querySelector(`#${CSS.escape(id)}`) || + root.querySelector(`[id="${CSS.escape(id)}"]`) || + root.querySelector(`[id*="${CSS.escape(id)}"]`) + ); +} + +export function MermaidDiagram({ chart, scriptIds = {} }) { + const { theme } = useTheme(); + const navigate = useNavigate(); + const ref = useRef(null); + const reactId = useId().replace(/:/g, ""); + + const scriptKey = JSON.stringify(scriptIds); + + useEffect(() => { + if (!chart || !ref.current) { + if (ref.current) ref.current.innerHTML = ""; + return; + } + + let cancelled = false; + mermaid.initialize({ + startOnLoad: false, + securityLevel: "loose", + suppressErrorRendering: true, + theme: theme === "dark" ? "dark" : "default", + }); + + const ids = JSON.parse(scriptKey); + const renderId = `mmd-${reactId}-${Date.now()}`; + mermaid + .render(renderId, chart) + .then(({ svg }) => { + if (cancelled || !ref.current) return; + ref.current.innerHTML = svg; + for (const [nodeId, script] of Object.entries(ids)) { + const el = findNode(ref.current, nodeId); + if (!el) continue; + el.style.cursor = "pointer"; + el.setAttribute("title", script); + el.addEventListener("click", () => { + navigate(`/scripts?edit=${encodeURIComponent(script)}`); + }); + } + }) + .catch((err) => { + if (cancelled || !ref.current) return; + ref.current.innerHTML = ""; + const p = document.createElement("p"); + p.className = "text-error text-sm"; + p.textContent = "Could not render diagram"; + ref.current.appendChild(p); + }); + + return () => { + cancelled = true; + }; + }, [chart, scriptKey, theme, navigate, reactId]); + + if (!chart) return null; + + return ( +
+ ); +} diff --git a/packages/web/src/index.css b/packages/web/src/index.css new file mode 100644 index 0000000..de45d5a --- /dev/null +++ b/packages/web/src/index.css @@ -0,0 +1,14 @@ +@import "tailwindcss"; +@plugin "daisyui" { + themes: light --default, dark --prefersdark; +} + +html, +body, +#root { + min-height: 100%; +} + +.mermaid-clickable [id^="s"] { + cursor: pointer; +} diff --git a/packages/web/src/lib/format.jsx b/packages/web/src/lib/format.jsx new file mode 100644 index 0000000..1cd3b04 --- /dev/null +++ b/packages/web/src/lib/format.jsx @@ -0,0 +1,51 @@ +export function formatTime(iso) { + if (!iso) return "—"; + const d = new Date(iso); + if (Number.isNaN(d.getTime())) return "—"; + return d.toLocaleString(); +} + +export function StatusBadge({ status }) { + const cls = + status === "success" + ? "badge-success" + : status === "failed" + ? "badge-error" + : status === "running" + ? "badge-warning" + : "badge-ghost"; + return {status ?? "—"}; +} + +/** Config load + last-run health for the workflows list. */ +export function WorkflowStatusBadge({ workflow }) { + if (workflow.loadError) { + return ( + + broken + + ); + } + if (!workflow.enabled) { + return disabled; + } + if (workflow.lastStatus === "failed") { + return failed; + } + if (workflow.lastStatus === "running") { + return running; + } + if (workflow.lastStatus === "success") { + return working; + } + return never run; +} + +export function levelName(level) { + if (level <= 10) return "trace"; + if (level <= 20) return "debug"; + if (level <= 30) return "info"; + if (level <= 40) return "warn"; + if (level <= 50) return "error"; + return "fatal"; +} diff --git a/packages/web/src/lib/workflow-mermaid.js b/packages/web/src/lib/workflow-mermaid.js new file mode 100644 index 0000000..b1b98a7 --- /dev/null +++ b/packages/web/src/lib/workflow-mermaid.js @@ -0,0 +1,67 @@ +function sanitize(label) { + const cleaned = String(label ?? "") + .replace(/[^A-Za-z0-9]+/g, "_") + .replace(/_+/g, "_") + .replace(/^_|_$/g, "") + .slice(0, 48); + return cleaned || "node"; +} + +function parseStep(step) { + if (typeof step === "string") return step; + if (step?.script) return step.script; + return null; +} + +export function workflowToArchitecture(parsed) { + /** @type {Record} */ + const scriptIds = {}; + if (!parsed || typeof parsed !== "object") { + return { chart: "", scriptIds }; + } + + const groupId = "wf"; + const name = sanitize(parsed.name || "workflow"); + const lines = ["architecture-beta", ` group ${groupId}[${name}]`]; + + const triggerIds = []; + const triggers = Array.isArray(parsed.triggers) ? parsed.triggers : []; + triggers.forEach((t, i) => { + const id = `t${i}`; + const icon = t?.type === "cron" ? "cloud" : "internet"; + const label = + t?.type === "cron" + ? `cron ${t.schedule ?? ""}` + : `${t?.method ?? "POST"} ${t?.path ?? ""}`; + lines.push(` service ${id}(${icon})[${sanitize(label)}] in ${groupId}`); + triggerIds.push(id); + }); + + const scripts = Array.isArray(parsed.scripts) ? parsed.scripts : []; + let prev = null; + let firstScript = null; + scripts.forEach((step, i) => { + const script = parseStep(step); + if (!script) return; + const id = `s${i}`; + scriptIds[id] = script; + lines.push(` service ${id}(server)[${sanitize(script)}] in ${groupId}`); + if (!firstScript) firstScript = id; + if (prev) lines.push(` ${prev}:R -- L:${id}`); + prev = id; + }); + + // All triggers point at the first script (not chained to each other). + if (firstScript) { + for (const tid of triggerIds) { + lines.push(` ${tid}:R -- L:${firstScript}`); + } + } + + // Keep multiple triggers in one column so they don't overlap. + if (triggerIds.length >= 2) { + lines.push(` align column ${triggerIds.join(" ")}`); + } + + return { chart: lines.join("\n"), scriptIds }; +} diff --git a/packages/web/src/main.jsx b/packages/web/src/main.jsx new file mode 100644 index 0000000..22ad123 --- /dev/null +++ b/packages/web/src/main.jsx @@ -0,0 +1,28 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { BrowserRouter } from "react-router-dom"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { ThemeProvider } from "./theme.jsx"; +import { App } from "./App.jsx"; +import "./index.css"; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 10_000, + refetchOnWindowFocus: false, + }, + }, +}); + +createRoot(document.getElementById("root")).render( + + + + + + + + + , +); diff --git a/packages/web/src/pages/AuthPage.jsx b/packages/web/src/pages/AuthPage.jsx new file mode 100644 index 0000000..5bfb0d2 --- /dev/null +++ b/packages/web/src/pages/AuthPage.jsx @@ -0,0 +1,64 @@ +import { useState } from "react"; +import { LuLogIn, LuUserPlus } from "react-icons/lu"; +import { errorMessage } from "../api/client.js"; +import { useLogin, useRegister } from "../api/hooks.js"; + +export function AuthPage({ needsSetup }) { + const login = useLogin(); + const register = useRegister(); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const mutation = needsSetup ? register : login; + const error = mutation.isError ? errorMessage(mutation.error) : null; + + function onSubmit(e) { + e.preventDefault(); + mutation.mutate({ username, password }); + } + + return ( +
+
+ + {needsSetup ? "Create admin" : "Sign in"} + + + setUsername(e.target.value)} + required + /> + + setPassword(e.target.value)} + required + minLength={8} + /> + {error ?

{error}

: null} + +
+
+ ); +} diff --git a/packages/web/src/pages/EventDetailPage.jsx b/packages/web/src/pages/EventDetailPage.jsx new file mode 100644 index 0000000..f7b1467 --- /dev/null +++ b/packages/web/src/pages/EventDetailPage.jsx @@ -0,0 +1,129 @@ +import { useState } from "react"; +import { Link, useParams } from "react-router-dom"; +import { LuArrowLeft } from "react-icons/lu"; +import { useRun } from "../api/hooks.js"; +import { formatTime, levelName, StatusBadge } from "../lib/format.jsx"; + +export function EventDetailPage() { + const { id } = useParams(); + const { data: run, isLoading, error } = useRun(id); + const [wordWrap, setWordWrap] = useState(true); + + if (isLoading) return ; + if (error || !run) return

Event not found

; + + return ( +
+
+ + + +

{run.workflow_name || run.workflow}

+ +
+ +
+
+
Started
+
{formatTime(run.started_at)}
+
+
+
Finished
+
{formatTime(run.finished_at)}
+
+
+
Trigger
+
+ {run.trigger_type} + {run.trigger_detail ? ` · ${run.trigger_detail}` : ""} +
+
+
+
Duration
+
{run.duration_ms != null ? `${run.duration_ms}ms` : "—"}
+
+
+ + {run.error ? ( +
+ {run.error} +
+ ) : null} + +
+

Steps

+
+ + + + + + + + + + + + {(run.steps ?? []).map((s) => ( + + + + + + + + ))} + +
#ScriptStatusDurationError
{s.step_index} + + {s.script} + + + + {s.duration_ms != null ? `${s.duration_ms}ms` : "—"}{s.error || ""}
+
+
+ +
+
+

Logs

+ +
+
+ {(run.logs ?? []).length === 0 ? ( +
+ — + no logs +
+ ) : ( + run.logs.map((l) => ( +
+ + {levelName(l.level)} + + + {l.ts} {l.msg ?? ""} + {l.payload ? ` ${JSON.stringify(l.payload)}` : ""} + +
+ )) + )} +
+
+
+ ); +} diff --git a/packages/web/src/pages/EventsPage.jsx b/packages/web/src/pages/EventsPage.jsx new file mode 100644 index 0000000..fc4e22e --- /dev/null +++ b/packages/web/src/pages/EventsPage.jsx @@ -0,0 +1,82 @@ +import { Link, useSearchParams } from "react-router-dom"; +import { useRuns } from "../api/hooks.js"; +import { formatTime, StatusBadge } from "../lib/format.jsx"; + +export function EventsPage() { + const [params, setParams] = useSearchParams(); + const workflow = params.get("workflow") || ""; + const status = params.get("status") || ""; + const { data: runs = [], isLoading } = useRuns({ + workflow: workflow || undefined, + status: status || undefined, + limit: 100, + }); + + function update(key, value) { + const next = new URLSearchParams(params); + if (value) next.set(key, value); + else next.delete(key); + setParams(next); + } + + return ( +
+

Events

+
+ update("workflow", e.target.value)} + /> + +
+ {isLoading ? ( + + ) : ( +
+ + + + + + + + + + + + {runs.map((r) => ( + + + + + + + + ))} + +
StatusWorkflowTriggerStartedDuration
+ + + + {r.workflow_name || r.workflow} + + + {r.trigger_type} + {r.trigger_detail ? ` · ${r.trigger_detail}` : ""} + {formatTime(r.started_at)}{r.duration_ms != null ? `${r.duration_ms}ms` : "—"}
+
+ )} +
+ ); +} diff --git a/packages/web/src/pages/HomePage.jsx b/packages/web/src/pages/HomePage.jsx new file mode 100644 index 0000000..c8cd838 --- /dev/null +++ b/packages/web/src/pages/HomePage.jsx @@ -0,0 +1,124 @@ +import { Link } from "react-router-dom"; +import { + LuActivity, + LuCode, + LuGitBranch, + LuTriangleAlert, +} from "react-icons/lu"; +import { useDashboard } from "../api/hooks.js"; +import { formatTime, StatusBadge } from "../lib/format.jsx"; + +export function HomePage() { + const { data, isLoading, error } = useDashboard(); + + if (isLoading) { + return ; + } + if (error) { + return

Failed to load dashboard

; + } + + const failed = data.needsAttention?.failed ?? []; + const broken = data.needsAttention?.brokenWorkflows ?? []; + + return ( +
+
+
+
+ +
+
Workflows
+
{data.workflowCount}
+
{data.enabledCount} enabled
+
+
+
+ +
+
Scripts
+
{data.scriptCount}
+
+
+
+ +
+
Running
+
{data.running?.length ?? 0}
+
+
+
+ +
+
Attention
+
+ {failed.length + broken.length} +
+
+
+ +
+

Running

+ +
+ +
+

Needs attention

+ {broken.length > 0 ? ( +
    + {broken.map((w) => ( +
  • + + {w.key}: {w.loadError} + +
  • + ))} +
+ ) : null} + +
+ +
+

Recent

+ +
+
+ ); +} + +function RunList({ runs, empty }) { + if (!runs?.length) { + return empty ?

{empty}

: null; + } + return ( +
+ + + + + + + + + + {runs.map((r) => ( + + + + + + ))} + +
StatusWorkflowStarted
+ + + + {r.workflow_name || r.workflow} + + {formatTime(r.started_at)}
+
+ ); +} diff --git a/packages/web/src/pages/ScriptsPage.jsx b/packages/web/src/pages/ScriptsPage.jsx new file mode 100644 index 0000000..8ca9307 --- /dev/null +++ b/packages/web/src/pages/ScriptsPage.jsx @@ -0,0 +1,198 @@ +import { useEffect, useState } from "react"; +import { useSearchParams } from "react-router-dom"; +import { LuPencil, LuPlus, LuSave, LuTrash2, LuX } from "react-icons/lu"; +import { errorMessage } from "../api/client.js"; +import { + useDeleteScript, + useSaveScript, + useScript, + useScripts, +} from "../api/hooks.js"; +import { CodeEditor } from "../components/CodeEditor.jsx"; + +const NEW_TEMPLATE = `export default async function main(ctx) { + return ctx; +} +`; + +export function ScriptsPage() { + const [params, setParams] = useSearchParams(); + const editName = params.get("edit"); + const { data: scripts = [], isLoading } = useScripts(); + const [mode, setMode] = useState(null); + const [name, setName] = useState(""); + const [content, setContent] = useState(""); + const [confirmDelete, setConfirmDelete] = useState(null); + + const existing = useScript(mode === "edit" ? name : null, mode === "edit"); + const save = useSaveScript(); + const del = useDeleteScript(); + + useEffect(() => { + if (editName) { + setMode("edit"); + setName(editName); + } + }, [editName]); + + useEffect(() => { + if (mode === "edit" && existing.data?.content != null) { + setContent(existing.data.content); + } + }, [mode, existing.data]); + + function openAdd() { + setMode("add"); + setName(""); + setContent(NEW_TEMPLATE); + setParams({}); + } + + function openEdit(script) { + setMode("edit"); + setName(script); + setParams({ edit: script }); + } + + function closeForm() { + setMode(null); + setName(""); + setContent(""); + setParams({}); + } + + function onSave(e) { + e.preventDefault(); + const file = name.endsWith(".js") ? name : `${name}.js`; + save.mutate( + { name: file, content }, + { + onSuccess: () => { + setMode("edit"); + setName(file); + setParams({ edit: file }); + }, + }, + ); + } + + return ( +
+
+

Scripts

+ +
+ + {isLoading ? ( + + ) : ( +
+ + + + + + + + {scripts.map((s) => ( + + + + + ))} + +
Name +
{s} + + +
+
+ )} + + {mode ? ( +
+
+

{mode === "add" ? "New script" : name}

+ +
+ {mode === "add" ? ( + setName(e.target.value)} + required + /> + ) : null} + {mode === "edit" && existing.isLoading ? ( + + ) : ( + + )} + {save.isError ? ( +

{errorMessage(save.error)}

+ ) : null} + + + ) : null} + + {confirmDelete ? ( + +
+

Delete {confirmDelete}?

+ {del.isError ? ( +

{errorMessage(del.error)}

+ ) : null} +
+ + +
+
+
+ +
+
+ ) : null} +
+ ); +} diff --git a/packages/web/src/pages/UsersPage.jsx b/packages/web/src/pages/UsersPage.jsx new file mode 100644 index 0000000..bc4dd47 --- /dev/null +++ b/packages/web/src/pages/UsersPage.jsx @@ -0,0 +1,194 @@ +import { useState } from "react"; +import { LuPencil, LuPlus, LuTrash2, LuX } from "react-icons/lu"; +import { errorMessage } from "../api/client.js"; +import { + useCreateUser, + useDeleteUser, + useUpdateUser, + useUsers, +} from "../api/hooks.js"; + +export function UsersPage() { + const { data: users = [], isLoading } = useUsers(); + const create = useCreateUser(); + const update = useUpdateUser(); + const del = useDeleteUser(); + const [mode, setMode] = useState(null); + const [form, setForm] = useState({ + username: "", + password: "", + role: "operator", + id: null, + }); + const [confirmDelete, setConfirmDelete] = useState(null); + + function openAdd() { + setMode("add"); + setForm({ username: "", password: "", role: "operator", id: null }); + } + + function openEdit(u) { + setMode("edit"); + setForm({ username: u.username, password: "", role: u.role, id: u.id }); + } + + function closeForm() { + setMode(null); + } + + function onSubmit(e) { + e.preventDefault(); + if (mode === "add") { + create.mutate( + { + username: form.username, + password: form.password, + role: form.role, + }, + { onSuccess: closeForm }, + ); + } else { + const body = { id: form.id, role: form.role }; + if (form.password) body.password = form.password; + update.mutate(body, { onSuccess: closeForm }); + } + } + + const mutation = mode === "add" ? create : update; + + return ( +
+
+

Users

+ +
+ + {isLoading ? ( + + ) : ( +
+ + + + + + + + + {users.map((u) => ( + + + + + + ))} + +
UsernameRole +
{u.username} + {u.role} + + + +
+
+ )} + + {mode ? ( +
+
+ + {mode === "add" ? "New user" : form.username} + + +
+ {mode === "add" ? ( + <> + + setForm({ ...form, username: e.target.value })} + required + /> + + ) : null} + + setForm({ ...form, password: e.target.value })} + required={mode === "add"} + minLength={mode === "add" ? 8 : undefined} + placeholder={mode === "edit" ? "leave blank to keep" : ""} + /> + + + {mutation.isError ? ( +

{errorMessage(mutation.error)}

+ ) : null} + +
+ ) : null} + + {confirmDelete ? ( + +
+

Delete {confirmDelete.username}?

+ {del.isError ? ( +

{errorMessage(del.error)}

+ ) : null} +
+ + +
+
+
+ +
+
+ ) : null} +
+ ); +} diff --git a/packages/web/src/pages/WorkflowsPage.jsx b/packages/web/src/pages/WorkflowsPage.jsx new file mode 100644 index 0000000..3a9f393 --- /dev/null +++ b/packages/web/src/pages/WorkflowsPage.jsx @@ -0,0 +1,384 @@ +import { useEffect, useMemo, useState } from "react"; +import { Link, useNavigate, useSearchParams } from "react-router-dom"; +import { parse as parseYaml } from "yaml"; +import { + LuActivity, + LuPencil, + LuPlay, + LuPlus, + LuRefreshCw, + LuSave, + LuTrash2, + LuX, +} from "react-icons/lu"; +import { errorMessage } from "../api/client.js"; +import { + useDeleteWorkflow, + useOwners, + useReregisterWorkflows, + useRunWorkflow, + useSaveWorkflow, + useWorkflow, + useWorkflows, +} from "../api/hooks.js"; +import { CodeEditor } from "../components/CodeEditor.jsx"; +import { MermaidDiagram } from "../components/MermaidDiagram.jsx"; +import { formatTime, WorkflowStatusBadge } from "../lib/format.jsx"; +import { workflowToArchitecture } from "../lib/workflow-mermaid.js"; + +const NEW_YAML = `name: new workflow +scripts: + - get-current-time.js +triggers: + - type: HTTP + method: POST + path: /new +`; + +export function WorkflowsPage() { + const navigate = useNavigate(); + const [params, setParams] = useSearchParams(); + const editParam = params.get("edit"); + const { data: workflows = [], isLoading } = useWorkflows(); + const { data: owners = [] } = useOwners(); + const [mode, setMode] = useState(null); + const [owner, setOwner] = useState("default"); + const [file, setFile] = useState(""); + const [content, setContent] = useState(""); + const [confirmDelete, setConfirmDelete] = useState(null); + const [runError, setRunError] = useState(null); + + const existing = useWorkflow( + mode === "edit" ? owner : null, + mode === "edit" ? file : null, + mode === "edit", + ); + const save = useSaveWorkflow(); + const del = useDeleteWorkflow(); + const run = useRunWorkflow(); + const reregister = useReregisterWorkflows(); + + useEffect(() => { + if (!editParam) return; + const slash = editParam.indexOf("/"); + if (slash === -1) return; + setMode("edit"); + setOwner(editParam.slice(0, slash)); + setFile(editParam.slice(slash + 1)); + }, [editParam]); + + useEffect(() => { + if (mode === "edit" && existing.data?.content != null) { + setContent(existing.data.content); + } + }, [mode, existing.data]); + + const { parsed, parseError, mermaid } = useMemo(() => { + try { + const parsedYaml = parseYaml(content); + const arch = workflowToArchitecture(parsedYaml); + return { parsed: parsedYaml, parseError: null, mermaid: arch }; + } catch (err) { + return { + parsed: null, + parseError: err instanceof Error ? err.message : String(err), + mermaid: { chart: "", scriptIds: {} }, + }; + } + }, [content]); + + function openAdd() { + setMode("add"); + setOwner(owners[0] || "default"); + setFile(""); + setContent(NEW_YAML); + setParams({}); + } + + function openEdit(w) { + setMode("edit"); + setOwner(w.owner); + setFile(w.file); + setParams({ edit: `${w.owner}/${w.file}` }); + } + + function closeForm() { + setMode(null); + setFile(""); + setContent(""); + setParams({}); + } + + function onSave(e) { + e.preventDefault(); + const yamlFile = file.endsWith(".yaml") || file.endsWith(".yml") ? file : `${file}.yaml`; + save.mutate( + { owner, file: yamlFile, content }, + { + onSuccess: () => { + setMode("edit"); + setFile(yamlFile); + setParams({ edit: `${owner}/${yamlFile}` }); + }, + }, + ); + } + + function onRun(w) { + setRunError(null); + run.mutate( + { owner: w.owner, file: w.file }, + { + onSuccess: (data) => { + if (data?.runId) navigate(`/events/${data.runId}`); + }, + onError: (err) => { + const runId = err?.response?.data?.runId; + if (runId) { + navigate(`/events/${runId}`); + return; + } + setRunError(`${w.key}: ${errorMessage(err)}`); + }, + }, + ); + } + + const runningKey = + run.isPending && run.variables + ? `${run.variables.owner}/${run.variables.file}` + : null; + + return ( +
+
+

Workflows

+
+ + +
+
+ + {reregister.isError ? ( +

{errorMessage(reregister.error)}

+ ) : null} + + {runError ?

{runError}

: null} + + {isLoading ? ( + + ) : ( +
+ + + + + + + + + + + + + {workflows.map((w) => ( + + + + + + + + + + ))} + +
NameOwnerStatusTriggersLast runRuns +
{w.name}{w.owner} + + + + {formatTime(w.lastInvokedAt)}{w.invocationCount} + + + + + + +
+
+ )} + + {mode ? ( +
+
+

+ {mode === "add" ? "New workflow" : `${owner}/${file}`} +

+ +
+ {mode === "add" ? ( +
+ setOwner(e.target.value)} + required + /> + setFile(e.target.value)} + required + /> +
+ ) : null} + {mode === "edit" && existing.isLoading ? ( + + ) : ( +
+ +
+ {parseError ? ( +

{parseError}

+ ) : ( + + )} + {parsed?.name ? ( +

{parsed.name}

+ ) : null} +
+
+ )} + {save.isError ? ( +

{errorMessage(save.error)}

+ ) : null} + + + ) : null} + + {confirmDelete ? ( + +
+

Delete {confirmDelete.key}?

+ {del.isError ? ( +

{errorMessage(del.error)}

+ ) : null} +
+ + +
+
+
+ +
+
+ ) : null} +
+ ); +} + +function triggerLabel(t) { + const type = String(t?.type ?? "").toLowerCase(); + if (type === "cron") return t.schedule || "cron"; + if (type === "http") return t.path || "/"; + return t?.type ?? "—"; +} + +function triggerKind(t) { + const type = String(t?.type ?? "").toLowerCase(); + if (type === "http") return t.method || "POST"; + return t?.type ?? "—"; +} + +function TriggerList({ triggers }) { + if (!triggers?.length) return —; + return ( +
    + {triggers.map((t, i) => ( +
  • + {triggerKind(t)} {triggerLabel(t)} +
  • + ))} +
+ ); +} diff --git a/packages/web/src/theme.jsx b/packages/web/src/theme.jsx new file mode 100644 index 0000000..701c37e --- /dev/null +++ b/packages/web/src/theme.jsx @@ -0,0 +1,27 @@ +import { createContext, useContext, useEffect, useState } from "react"; + +const ThemeContext = createContext(null); + +export function ThemeProvider({ children }) { + const [theme, setTheme] = useState(() => { + if (typeof localStorage === "undefined") return "light"; + return localStorage.getItem("theme") || "light"; + }); + + useEffect(() => { + document.documentElement.setAttribute("data-theme", theme); + localStorage.setItem("theme", theme); + }, [theme]); + + const toggle = () => setTheme((t) => (t === "dark" ? "light" : "dark")); + + return ( + + {children} + + ); +} + +export function useTheme() { + return useContext(ThemeContext); +} diff --git a/packages/web/vite.config.js b/packages/web/vite.config.js new file mode 100644 index 0000000..0c6e009 --- /dev/null +++ b/packages/web/vite.config.js @@ -0,0 +1,14 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; + +export default defineConfig({ + plugins: [react(), tailwindcss()], + server: { + port: 5173, + proxy: { + "/api": { target: "http://127.0.0.1:9000", changeOrigin: true }, + "/admin": { target: "http://127.0.0.1:9000", changeOrigin: true }, + }, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e55a8bf..1564770 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,11 +6,28 @@ settings: importers: - .: + .: {} + + packages/server: dependencies: + '@fastify/cookie': + specifier: ^11.0.2 + version: 11.1.2 + '@fastify/cors': + specifier: ^11.1.0 + version: 11.3.0 + '@fastify/jwt': + specifier: ^9.1.0 + version: 9.1.0 + '@fastify/static': + specifier: ^8.2.0 + version: 8.3.0 axios: specifier: ^1.19.0 version: 1.19.0 + bcryptjs: + specifier: ^3.0.2 + version: 3.0.3 better-sqlite3: specifier: ^13.0.3 version: 13.0.3 @@ -36,11 +53,314 @@ importers: specifier: ^2.9.0 version: 2.9.0 + packages/web: + dependencies: + '@monaco-editor/react': + specifier: ^4.7.0 + version: 4.7.0(monaco-editor@0.56.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@tanstack/react-query': + specifier: ^5.84.0 + version: 5.101.4(react@19.2.8) + axios: + specifier: ^1.11.0 + version: 1.19.0 + mermaid: + specifier: ^11.9.0 + version: 11.16.1 + react: + specifier: ^19.1.1 + version: 19.2.8 + react-dom: + specifier: ^19.1.1 + version: 19.2.8(react@19.2.8) + react-icons: + specifier: ^5.5.0 + version: 5.7.0(react@19.2.8) + react-router-dom: + specifier: ^7.8.0 + version: 7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + yaml: + specifier: ^2.8.1 + version: 2.9.0 + devDependencies: + '@tailwindcss/vite': + specifier: ^4.1.11 + version: 4.3.3(vite@7.3.6(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0)) + '@vitejs/plugin-react': + specifier: ^5.0.0 + version: 5.2.0(vite@7.3.6(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0)) + daisyui: + specifier: ^5.0.50 + version: 5.7.16 + tailwindcss: + specifier: ^4.1.11 + version: 4.3.3 + vite: + specifier: ^7.1.2 + version: 7.3.6(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0) + packages: + '@antfu/install-pkg@1.1.0': + resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.8': + resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.8': + resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.29.7': + resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.29.7': + resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.8': + resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + + '@braintree/sanitize-url@7.1.2': + resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} + + '@chevrotain/types@11.1.2': + resolution: {integrity: sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==} + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@fastify/accept-negotiator@2.1.0': + resolution: {integrity: sha512-F3EVbzWt+xcnVaOHmWyIlpuFtbxOln7HDZQsh09MtMmMm/CipMayNt8hnIL8VQi54u2ZociDbf+iluGYkf7B1A==} + '@fastify/ajv-compiler@4.0.6': resolution: {integrity: sha512-NtuzM0SfaMJbGlnjr9LWQUN5LzgSrbB8tf/wRZNas+4E1O/Nmzl53e7ruT61HDZyRCJGC6FxIogmNZO1c5ETBA==} + '@fastify/cookie@11.1.2': + resolution: {integrity: sha512-Dtrpk/YOGUsbRMvP/8ZqPpwnMRv0qSqodFdoQ2B589Obc7jw4s4Qla+cV72Bsm7WsZJnqlYFX/i7uSBq0xzg6g==} + + '@fastify/cors@11.3.0': + resolution: {integrity: sha512-ggQGua+xHv1MvePbPr0v//xLYEsCXbWspquXCJS9Ot5YoRXq8J8ZWzHnxDBVnbtXosvistXo6LtNzOJswf64Fw==} + '@fastify/error@4.2.0': resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} @@ -50,15 +370,422 @@ packages: '@fastify/forwarded@3.0.2': resolution: {integrity: sha512-NE8HgKLgYejV9lDpqkEFaDKMLYelJBVfHekhB0UKvX0ghagXRJqg68feg8er1NPXxG4N9i6vPxzt8E+3wHfcmA==} + '@fastify/jwt@9.1.0': + resolution: {integrity: sha512-CiGHCnS5cPMdb004c70sUWhQTfzrJHAeTywt7nVw6dAiI0z1o4WRvU94xfijhkaId4bIxTCOjFgn4sU+Gvk43w==} + '@fastify/merge-json-schemas@0.2.1': resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} '@fastify/proxy-addr@5.1.0': resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} + '@fastify/send@4.1.1': + resolution: {integrity: sha512-BYo+EiaKwlxH+WetGk6hAs1d39iP0y1gqB8lGF/qwkJ9ZZ/cBY1vx5NvExb9Sc3yRMFjD5X4Eyh4e4+TzRkzdw==} + + '@fastify/static@8.3.0': + resolution: {integrity: sha512-yKxviR5PH1OKNnisIzZKmgZSus0r2OZb8qCSbqmw34aolT4g3UlzYfeBRym+HJ1J471CR8e2ldNub4PubD1coA==} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/utils@3.1.4': + resolution: {integrity: sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw==} + + '@isaacs/cliui@9.0.0': + resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} + engines: {node: '>=18'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@lukeed/ms@2.0.2': + resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} + engines: {node: '>=8'} + + '@mermaid-js/parser@1.2.0': + resolution: {integrity: sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA==} + + '@monaco-editor/loader@1.7.0': + resolution: {integrity: sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==} + + '@monaco-editor/react@4.7.0': + resolution: {integrity: sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==} + peerDependencies: + monaco-editor: '>= 0.25.0 < 1' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + '@rolldown/pluginutils@1.0.0-rc.3': + resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} + + '@rollup/rollup-android-arm-eabi@4.62.4': + resolution: {integrity: sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.4': + resolution: {integrity: sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.4': + resolution: {integrity: sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.4': + resolution: {integrity: sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.4': + resolution: {integrity: sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.4': + resolution: {integrity: sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.4': + resolution: {integrity: sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.62.4': + resolution: {integrity: sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.62.4': + resolution: {integrity: sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.62.4': + resolution: {integrity: sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.62.4': + resolution: {integrity: sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.62.4': + resolution: {integrity: sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.62.4': + resolution: {integrity: sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.62.4': + resolution: {integrity: sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.62.4': + resolution: {integrity: sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.62.4': + resolution: {integrity: sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.62.4': + resolution: {integrity: sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.62.4': + resolution: {integrity: sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.62.4': + resolution: {integrity: sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.62.4': + resolution: {integrity: sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.4': + resolution: {integrity: sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.4': + resolution: {integrity: sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.4': + resolution: {integrity: sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.4': + resolution: {integrity: sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.4': + resolution: {integrity: sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==} + cpu: [x64] + os: [win32] + + '@tailwindcss/node@4.3.3': + resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} + + '@tailwindcss/oxide-android-arm64@4.3.3': + resolution: {integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + resolution: {integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.3': + resolution: {integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + resolution: {integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + resolution: {integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + resolution: {integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + resolution: {integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.3': + resolution: {integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.3.3': + resolution: {integrity: sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + + '@tanstack/query-core@5.101.4': + resolution: {integrity: sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw==} + + '@tanstack/react-query@5.101.4': + resolution: {integrity: sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA==} + peerDependencies: + react: ^18 || ^19 + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-axis@3.0.6': + resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} + + '@types/d3-brush@3.0.6': + resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} + + '@types/d3-chord@3.0.6': + resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-contour@3.0.6': + resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} + + '@types/d3-delaunay@6.0.4': + resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} + + '@types/d3-dispatch@3.0.7': + resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-dsv@3.0.7': + resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-fetch@3.0.7': + resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} + + '@types/d3-force@3.0.10': + resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} + + '@types/d3-format@3.0.4': + resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} + + '@types/d3-geo@3.1.1': + resolution: {integrity: sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w==} + + '@types/d3-hierarchy@3.1.7': + resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-polygon@3.0.2': + resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} + + '@types/d3-quadtree@3.0.6': + resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} + + '@types/d3-random@3.0.4': + resolution: {integrity: sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA==} + + '@types/d3-scale-chromatic@3.1.0': + resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time-format@4.0.3': + resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/d3-transition@3.0.9': + resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} + + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + + '@types/d3@7.4.3': + resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@upsetjs/venn.js@2.0.0': + resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==} + + '@vitejs/plugin-react@5.2.0': + resolution: {integrity: sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + abstract-logging@2.0.1: resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} @@ -77,6 +804,9 @@ packages: ajv@8.20.0: resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + asn1.js@5.4.1: + resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -90,14 +820,42 @@ packages: axios@1.19.0: resolution: {integrity: sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.11.14: + resolution: {integrity: sha512-JyJ954WzuIR8/FFzX0o5krdSTrBAkcCSRfWSleRsIHSWV+cZe2FI1PKggVkFke1hBldRs+LRxUczzE9iPmgZww==} + engines: {node: '>=6.0.0'} + hasBin: true + + bcryptjs@3.0.3: + resolution: {integrity: sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==} + hasBin: true + better-sqlite3@13.0.3: resolution: {integrity: sha512-RbOBxmLBG8uvFUc15X9+9SFemKcQ0WBuISBVkpuiaUB2qblC8UWlHEjdWVoZ8AdhSwmoEgsiXKfopX0CQxaACQ==} engines: {node: '>=22'} + bn.js@4.12.5: + resolution: {integrity: sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==} + + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + engines: {node: 20 || >=22} + + browserslist@4.28.8: + resolution: {integrity: sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} + caniuse-lite@1.0.30001809: + resolution: {integrity: sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==} + colorette@2.0.19: resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==} @@ -109,13 +867,204 @@ packages: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@1.1.1: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} + cookie@2.0.1: + resolution: {integrity: sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w==} + engines: {node: '>=22'} + + cose-base@1.0.3: + resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} + + cose-base@2.2.0: + resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cytoscape-cose-bilkent@4.1.0: + resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape-fcose@2.2.0: + resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape@3.34.1: + resolution: {integrity: sha512-Lr0RvH9H75y9ar8h9Toy6u4lxRSCcxUq+hHcQ26sVWo6BnaQp1gwEZOYqwuYTZhyW7npyKnNLP8oJ2p1/3OZ7g==} + engines: {node: '>=0.10'} + + d3-array@2.12.1: + resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + + d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + + d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@1.0.9: + resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + + d3-sankey@0.12.3: + resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-shape@1.3.7: + resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + + dagre-d3-es@7.0.14: + resolution: {integrity: sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==} + + daisyui@5.7.16: + resolution: {integrity: sha512-V9CJxvrWIXTDuP/0tpijjWaKaKJNYo2IrULJfTWs3/DjW9rPqswk1n8NCLQRLrTutmlG2Ech7nkJDpCnF+6Dzw==} + date-fns@4.4.0: resolution: {integrity: sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==} + dayjs@1.11.21: + resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==} + debug@4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -134,18 +1083,45 @@ packages: supports-color: optional: true + delaunator@5.1.0: + resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dompurify@3.4.13: + resolution: {integrity: sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==} + + dompurify@3.4.8: + resolution: {integrity: sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ==} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + electron-to-chromium@1.5.406: + resolution: {integrity: sha512-hWH5ORBi3d0IipnMh7BN5GDTaAmrSSSWmznwt2zltdiRNEWoEQyTwF0FFSBxzHO7hLSRT6loQu3IQGV0wg/Tvg==} + + enhanced-resolve@5.24.5: + resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==} + engines: {node: '>=10.13.0'} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -162,10 +1138,21 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} + es-toolkit@1.50.0: + resolution: {integrity: sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==} + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + esm@3.2.25: resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==} engines: {node: '>=6'} @@ -179,6 +1166,10 @@ packages: fast-json-stringify@7.0.1: resolution: {integrity: sha512-eRSayARSbbwlBjpP4vnTTIRD5QPcIrmihPxDeN1DtKnHPg66UuJLx+8hlK1kaFdjvzyQ/dzALoi4vwAQ+T+iZA==} + fast-jwt@5.0.6: + resolution: {integrity: sha512-LPE7OCGUl11q3ZgW681cEU2d0d2JZ37hhJAmetCgNyW8waVaJVZXhyFF6U2so1Iim58Yc7pfxJe2P7MNetQH2g==} + engines: {node: '>=20'} + fast-querystring@1.1.2: resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} @@ -188,12 +1179,37 @@ packages: fast-uri@4.1.2: resolution: {integrity: sha512-TyGmBcbDTZXcb2cj5MV89DrF42DKvb3y5DDUNh95iO+IMeAzMkVSxK1PZRrRIpc9yg8U2GhGdbofNa0LS/a4Bw==} + fastfall@1.5.1: + resolution: {integrity: sha512-KH6p+Z8AKPXnmA7+Iz2Lh8ARCMr+8WNPVludm1LGkZoD2MjY6LVnRMtTKhkdzI+jr0RzQWXKzKyBJm1zoHEL4Q==} + engines: {node: '>=0.10.0'} + + fastify-plugin@5.1.0: + resolution: {integrity: sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==} + + fastify-plugin@6.0.0: + resolution: {integrity: sha512-fZOty7z3O7vOliF6d8bHE3wiEh1KcNnKEQensSgTk9C1DvN6nRLS++XVd86v33Hw/8u9Un8A1zDrQ8ujcQDHEg==} + fastify@5.12.0: resolution: {integrity: sha512-A3RNEaDIHWaxFW8n8rNJaW1wQ+XAXuoU71llfUQJjuh5WaYLmKfRhhanaJBOx8m2EBPQkR6sDBovYdHNe9F6rA==} + fastparallel@2.4.1: + resolution: {integrity: sha512-qUmhxPgNHmvRjZKBFUNI0oZuuH9OlSIOXmJ98lhKPxMZZ7zS/Fi0wRHOihDSz0R1YiIOjxzOY4bq65YTcdBi2Q==} + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + fastseries@1.7.2: + resolution: {integrity: sha512-dTPFrPGS8SNSzAt7u/CbMKCJ3s01N04s4JFbORHcmyvVfVKmbhMD1VtRbh5enGHxkaQDqWyLefiKOGGmohGDDQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + find-my-way@9.7.0: resolution: {integrity: sha512-f2JHn75x2JlwUwLenZypgczR7YWMb/uO9BvUXtus+JMgkbIkLADd38cI4EiV+OQqrGo1Zlq6V8wnqMJ8e62wUQ==} engines: {node: '>=20'} @@ -207,13 +1223,26 @@ packages: debug: optional: true + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + form-data@4.0.6: resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} engines: {node: '>= 6'} + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -229,10 +1258,22 @@ packages: getopts@2.3.0: resolution: {integrity: sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==} + glob@11.1.0: + resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} + engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + hachure-fill@0.5.2: + resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -245,10 +1286,31 @@ packages: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + https-proxy-agent@5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + import-meta-resolve@4.2.0: + resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + internmap@1.0.1: + resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + interpret@2.2.0: resolution: {integrity: sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==} engines: {node: '>= 0.10'} @@ -261,16 +1323,47 @@ packages: resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} engines: {node: '>= 0.4'} + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@4.2.3: + resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} + engines: {node: 20 || >=22} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + json-schema-ref-resolver@3.0.0: resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + jsonata@2.2.2: resolution: {integrity: sha512-XDFH2PuaAXv0AXJEWwElXbqADmKFGKMLMkFb+qOz0EhjQW2EIJ6pgtordLU+4u3IVERyBfqy6bi6kZKEncvRqA==} engines: {node: '>= 8'} + katex@0.16.47: + resolution: {integrity: sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==} + hasBin: true + + khroma@2.1.0: + resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} + knex@3.3.0: resolution: {integrity: sha512-LgWl031hNuLv9Lhxdd9093zULa2aNcoP04Bk29e5NidgRcEr/T0rAr2WYi4BhjTiCDoQiRU56meUE5rppLKZzQ==} engines: {node: '>=16'} @@ -305,16 +1398,118 @@ packages: tedious: optional: true + layout-base@1.0.2: + resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} + + layout-base@2.0.1: + resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} + light-my-request@6.6.0: resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} + lodash@4.18.1: resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + marked@14.0.0: + resolution: {integrity: sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==} + engines: {node: '>= 18'} + hasBin: true + + marked@16.4.2: + resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==} + engines: {node: '>= 20'} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mermaid@11.16.1: + resolution: {integrity: sha512-TQsq6u22fAn3rek5VOubrhKPo1g5hwC3FXUN9hiyupTckcYiGuuKGkNQrKYwGJkXUxZdojwRG46gsSCFZMDp4g==} + mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -323,12 +1518,39 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + mnemonist@0.40.4: + resolution: {integrity: sha512-ZAv+KNavneRVzu4tUeOgzkScI3W5BGwZ3rkxIpKtzzVgfTtWQFN1CgX0U72cyvyh3iTuHL3SiSmrQxTlryEIcw==} + + monaco-editor@0.56.0: + resolution: {integrity: sha512-sXboRm3BeBeLm938eaiyLMe0OxzfXIlZvbv4ir/jVgQy1zDhWjgmny0WoN45fuDKhCCQsYMbBJrv/A6jd8aCUg==} + ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + node-addon-api@8.9.2: resolution: {integrity: sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg==} engines: {node: ^18 || ^20 || >= 21} @@ -337,16 +1559,47 @@ packages: resolution: {integrity: sha512-Si/bzYiKRHOB8/a99T2+SDGN582ONDMSTlJr5oCkT6GtnqPjZ2s10eoQRYkW9ZHwjVxONL+W8Fb+qR0AHMQsdg==} engines: {node: '>=20'} + node-releases@2.0.53: + resolution: {integrity: sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==} + engines: {node: '>=18'} + + obliterator@2.0.5: + resolution: {integrity: sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==} + on-exit-leak-free@2.1.2: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + package-manager-detector@1.8.0: + resolution: {integrity: sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==} + + path-data-parser@0.1.0: + resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + pg-connection-string@2.6.2: resolution: {integrity: sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==} + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + pino-abstract-transport@3.0.0: resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} @@ -360,6 +1613,16 @@ packages: resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} hasBin: true + points-on-curve@0.2.0: + resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} + + points-on-path@0.2.1: + resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} + + postcss@8.5.26: + resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} + engines: {node: ^10 || ^12 || >=14} + process-warning@4.0.1: resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} @@ -373,6 +1636,41 @@ packages: quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + react-dom@19.2.8: + resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==} + peerDependencies: + react: ^19.2.8 + + react-icons@5.7.0: + resolution: {integrity: sha512-LBLy340Rzqy6+/yVhZKT3B/QpP1BZaesGqasf09HPOBzRarcDIFH0WwXlXQfE7q7ipxK4MSiC5DIBWURCny6fw==} + peerDependencies: + react: '*' + + react-refresh@0.18.0: + resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} + engines: {node: '>=0.10.0'} + + react-router-dom@7.18.2: + resolution: {integrity: sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.18.2: + resolution: {integrity: sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react@19.2.8: + resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} + engines: {node: '>=0.10.0'} + real-require@0.2.0: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} @@ -408,6 +1706,23 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + robust-predicates@3.0.3: + resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} + + rollup@4.62.4: + resolution: {integrity: sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + roughjs@4.6.6: + resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} + + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-regex2@5.1.1: resolution: {integrity: sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==} hasBin: true @@ -416,9 +1731,19 @@ packages: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} engines: {node: '>=10'} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + secure-json-parse@4.1.0: resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + semver@7.8.5: resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} @@ -427,17 +1752,56 @@ packages: set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + sonic-boom@4.2.1: resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} + state-local@1.0.7: + resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + steed@1.1.3: + resolution: {integrity: sha512-EUkci0FAUiE4IvGTSKcDJIQ/eRUP2JJb56+fvZ4sdnguLTqIdKjSxUe138poW8mkvKWXW2sFPrgTsxqoISnmoA==} + + stylis@4.4.0: + resolution: {integrity: sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==} + supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + tailwindcss@4.3.3: + resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + tarn@3.1.2: resolution: {integrity: sha512-3RTvqKZcK/17jnJ8rMKFXbyNogywTs1z0gVPPwFsJGX46rkmUHOdIaSQ/aVO1rS7nH+soiXiWk7rvUXxndm8Dg==} engines: {node: '>=8.0.0'} @@ -450,10 +1814,88 @@ packages: resolution: {integrity: sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==} engines: {node: '>=8'} + tinyexec@1.3.0: + resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + toad-cache@3.7.4: resolution: {integrity: sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==} engines: {node: '>=20'} + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + ts-dedent@2.3.0: + resolution: {integrity: sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==} + engines: {node: '>=6.10'} + + update-browserslist-db@1.3.1: + resolution: {integrity: sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uuid@14.0.1: + resolution: {integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==} + hasBin: true + + vite@7.3.6: + resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml@2.9.0: resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} engines: {node: '>= 14.6'} @@ -461,12 +1903,223 @@ packages: snapshots: + '@antfu/install-pkg@1.1.0': + dependencies: + package-manager-detector: 1.8.0 + tinyexec: 1.3.0 + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.8': + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.8 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + + '@babel/parser@7.29.8': + dependencies: + '@babel/types': 7.29.8 + + '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + + '@babel/traverse@7.29.8': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@braintree/sanitize-url@7.1.2': {} + + '@chevrotain/types@11.1.2': {} + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@fastify/accept-negotiator@2.1.0': {} + '@fastify/ajv-compiler@4.0.6': dependencies: ajv: 8.20.0 ajv-formats: 3.0.1(ajv@8.20.0) fast-uri: 4.1.2 + '@fastify/cookie@11.1.2': + dependencies: + cookie: 2.0.1 + fastify-plugin: 6.0.0 + + '@fastify/cors@11.3.0': + dependencies: + fastify-plugin: 6.0.0 + toad-cache: 3.7.4 + '@fastify/error@4.2.0': {} '@fastify/fast-json-stringify-compiler@5.1.0': @@ -475,6 +2128,14 @@ snapshots: '@fastify/forwarded@3.0.2': {} + '@fastify/jwt@9.1.0': + dependencies: + '@fastify/error': 4.2.0 + '@lukeed/ms': 2.0.2 + fast-jwt: 5.0.6 + fastify-plugin: 5.1.0 + steed: 1.1.3 + '@fastify/merge-json-schemas@0.2.1': dependencies: dequal: 2.0.3 @@ -484,8 +2145,388 @@ snapshots: '@fastify/forwarded': 3.0.2 ipaddr.js: 2.5.0 + '@fastify/send@4.1.1': + dependencies: + '@lukeed/ms': 2.0.2 + escape-html: 1.0.3 + fast-decode-uri-component: 1.0.1 + http-errors: 2.0.1 + mime: 3.0.0 + + '@fastify/static@8.3.0': + dependencies: + '@fastify/accept-negotiator': 2.1.0 + '@fastify/send': 4.1.1 + content-disposition: 0.5.4 + fastify-plugin: 5.1.0 + fastq: 1.20.1 + glob: 11.1.0 + + '@iconify/types@2.0.0': {} + + '@iconify/utils@3.1.4': + dependencies: + '@antfu/install-pkg': 1.1.0 + '@iconify/types': 2.0.0 + import-meta-resolve: 4.2.0 + + '@isaacs/cliui@9.0.0': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@lukeed/ms@2.0.2': {} + + '@mermaid-js/parser@1.2.0': + dependencies: + '@chevrotain/types': 11.1.2 + + '@monaco-editor/loader@1.7.0': + dependencies: + state-local: 1.0.7 + + '@monaco-editor/react@4.7.0(monaco-editor@0.56.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@monaco-editor/loader': 1.7.0 + monaco-editor: 0.56.0 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + '@pinojs/redact@0.4.0': {} + '@rolldown/pluginutils@1.0.0-rc.3': {} + + '@rollup/rollup-android-arm-eabi@4.62.4': + optional: true + + '@rollup/rollup-android-arm64@4.62.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.62.4': + optional: true + + '@rollup/rollup-darwin-x64@4.62.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.62.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.62.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.62.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.62.4': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.4': + optional: true + + '@tailwindcss/node@4.3.3': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.24.5 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.3 + + '@tailwindcss/oxide-android-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide@4.3.3': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-x64': 4.3.3 + '@tailwindcss/oxide-freebsd-x64': 4.3.3 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.3 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.3 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-x64-musl': 4.3.3 + '@tailwindcss/oxide-wasm32-wasi': 4.3.3 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 + + '@tailwindcss/vite@4.3.3(vite@7.3.6(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0))': + dependencies: + '@tailwindcss/node': 4.3.3 + '@tailwindcss/oxide': 4.3.3 + tailwindcss: 4.3.3 + vite: 7.3.6(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0) + + '@tanstack/query-core@5.101.4': {} + + '@tanstack/react-query@5.101.4(react@19.2.8)': + dependencies: + '@tanstack/query-core': 5.101.4 + react: 19.2.8 + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.8 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.8 + + '@types/d3-array@3.2.2': {} + + '@types/d3-axis@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-brush@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-chord@3.0.6': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-contour@3.0.6': + dependencies: + '@types/d3-array': 3.2.2 + '@types/geojson': 7946.0.16 + + '@types/d3-delaunay@6.0.4': {} + + '@types/d3-dispatch@3.0.7': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-dsv@3.0.7': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-fetch@3.0.7': + dependencies: + '@types/d3-dsv': 3.0.7 + + '@types/d3-force@3.0.10': {} + + '@types/d3-format@3.0.4': {} + + '@types/d3-geo@3.1.1': + dependencies: + '@types/geojson': 7946.0.16 + + '@types/d3-hierarchy@3.1.7': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-polygon@3.0.2': {} + + '@types/d3-quadtree@3.0.6': {} + + '@types/d3-random@3.0.4': {} + + '@types/d3-scale-chromatic@3.1.0': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-selection@3.0.11': {} + + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time-format@4.0.3': {} + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + + '@types/d3@7.4.3': + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-axis': 3.0.6 + '@types/d3-brush': 3.0.6 + '@types/d3-chord': 3.0.6 + '@types/d3-color': 3.1.3 + '@types/d3-contour': 3.0.6 + '@types/d3-delaunay': 6.0.4 + '@types/d3-dispatch': 3.0.7 + '@types/d3-drag': 3.0.7 + '@types/d3-dsv': 3.0.7 + '@types/d3-ease': 3.0.2 + '@types/d3-fetch': 3.0.7 + '@types/d3-force': 3.0.10 + '@types/d3-format': 3.0.4 + '@types/d3-geo': 3.1.1 + '@types/d3-hierarchy': 3.1.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-path': 3.1.1 + '@types/d3-polygon': 3.0.2 + '@types/d3-quadtree': 3.0.6 + '@types/d3-random': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-scale-chromatic': 3.1.0 + '@types/d3-selection': 3.0.11 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-time-format': 4.0.3 + '@types/d3-timer': 3.0.2 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + + '@types/estree@1.0.9': {} + + '@types/geojson@7946.0.16': {} + + '@types/trusted-types@2.0.7': + optional: true + + '@upsetjs/venn.js@2.0.0': + optionalDependencies: + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + '@vitejs/plugin-react@5.2.0(vite@7.3.6(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0))': + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7) + '@rolldown/pluginutils': 1.0.0-rc.3 + '@types/babel__core': 7.20.5 + react-refresh: 0.18.0 + vite: 7.3.6(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0) + transitivePeerDependencies: + - supports-color + abstract-logging@2.0.1: {} agent-base@6.0.2: @@ -505,6 +2546,13 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + asn1.js@5.4.1: + dependencies: + bn.js: 4.12.5 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + safer-buffer: 2.1.2 + asynckit@0.4.0: {} atomic-sleep@1.0.0: {} @@ -524,15 +2572,37 @@ snapshots: - debug - supports-color + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.11.14: {} + + bcryptjs@3.0.3: {} + better-sqlite3@13.0.3: dependencies: node-addon-api: 8.9.2 + bn.js@4.12.5: {} + + brace-expansion@5.0.9: + dependencies: + balanced-match: 4.0.4 + + browserslist@4.28.8: + dependencies: + baseline-browser-mapping: 2.11.14 + caniuse-lite: 1.0.30001809 + electron-to-chromium: 1.5.406 + node-releases: 2.0.53 + update-browserslist-db: 1.3.1(browserslist@4.28.8) + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 function-bind: 1.1.2 + caniuse-lite@1.0.30001809: {} + colorette@2.0.19: {} combined-stream@1.0.8: @@ -541,10 +2611,224 @@ snapshots: commander@10.0.1: {} + commander@7.2.0: {} + + commander@8.3.0: {} + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + convert-source-map@2.0.0: {} + cookie@1.1.1: {} + cookie@2.0.1: {} + + cose-base@1.0.3: + dependencies: + layout-base: 1.0.2 + + cose-base@2.2.0: + dependencies: + layout-base: 2.0.1 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cytoscape-cose-bilkent@4.1.0(cytoscape@3.34.1): + dependencies: + cose-base: 1.0.3 + cytoscape: 3.34.1 + + cytoscape-fcose@2.2.0(cytoscape@3.34.1): + dependencies: + cose-base: 2.2.0 + cytoscape: 3.34.1 + + cytoscape@3.34.1: {} + + d3-array@2.12.1: + dependencies: + internmap: 1.0.1 + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-axis@3.0.0: {} + + d3-brush@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + + d3-color@3.1.0: {} + + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.1.0 + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + + d3-ease@3.0.1: {} + + d3-fetch@3.0.1: + dependencies: + d3-dsv: 3.0.1 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-format@3.1.2: {} + + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + d3-hierarchy@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@1.0.9: {} + + d3-path@3.1.0: {} + + d3-polygon@3.0.1: {} + + d3-quadtree@3.0.1: {} + + d3-random@3.0.1: {} + + d3-sankey@0.12.3: + dependencies: + d3-array: 2.12.1 + d3-shape: 1.3.7 + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-shape@1.3.7: + dependencies: + d3-path: 1.0.9 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3@7.9.0: + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.2 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + + dagre-d3-es@7.0.14: + dependencies: + d3: 7.9.0 + lodash-es: 4.18.1 + + daisyui@5.7.16: {} + date-fns@4.4.0: {} + dayjs@1.11.21: {} + debug@4.3.4: dependencies: ms: 2.1.2 @@ -553,16 +2837,43 @@ snapshots: dependencies: ms: 2.1.3 + delaunator@5.1.0: + dependencies: + robust-predicates: 3.0.3 + delayed-stream@1.0.0: {} + depd@2.0.0: {} + dequal@2.0.3: {} + detect-libc@2.1.2: {} + + dompurify@3.4.13: + optionalDependencies: + '@types/trusted-types': 2.0.7 + + dompurify@3.4.8: + optionalDependencies: + '@types/trusted-types': 2.0.7 + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 es-errors: 1.3.0 gopd: 1.2.0 + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + electron-to-chromium@1.5.406: {} + + enhanced-resolve@5.24.5: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -578,8 +2889,41 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.4 + es-toolkit@1.50.0: {} + + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + escalade@3.2.0: {} + escape-html@1.0.3: {} + esm@3.2.25: {} fast-decode-uri-component@1.0.1: {} @@ -595,6 +2939,13 @@ snapshots: json-schema-ref-resolver: 3.0.0 rfdc: 1.4.1 + fast-jwt@5.0.6: + dependencies: + '@lukeed/ms': 2.0.2 + asn1.js: 5.4.1 + ecdsa-sig-formatter: 1.0.11 + mnemonist: 0.40.4 + fast-querystring@1.1.2: dependencies: fast-decode-uri-component: 1.0.1 @@ -603,6 +2954,14 @@ snapshots: fast-uri@4.1.2: {} + fastfall@1.5.1: + dependencies: + reusify: 1.1.0 + + fastify-plugin@5.1.0: {} + + fastify-plugin@6.0.0: {} + fastify@5.12.0: dependencies: '@fastify/ajv-compiler': 4.0.6 @@ -621,10 +2980,24 @@ snapshots: semver: 7.8.5 toad-cache: 3.7.4 + fastparallel@2.4.1: + dependencies: + reusify: 1.1.0 + xtend: 4.0.2 + fastq@1.20.1: dependencies: reusify: 1.1.0 + fastseries@1.7.2: + dependencies: + reusify: 1.1.0 + xtend: 4.0.2 + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + find-my-way@9.7.0: dependencies: fast-deep-equal: 3.1.3 @@ -633,6 +3006,11 @@ snapshots: follow-redirects@1.16.0: {} + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + form-data@4.0.6: dependencies: asynckit: 0.4.0 @@ -641,8 +3019,13 @@ snapshots: hasown: 2.0.4 mime-types: 2.1.35 + fsevents@2.3.3: + optional: true + function-bind@1.1.2: {} + gensync@1.0.0-beta.2: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -665,8 +3048,21 @@ snapshots: getopts@2.3.0: {} + glob@11.1.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 4.2.3 + minimatch: 10.2.6 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.2 + gopd@1.2.0: {} + graceful-fs@4.2.11: {} + + hachure-fill@0.5.2: {} + has-symbols@1.1.0: {} has-tostringtag@1.0.2: @@ -677,6 +3073,14 @@ snapshots: dependencies: function-bind: 1.1.2 + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 @@ -684,6 +3088,18 @@ snapshots: transitivePeerDependencies: - supports-color + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + import-meta-resolve@4.2.0: {} + + inherits@2.0.4: {} + + internmap@1.0.1: {} + + internmap@2.0.3: {} + interpret@2.2.0: {} ipaddr.js@2.5.0: {} @@ -692,14 +3108,34 @@ snapshots: dependencies: hasown: 2.0.4 + isexe@2.0.0: {} + + jackspeak@4.2.3: + dependencies: + '@isaacs/cliui': 9.0.0 + + jiti@2.7.0: {} + + js-tokens@4.0.0: {} + + jsesc@3.1.0: {} + json-schema-ref-resolver@3.0.0: dependencies: dequal: 2.0.3 json-schema-traverse@1.0.0: {} + json5@2.2.3: {} + jsonata@2.2.2: {} + katex@0.16.47: + dependencies: + commander: 8.3.0 + + khroma@2.1.0: {} + knex@3.3.0(better-sqlite3@13.0.3): dependencies: colorette: 2.0.19 @@ -721,36 +3157,171 @@ snapshots: transitivePeerDependencies: - supports-color + layout-base@1.0.2: {} + + layout-base@2.0.1: {} + light-my-request@6.6.0: dependencies: cookie: 1.1.1 process-warning: 4.0.1 set-cookie-parser: 2.7.2 + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + lodash-es@4.18.1: {} + lodash@4.18.1: {} + lru-cache@11.5.2: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + marked@14.0.0: {} + + marked@16.4.2: {} + math-intrinsics@1.1.0: {} + mermaid@11.16.1: + dependencies: + '@braintree/sanitize-url': 7.1.2 + '@iconify/utils': 3.1.4 + '@mermaid-js/parser': 1.2.0 + '@types/d3': 7.4.3 + '@upsetjs/venn.js': 2.0.0 + cytoscape: 3.34.1 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.34.1) + cytoscape-fcose: 2.2.0(cytoscape@3.34.1) + d3: 7.9.0 + d3-sankey: 0.12.3 + dagre-d3-es: 7.0.14 + dayjs: 1.11.21 + dompurify: 3.4.13 + es-toolkit: 1.50.0 + katex: 0.16.47 + khroma: 2.1.0 + marked: 16.4.2 + roughjs: 4.6.6 + stylis: 4.4.0 + ts-dedent: 2.3.0 + uuid: 14.0.1 + mime-db@1.52.0: {} mime-types@2.1.35: dependencies: mime-db: 1.52.0 + mime@3.0.0: {} + + minimalistic-assert@1.0.1: {} + + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.9 + + minipass@7.1.3: {} + + mnemonist@0.40.4: + dependencies: + obliterator: 2.0.5 + + monaco-editor@0.56.0: + dependencies: + dompurify: 3.4.8 + marked: 14.0.0 + ms@2.1.2: {} ms@2.1.3: {} + nanoid@3.3.18: {} + node-addon-api@8.9.2: {} node-cron@4.6.0: {} + node-releases@2.0.53: {} + + obliterator@2.0.5: {} + on-exit-leak-free@2.1.2: {} + package-json-from-dist@1.0.1: {} + + package-manager-detector@1.8.0: {} + + path-data-parser@0.1.0: {} + + path-key@3.1.1: {} + path-parse@1.0.7: {} + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.2 + minipass: 7.1.3 + pg-connection-string@2.6.2: {} + picocolors@1.1.1: {} + + picomatch@4.0.5: {} + pino-abstract-transport@3.0.0: dependencies: split2: 4.2.0 @@ -776,6 +3347,19 @@ snapshots: sonic-boom: 4.2.1 thread-stream: 4.2.0 + points-on-curve@0.2.0: {} + + points-on-path@0.2.1: + dependencies: + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + + postcss@8.5.26: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + process-warning@4.0.1: {} process-warning@5.1.0: {} @@ -784,6 +3368,33 @@ snapshots: quick-format-unescaped@4.0.4: {} + react-dom@19.2.8(react@19.2.8): + dependencies: + react: 19.2.8 + scheduler: 0.27.0 + + react-icons@5.7.0(react@19.2.8): + dependencies: + react: 19.2.8 + + react-refresh@0.18.0: {} + + react-router-dom@7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-router: 7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + + react-router@7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + cookie: 1.1.1 + react: 19.2.8 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.2.8(react@19.2.8) + + react@19.2.8: {} + real-require@0.2.0: {} real-require@1.0.0: {} @@ -809,26 +3420,107 @@ snapshots: rfdc@1.4.1: {} + robust-predicates@3.0.3: {} + + rollup@4.62.4: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.62.4 + '@rollup/rollup-android-arm64': 4.62.4 + '@rollup/rollup-darwin-arm64': 4.62.4 + '@rollup/rollup-darwin-x64': 4.62.4 + '@rollup/rollup-freebsd-arm64': 4.62.4 + '@rollup/rollup-freebsd-x64': 4.62.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.4 + '@rollup/rollup-linux-arm-musleabihf': 4.62.4 + '@rollup/rollup-linux-arm64-gnu': 4.62.4 + '@rollup/rollup-linux-arm64-musl': 4.62.4 + '@rollup/rollup-linux-loong64-gnu': 4.62.4 + '@rollup/rollup-linux-loong64-musl': 4.62.4 + '@rollup/rollup-linux-ppc64-gnu': 4.62.4 + '@rollup/rollup-linux-ppc64-musl': 4.62.4 + '@rollup/rollup-linux-riscv64-gnu': 4.62.4 + '@rollup/rollup-linux-riscv64-musl': 4.62.4 + '@rollup/rollup-linux-s390x-gnu': 4.62.4 + '@rollup/rollup-linux-x64-gnu': 4.62.4 + '@rollup/rollup-linux-x64-musl': 4.62.4 + '@rollup/rollup-openbsd-x64': 4.62.4 + '@rollup/rollup-openharmony-arm64': 4.62.4 + '@rollup/rollup-win32-arm64-msvc': 4.62.4 + '@rollup/rollup-win32-ia32-msvc': 4.62.4 + '@rollup/rollup-win32-x64-gnu': 4.62.4 + '@rollup/rollup-win32-x64-msvc': 4.62.4 + fsevents: 2.3.3 + + roughjs@4.6.6: + dependencies: + hachure-fill: 0.5.2 + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + points-on-path: 0.2.1 + + rw@1.3.3: {} + + safe-buffer@5.2.1: {} + safe-regex2@5.1.1: dependencies: ret: 0.5.0 safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} + + scheduler@0.27.0: {} + secure-json-parse@4.1.0: {} + semver@6.3.1: {} + semver@7.8.5: {} set-cookie-parser@2.7.2: {} + setprototypeof@1.2.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + sonic-boom@4.2.1: dependencies: atomic-sleep: 1.0.0 + source-map-js@1.2.1: {} + split2@4.2.0: {} + state-local@1.0.7: {} + + statuses@2.0.2: {} + + steed@1.1.3: + dependencies: + fastfall: 1.5.1 + fastparallel: 2.4.1 + fastq: 1.20.1 + fastseries: 1.7.2 + reusify: 1.1.0 + + stylis@4.4.0: {} + supports-preserve-symlinks-flag@1.0.0: {} + tailwindcss@4.3.3: {} + + tapable@2.3.3: {} + tarn@3.1.2: {} thread-stream@4.2.0: @@ -837,6 +3529,47 @@ snapshots: tildify@2.0.0: {} + tinyexec@1.3.0: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + toad-cache@3.7.4: {} + toidentifier@1.0.1: {} + + ts-dedent@2.3.0: {} + + update-browserslist-db@1.3.1(browserslist@4.28.8): + dependencies: + browserslist: 4.28.8 + escalade: 3.2.0 + picocolors: 1.1.1 + + uuid@14.0.1: {} + + vite@7.3.6(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0): + dependencies: + esbuild: 0.28.2 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + postcss: 8.5.26 + rollup: 4.62.4 + tinyglobby: 0.2.17 + optionalDependencies: + fsevents: 2.3.3 + jiti: 2.7.0 + lightningcss: 1.32.0 + yaml: 2.9.0 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + xtend@4.0.2: {} + + yallist@3.1.1: {} + yaml@2.9.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..924b55f --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - packages/*