From 72e8b2de91313f1c6627d1e23b6d7c5f200f0788 Mon Sep 17 00:00:00 2001 From: Labyricorn Date: Sat, 25 Jul 2026 10:13:22 -0700 Subject: [PATCH] Harden control plane boundaries --- .env.example | 24 +- .github/workflows/ci.yml | 24 + .../0001-git-source-management-boundary.md | 34 + ...0002-remote-publication-not-implemented.md | 30 + README.md | 15 +- bun.lock | 661 ------------------ package-lock.json | 409 +---------- package.json | 4 +- server.ts | 595 +--------------- src/backend/buildEngine.test.ts | 22 +- src/backend/buildEngine.ts | 1 + src/backend/gitOperations.ts | 180 +++++ src/backend/giteaDiscovery.test.ts | 92 +++ src/backend/http/contentRouter.ts | 128 ++++ src/backend/http/controlRouter.ts | 124 ++++ src/backend/http/gitSourceRouter.ts | 230 ++++++ src/backend/http/giteaDiscovery.ts | 198 ++++++ src/backend/http/previewRouter.ts | 58 ++ src/backend/releaseLifecycle.test.ts | 161 +++++ src/backend/repositoryBuildEngine.ts | 38 +- src/backend/repositorySnapshot.test.ts | 43 ++ src/backend/store.ts | 17 + src/backend/testing/concurrentBuildWorker.ts | 21 + src/backend/testing/repositoryFixture.ts | 53 ++ 24 files changed, 1507 insertions(+), 1655 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .pandaos/adrs/0001-git-source-management-boundary.md create mode 100644 .pandaos/adrs/0002-remote-publication-not-implemented.md delete mode 100644 bun.lock create mode 100644 src/backend/gitOperations.ts create mode 100644 src/backend/giteaDiscovery.test.ts create mode 100644 src/backend/http/contentRouter.ts create mode 100644 src/backend/http/controlRouter.ts create mode 100644 src/backend/http/gitSourceRouter.ts create mode 100644 src/backend/http/giteaDiscovery.ts create mode 100644 src/backend/http/previewRouter.ts create mode 100644 src/backend/releaseLifecycle.test.ts create mode 100644 src/backend/repositorySnapshot.test.ts create mode 100644 src/backend/testing/concurrentBuildWorker.ts create mode 100644 src/backend/testing/repositoryFixture.ts diff --git a/.env.example b/.env.example index 7a550fe..01d0e1a 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,17 @@ -# GEMINI_API_KEY: Required for Gemini AI API calls. -# AI Studio automatically injects this at runtime from user secrets. -# Users configure this via the Secrets panel in the AI Studio UI. -GEMINI_API_KEY="MY_GEMINI_API_KEY" +HOST="127.0.0.1" +PORT="3000" +TRUST_PROXY="loopback" -# APP_URL: The URL where this applet is hosted. -# AI Studio automatically injects this at runtime with the Cloud Run service URL. -# Used for self-referential links, OAuth callbacks, and API endpoints. -APP_URL="MY_APP_URL" +# Gitea discovery is disabled unless all three values are configured. +GITEA_BASE_URL="https://git.example.internal" +GITEA_ALLOWED_ORIGINS="https://git.example.internal" +GITEA_TOKEN="replace-with-a-read-only-gitea-token" +GITEA_DISCOVERY_TIMEOUT_MS="5000" +GITEA_DISCOVERY_RATE_LIMIT="5" +GITEA_DISCOVERY_RATE_WINDOW_MS="60000" + +# Hosts accepted by Git connectivity and synchronization endpoints. +GIT_ALLOWED_HOSTS="git.example.internal" + +# Optional path-delimited roots for local development repositories. +GIT_ALLOWED_LOCAL_ROOTS="." diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..46bbfa4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,24 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npm test + - run: npm run lint + - run: npm run build diff --git a/.pandaos/adrs/0001-git-source-management-boundary.md b/.pandaos/adrs/0001-git-source-management-boundary.md new file mode 100644 index 0000000..613ed2d --- /dev/null +++ b/.pandaos/adrs/0001-git-source-management-boundary.md @@ -0,0 +1,34 @@ +# ADR 0001: Keep source inventory subordinate to Git-owned build inputs + +- Status: Accepted +- Date: 2026-07-25 + +## Context + +The control plane can inspect and discover repositories, while the deterministic +builder resolves its site definition and other declared inputs from exact Git +commits. A mutable source-management UI must not become an alternate build +configuration channel. + +## Decision + +Repository discovery and connectivity state remain operational inventory only. +They may not alter a release input, theme selection, project declaration, or +source pin. Build inputs continue to come from closed-schema, Git-owned +configuration resolved into immutable snapshots. + +Gitea discovery is fail-closed: its HTTPS origin must be configured and +allow-listed, its API token comes from the process environment, requests use a +bounded timeout, and callers are rate-limited. Discovered entries are merged +atomically into inventory and begin with projection disabled. + +Source-management UX may add review and configuration workflows later, but any +accepted change must be committed to the Git-owned configuration before it can +affect a release. + +## Consequences + +- Discovery cannot silently expand release inputs. +- Operators must configure Gitea credentials and origin policy explicitly. +- A future writable source UX requires a new ADR describing its Git commit and + review workflow. diff --git a/.pandaos/adrs/0002-remote-publication-not-implemented.md b/.pandaos/adrs/0002-remote-publication-not-implemented.md new file mode 100644 index 0000000..c336467 --- /dev/null +++ b/.pandaos/adrs/0002-remote-publication-not-implemented.md @@ -0,0 +1,30 @@ +# ADR 0002: Remote publication remains unimplemented and fail-closed + +- Status: Accepted +- Date: 2026-07-25 + +## Context + +The control-plane UI models SSH/rsync targets, but there is no production-ready +remote deployment implementation. Returning simulated connectivity or success +would allow operators and reviewers to mistake a UI model for a verified +publication path. + +## Decision + +Remote publication and remote connectivity tests return an explicit +`E_REMOTE_PUBLICATION_NOT_IMPLEMENTED`/HTTP 501 response. The store contains a +compile-time false capability constant and checks it before reading target or +build configuration. No configuration field, credential, target state, or +environment variable can enable shell execution or rsync. + +Local staging and activation continue to operate only on checksum-verified, +immutable release directories. + +## Consequences + +- The control plane cannot deploy a release to a remote host. +- Target records remain descriptive UI state only. +- Implementing SSH/rsync requires a new ADR, an explicit capability boundary, + host-key verification, credential isolation, atomic remote promotion, and + end-to-end tests. diff --git a/README.md b/README.md index 31df6bd..ebdc857 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,8 @@ The build engine now: ## Development Requires Node.js 22 or later. +npm is the canonical package manager. CI uses `npm ci` and `package-lock.json`; +other lockfiles are intentionally unsupported. ```bash npm install @@ -36,6 +38,15 @@ npm start The development server listens on port `3000` by default. In the current production deployment, the Node service listens privately on `127.0.0.1:3001`; Nginx exposes the control plane on port 80 and the active rendered release on port 3000. +Gitea discovery is disabled until `GITEA_BASE_URL`, `GITEA_ALLOWED_ORIGINS`, +and a read-only `GITEA_TOKEN` are configured. Discovery uses authenticated +HTTPS requests with an abort deadline and per-client rate limiting. Git +connectivity endpoints accept only configured hosts or local repositories under +`GIT_ALLOWED_LOCAL_ROOTS`; they invoke Git without a shell. + +The deterministic renderer has no AI SDK dependency and does not accept model +output as a release input. + ## Operations See [Website Engine Operations](docs/OPERATIONS.md) for the verified private-network topology, deployment procedure, service checks, key fingerprints, credential-handling rules, and recovery guidance. The operations document intentionally contains no passwords, tokens, or private-key material. @@ -57,7 +68,7 @@ Validation failures produce a failed build record but never publish a partial re ## Current boundaries - Theme packages are presentation data only; Node, shell, WASM, package-manager scripts, custom Liquid tags, and custom filters are not executed. -- The reference loader resolves the configured site-definition repository and supports separately pinned source snapshots through the typed boundary; source-management UX remains follow-up work. -- Remote rsync deployment is still represented by the existing control-plane simulation and must not be treated as a completed production deploy path. +- The reference loader resolves the configured site-definition repository and supports separately pinned source snapshots through the typed boundary; source-management UX remains follow-up work governed by [ADR 0001](.pandaos/adrs/0001-git-source-management-boundary.md). +- Remote rsync deployment is not implemented. Push and connection-test endpoints fail closed with HTTP 501, and no configuration field can enable execution; see [ADR 0002](.pandaos/adrs/0002-remote-publication-not-implemented.md). See [docs/OPERATIONS.md](docs/OPERATIONS.md) for snapshot, release, promotion, failure, and rollback procedures. diff --git a/bun.lock b/bun.lock deleted file mode 100644 index 59d4ada..0000000 --- a/bun.lock +++ /dev/null @@ -1,661 +0,0 @@ -{ - "lockfileVersion": 1, - "configVersion": 1, - "workspaces": { - "": { - "name": "react-example", - "dependencies": { - "@google/genai": "^2.4.0", - "@tailwindcss/vite": "^4.1.14", - "@vitejs/plugin-react": "^5.0.4", - "dotenv": "^17.2.3", - "express": "^4.21.2", - "lucide-react": "^0.546.0", - "motion": "^12.23.24", - "react": "^19.0.1", - "react-dom": "^19.0.1", - "vite": "^6.2.3", - "yaml": "^2.9.0", - }, - "devDependencies": { - "@types/express": "^4.17.21", - "@types/node": "^22.14.0", - "autoprefixer": "^10.4.21", - "esbuild": "^0.25.0", - "tailwindcss": "^4.1.14", - "tsx": "^4.21.0", - "typescript": "~5.8.2", - "vite": "^6.2.3", - }, - }, - }, - "packages": { - "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - - "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="], - - "@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="], - - "@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], - - "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="], - - "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], - - "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="], - - "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="], - - "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - - "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="], - - "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="], - - "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], - - "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw=="], - - "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q=="], - - "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - - "@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], - - "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - - "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - - "@google/genai": ["@google/genai@2.13.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-GM7C8Kaomvjz05x5JEO6+l3d/pciL9LxAG9dUjJLD7nTPZ9X0Cfsf2Z7eET6UjgWyUmxXCHtYnQoQ77F9+ZIOQ=="], - - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], - - "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], - - "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], - - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - - "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - - "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], - - "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], - - "@protobufjs/codegen": ["@protobufjs/codegen@2.0.5", "", {}, "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g=="], - - "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.1", "", {}, "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg=="], - - "@protobufjs/fetch": ["@protobufjs/fetch@1.1.1", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1" } }, "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw=="], - - "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], - - "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], - - "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], - - "@protobufjs/utf8": ["@protobufjs/utf8@1.1.2", "", {}, "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug=="], - - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="], - - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.2", "", { "os": "android", "cpu": "arm" }, "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg=="], - - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw=="], - - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A=="], - - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA=="], - - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw=="], - - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg=="], - - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg=="], - - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA=="], - - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA=="], - - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ=="], - - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg=="], - - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ=="], - - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A=="], - - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w=="], - - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg=="], - - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q=="], - - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg=="], - - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A=="], - - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg=="], - - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg=="], - - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.2", "", { "os": "none", "cpu": "arm64" }, "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA=="], - - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg=="], - - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q=="], - - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg=="], - - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA=="], - - "@tailwindcss/node": ["@tailwindcss/node@4.3.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="], - - "@tailwindcss/oxide": ["@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" } }, "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA=="], - - "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.3", "", { "os": "android", "cpu": "arm64" }, "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw=="], - - "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw=="], - - "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw=="], - - "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw=="], - - "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3", "", { "os": "linux", "cpu": "arm" }, "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ=="], - - "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w=="], - - "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA=="], - - "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w=="], - - "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img=="], - - "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.3", "", { "dependencies": { "@emnapi/core": "^1.11.1", "@emnapi/runtime": "^1.11.1", "@emnapi/wasi-threads": "^1.2.2", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ=="], - - "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ=="], - - "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw=="], - - "@tailwindcss/vite": ["@tailwindcss/vite@4.3.3", "", { "dependencies": { "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "tailwindcss": "4.3.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw=="], - - "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], - - "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], - - "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], - - "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], - - "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], - - "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], - - "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], - - "@types/express": ["@types/express@4.17.25", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "^1" } }, "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw=="], - - "@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.9", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg=="], - - "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], - - "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], - - "@types/node": ["@types/node@22.20.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q=="], - - "@types/qs": ["@types/qs@6.15.1", "", {}, "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw=="], - - "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], - - "@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="], - - "@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="], - - "@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="], - - "@vitejs/plugin-react": ["@vitejs/plugin-react@5.2.0", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="], - - "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], - - "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - - "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], - - "autoprefixer": ["autoprefixer@10.5.4", "", { "dependencies": { "browserslist": "^4.28.6", "caniuse-lite": "^1.0.30001806", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA=="], - - "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - - "baseline-browser-mapping": ["baseline-browser-mapping@2.11.1", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-HYXq73DDpCtNzOmrFsm9eSwCvWCql0RzqjpDzXN9EadiLJ4DNat0nsZ/Bzmy+Ud12mb4/zKDY0cQ805ZzN+i0A=="], - - "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], - - "body-parser": ["body-parser@1.20.6", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g=="], - - "browserslist": ["browserslist@4.28.7", "", { "dependencies": { "baseline-browser-mapping": "^2.10.44", "caniuse-lite": "^1.0.30001806", "electron-to-chromium": "^1.5.393", "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw=="], - - "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], - - "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], - - "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], - - "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], - - "caniuse-lite": ["caniuse-lite@1.0.30001806", "", {}, "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw=="], - - "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], - - "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], - - "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - - "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - - "cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="], - - "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], - - "debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - - "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], - - "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], - - "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - - "dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], - - "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - - "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], - - "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - - "electron-to-chromium": ["electron-to-chromium@1.5.395", "", {}, "sha512-7zt9Aw+SrmxLWLN0zhaTWZQiCdryLVrYTq5R7iZakLvi2UQPYMMsROYV/2qVCzMeCiSXHwKOU+sZ4zOVVlrtKA=="], - - "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - - "enhanced-resolve": ["enhanced-resolve@5.24.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ=="], - - "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], - - "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - - "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], - - "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], - - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - - "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], - - "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], - - "express": ["express@4.22.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q=="], - - "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], - - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - - "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], - - "finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="], - - "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], - - "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - - "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], - - "framer-motion": ["framer-motion@12.42.2", "", { "dependencies": { "motion-dom": "^12.42.2", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw=="], - - "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], - - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - - "gaxios": ["gaxios@7.2.0", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-CUVb4wcYe+771XevyH6HtGmXFAGGKkIC3kswAP8Z1JCe0j80JMaTPZH930DWFrvo0atjh18Arc0pEyUCWa5bfg=="], - - "gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], - - "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], - - "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], - - "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - - "google-auth-library": ["google-auth-library@10.9.0", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.1.4", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-xtvUqvINPhTaBm7nXqlYPcrMHJPm1lCNdSovxnKKhTm+4JsvQ+KGVYJViLoH9Yxu8w+T0Qv5HubzYT9BLrppJg=="], - - "google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], - - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], - - "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - - "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], - - "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], - - "http-errors": ["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" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], - - "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], - - "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], - - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - - "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], - - "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], - - "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - - "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], - - "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], - - "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], - - "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], - - "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], - - "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "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" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], - - "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], - - "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], - - "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], - - "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], - - "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], - - "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], - - "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], - - "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], - - "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], - - "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], - - "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], - - "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], - - "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - - "lucide-react": ["lucide-react@0.546.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ=="], - - "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], - - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - - "media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], - - "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], - - "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], - - "mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], - - "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - - "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - - "motion": ["motion@12.42.2", "", { "dependencies": { "framer-motion": "^12.42.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-Atvv11yUKIid41cVrRBDVX5m8tF8kNpExRSlbpt6APClhDjtwQssgFHhQzejxw7/7YYbjHSPKBVbHo05BuJT5Q=="], - - "motion-dom": ["motion-dom@12.42.2", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA=="], - - "motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="], - - "ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - - "nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="], - - "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], - - "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], - - "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], - - "node-releases": ["node-releases@2.0.51", "", {}, "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ=="], - - "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - - "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], - - "p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="], - - "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], - - "path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], - - "postcss": ["postcss@8.5.22", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ=="], - - "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], - - "protobufjs": ["protobufjs@7.6.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw=="], - - "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], - - "qs": ["qs@6.15.3", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="], - - "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], - - "raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], - - "react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="], - - "react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="], - - "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], - - "retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], - - "rollup": ["rollup@4.62.2", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.62.2", "@rollup/rollup-android-arm64": "4.62.2", "@rollup/rollup-darwin-arm64": "4.62.2", "@rollup/rollup-darwin-x64": "4.62.2", "@rollup/rollup-freebsd-arm64": "4.62.2", "@rollup/rollup-freebsd-x64": "4.62.2", "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", "@rollup/rollup-linux-arm-musleabihf": "4.62.2", "@rollup/rollup-linux-arm64-gnu": "4.62.2", "@rollup/rollup-linux-arm64-musl": "4.62.2", "@rollup/rollup-linux-loong64-gnu": "4.62.2", "@rollup/rollup-linux-loong64-musl": "4.62.2", "@rollup/rollup-linux-ppc64-gnu": "4.62.2", "@rollup/rollup-linux-ppc64-musl": "4.62.2", "@rollup/rollup-linux-riscv64-gnu": "4.62.2", "@rollup/rollup-linux-riscv64-musl": "4.62.2", "@rollup/rollup-linux-s390x-gnu": "4.62.2", "@rollup/rollup-linux-x64-gnu": "4.62.2", "@rollup/rollup-linux-x64-musl": "4.62.2", "@rollup/rollup-openbsd-x64": "4.62.2", "@rollup/rollup-openharmony-arm64": "4.62.2", "@rollup/rollup-win32-arm64-msvc": "4.62.2", "@rollup/rollup-win32-ia32-msvc": "4.62.2", "@rollup/rollup-win32-x64-gnu": "4.62.2", "@rollup/rollup-win32-x64-msvc": "4.62.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA=="], - - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - - "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], - - "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], - - "serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], - - "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - - "side-channel": ["side-channel@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="], - - "side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="], - - "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], - - "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - - "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], - - "tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="], - - "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], - - "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], - - "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "tsx": ["tsx@4.23.1", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ=="], - - "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], - - "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], - - "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], - - "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], - - "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], - - "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - - "vite": ["vite@6.4.3", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A=="], - - "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], - - "ws": ["ws@8.21.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw=="], - - "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - - "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], - - "@babel/core/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "@babel/traverse/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="], - - "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], - - "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], - - "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="], - - "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], - - "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "https-proxy-agent/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "tsx/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], - - "@babel/core/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "@babel/traverse/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "https-proxy-agent/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], - - "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], - - "tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], - - "tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], - - "tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], - - "tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], - - "tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], - - "tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], - - "tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], - - "tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], - - "tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], - - "tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], - - "tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], - - "tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], - - "tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], - - "tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], - - "tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], - - "tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], - - "tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], - - "tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], - - "tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], - - "tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], - - "tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], - - "tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], - - "tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], - - "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], - } -} diff --git a/package-lock.json b/package-lock.json index 04d5bfb..fdb9d46 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,6 @@ "name": "react-example", "version": "0.0.0", "dependencies": { - "@google/genai": "^2.4.0", "@tailwindcss/vite": "^4.1.14", "@vitejs/plugin-react": "^5.0.4", "ajv": "8.20.0", @@ -712,30 +711,6 @@ "node": ">=18" } }, - "node_modules/@google/genai": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/@google/genai/-/genai-2.13.0.tgz", - "integrity": "sha512-GM7C8Kaomvjz05x5JEO6+l3d/pciL9LxAG9dUjJLD7nTPZ9X0Cfsf2Z7eET6UjgWyUmxXCHtYnQoQ77F9+ZIOQ==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "google-auth-library": "^10.3.0", - "p-retry": "^4.6.2", - "protobufjs": "^7.5.4", - "ws": "^8.18.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.25.2" - }, - "peerDependenciesMeta": { - "@modelcontextprotocol/sdk": { - "optional": true - } - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -781,63 +756,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", - "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", - "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", - "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", - "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", - "license": "BSD-3-Clause" - }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-rc.3", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", @@ -1649,6 +1567,7 @@ "version": "22.20.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -1668,12 +1587,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", - "license": "MIT" - }, "node_modules/@types/send": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", @@ -1740,15 +1653,6 @@ "node": ">= 0.6" } }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, "node_modules/ajv": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", @@ -1808,26 +1712,6 @@ "postcss": "^8.1.0" } }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/baseline-browser-mapping": { "version": "2.11.1", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.1.tgz", @@ -1840,15 +1724,6 @@ "node": ">=6.0.0" } }, - "node_modules/bignumber.js": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", - "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/body-parser": { "version": "1.20.6", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", @@ -1921,12 +1796,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "license": "BSD-3-Clause" - }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -2036,15 +1905,6 @@ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", "license": "MIT" }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2116,15 +1976,6 @@ "node": ">= 0.4" } }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -2315,12 +2166,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2360,29 +2205,6 @@ } } }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, "node_modules/finalhandler": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", @@ -2416,18 +2238,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "license": "MIT", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -2510,34 +2320,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gaxios": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.3.0.tgz", - "integrity": "sha512-RB5vLV+vvQeoFPCX4QMK6/hjVkbIamPp1QSUD0CiZcnj12qbpiL+pLbYtgD+oZkWl0tl9z+o2Utp+MpM3QRhBA==", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "node-fetch": "^3.3.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/gcp-metadata": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", - "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^7.0.0", - "google-logging-utils": "^1.0.0", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2584,32 +2366,6 @@ "node": ">= 0.4" } }, - "node_modules/google-auth-library": { - "version": "10.9.0", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.9.0.tgz", - "integrity": "sha512-xtvUqvINPhTaBm7nXqlYPcrMHJPm1lCNdSovxnKKhTm+4JsvQ+KGVYJViLoH9Yxu8w+T0Qv5HubzYT9BLrppJg==", - "license": "Apache-2.0", - "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^7.1.4", - "gcp-metadata": "8.1.2", - "google-logging-utils": "1.1.3", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/google-logging-utils": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", - "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -2672,19 +2428,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -2739,15 +2482,6 @@ "node": ">=6" } }, - "node_modules/json-bigint": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", - "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", - "license": "MIT", - "dependencies": { - "bignumber.js": "^9.0.0" - } - }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -2766,27 +2500,6 @@ "node": ">=6" } }, - "node_modules/jwa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", - "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", - "license": "MIT", - "dependencies": { - "jwa": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, "node_modules/lightningcss": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", @@ -3068,12 +2781,6 @@ "url": "https://opencollective.com/liquidjs" } }, - "node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "license": "Apache-2.0" - }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3244,44 +2951,6 @@ "node": ">= 0.6" } }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, "node_modules/node-releases": { "version": "2.0.51", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", @@ -3315,19 +2984,6 @@ "node": ">= 0.8" } }, - "node_modules/p-retry": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", - "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", - "license": "MIT", - "dependencies": { - "@types/retry": "0.12.0", - "retry": "^0.13.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -3396,29 +3052,6 @@ "dev": true, "license": "MIT" }, - "node_modules/protobufjs": { - "version": "7.6.5", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", - "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.5", - "@protobufjs/eventemitter": "^1.1.1", - "@protobufjs/fetch": "^1.1.1", - "@protobufjs/float": "^1.0.2", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.1", - "@types/node": ">=13.7.0", - "long": "^5.3.2" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -3511,15 +3144,6 @@ "node": ">=0.10.0" } }, - "node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/rollup": { "version": "4.62.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", @@ -4313,6 +3937,7 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "devOptional": true, "license": "MIT" }, "node_modules/unpipe": { @@ -4446,36 +4071,6 @@ } } }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/ws": { - "version": "8.21.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", - "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index 7f74552..351615b 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "private": true, "version": "0.0.0", "type": "module", + "packageManager": "npm@11.12.1", "scripts": { "dev": "tsx server.ts", "build": "vite build && esbuild server.ts --bundle --platform=node --format=cjs --packages=external --sourcemap --outfile=dist/server.cjs", @@ -10,10 +11,9 @@ "preview": "vite preview", "clean": "rm -rf dist server.js", "lint": "tsc --noEmit", - "test": "node --import tsx --test src/backend/buildEngine.test.ts" + "test": "node --import tsx --test src/backend/buildEngine.test.ts src/backend/giteaDiscovery.test.ts src/backend/releaseLifecycle.test.ts src/backend/repositorySnapshot.test.ts" }, "dependencies": { - "@google/genai": "^2.4.0", "@tailwindcss/vite": "^4.1.14", "@vitejs/plugin-react": "^5.0.4", "ajv": "8.20.0", diff --git a/server.ts b/server.ts index f059744..4dbfe7d 100644 --- a/server.ts +++ b/server.ts @@ -1,575 +1,33 @@ +import "dotenv/config"; import express from "express"; -import path from "path"; -import fs from "fs"; +import path from "node:path"; import { createServer as createViteServer } from "vite"; -import { store } from "./src/backend/store"; -import { CredentialRef } from "./src/types"; -import YAML from "yaml"; -import { exec } from "child_process"; -import { promisify } from "util"; +import { contentRouter } from "./src/backend/http/contentRouter"; +import { controlRouter } from "./src/backend/http/controlRouter"; +import { gitSourceRouter } from "./src/backend/http/gitSourceRouter"; +import { previewRouter } from "./src/backend/http/previewRouter"; -const execAsync = promisify(exec); - -async function startServer() { - const app = express(); - const port = Number.parseInt(process.env.PORT ?? "3000", 10); - const host = process.env.HOST ?? "0.0.0.0"; - - if (!Number.isInteger(port) || port < 1 || port > 65535) { - throw new Error(`Invalid PORT value: ${process.env.PORT}`); +const parsePort = (value: string): number => { + const port = Number.parseInt(value, 10); + if (!Number.isInteger(port) || port < 1 || port > 65_535) { + throw new Error(`Invalid PORT value: ${value}`); } + return port; +}; - app.use(express.json()); +async function startServer(): Promise { + const app = express(); + const port = parsePort(process.env.PORT ?? "3000"); + const host = process.env.HOST ?? "127.0.0.1"; - // --- API ROUTES --- + app.disable("x-powered-by"); + app.set("trust proxy", process.env.TRUST_PROXY ?? "loopback"); + app.use(express.json({ limit: "1mb" })); + app.use("/api", controlRouter); + app.use("/api", gitSourceRouter); + app.use("/api", contentRouter); + app.use("/api", previewRouter); - // Health - app.get("/api/health", (req, res) => { - res.json({ status: "ok", service: "labyricorn-site-builder" }); - }); - - // Site Config - app.get("/api/site-config", (req, res) => { - res.json(store.siteConfig); - }); - - app.put("/api/site-config", (_req, res) => { - res.status(405).json({ - code: "E_CONFIG_READ_ONLY", - message: "Site configuration is Git-owned and read-only in v1.", - }); - }); - - // --- GITEA DISCOVERY --- - app.post("/api/gitea/discover", async (req, res) => { - store.addAudit( - "Gitea Discovery", - "config", - "Initiating repository discovery from git.labyricorn.com", - ); - - try { - // Real backend operation: Attempt to connect to the Gitea instance - const response = await fetch( - "https://git.labyricorn.com/api/v1/repos/search", - { - timeout: 5000, - } as any, - ).catch((e) => { - throw new Error( - `Connection to git.labyricorn.com failed: ${e.message}`, - ); - }); - - if (!response.ok) { - throw new Error(`Gitea returned HTTP ${response.status}`); - } - - const data = await response.json(); - const repos = data.data || []; - - let added = 0; - for (const repo of repos) { - if ( - !store.gitSources.find( - (s) => - s.repository === repo.clone_url || s.repository === repo.ssh_url, - ) - ) { - store.gitSources.push({ - id: repo.name, - repository: repo.clone_url, - ref: repo.default_branch || "main", - branch: repo.default_branch || "main", - type: "hybrid", - health: "Warning", - reachability: "Unreachable", - configurationState: "Not Configured", - visibility: repo.private ? "Private in Gitea" : "Public in Gitea", - projectionEnabled: false, - produces: [], - status: "idle", - explainWhy: - "Discovered from Gitea. Awaiting initial synchronization.", - required: false, - credential: "none", - }); - added++; - } - } - - res.json({ - success: true, - discoveredCount: repos.length, - addedCount: added, - message: `Successfully discovered ${repos.length} repositories from Gitea.`, - }); - } catch (e: any) { - store.addAudit("Gitea Discovery Failed", "config", e.message, "failure"); - res.status(502).json({ error: e.message, success: false }); - } - }); - - // Git Sources - app.get("/api/git-sources", (req, res) => { - res.json(store.gitSources); - }); - - app.post("/api/git-sources", async (req, res) => { - const newSource = req.body; - - // Phase 2 Defaults - newSource.type = newSource.type || "hybrid"; - newSource.health = "Warning"; - newSource.reachability = "Unreachable"; - newSource.configurationState = "Not Configured"; - newSource.visibility = "Private in Gitea"; - newSource.projectionEnabled = false; - newSource.produces = []; - newSource.explainWhy = "Awaiting initial synchronization."; - newSource.status = "syncing"; - - try { - if ( - newSource.repository.startsWith("http") || - newSource.repository.startsWith("git@") || - newSource.repository.startsWith("/") - ) { - const { stdout } = await execAsync( - `GIT_TERMINAL_PROMPT=0 git ls-remote ${newSource.repository} ${newSource.ref || "HEAD"}`, - { timeout: 10000 }, - ); - const match = stdout.trim().split("\\t")[0]; - if (match) { - newSource.status = "connected"; - newSource.reachability = "Healthy"; - newSource.lastResolvedCommit = match; - newSource.lastSyncedAt = new Date().toISOString(); - newSource.explainWhy = - "Connected to repository, but missing .labyricorn/site.yml metadata."; - } else { - newSource.status = "error"; - newSource.explainWhy = "Ref not found in repository."; - } - } else { - newSource.status = "offline"; - newSource.explainWhy = - "Backend Not Yet Implemented for non-http/git repositories. (Development Mode)"; - } - } catch (e: any) { - newSource.status = "error"; - newSource.explainWhy = `Git ls-remote failed: ${e.message.split("\\n")[0]}`; - } - - store.gitSources.push(newSource); - store.addAudit( - "Add Git Source", - "source", - `Added Git source ${newSource.id} (${newSource.repository})`, - ); - res.json({ success: true, source: newSource }); - }); - - app.put("/api/git-sources/:id", (req, res) => { - const { id } = req.params; - const idx = store.gitSources.findIndex((s) => s.id === id); - if (idx !== -1) { - store.gitSources[idx] = { ...store.gitSources[idx], ...req.body }; - store.addAudit("Update Git Source", "source", `Updated Git source ${id}`); - res.json({ success: true, source: store.gitSources[idx] }); - } else { - res.status(404).json({ error: "Source not found" }); - } - }); - - app.delete("/api/git-sources/:id", (req, res) => { - const { id } = req.params; - store.gitSources = store.gitSources.filter((s) => s.id !== id); - store.addAudit("Delete Git Source", "source", `Removed Git source ${id}`); - res.json({ success: true }); - }); - - app.post("/api/git-sources/:id/test", async (req, res) => { - const { id } = req.params; - const src = store.gitSources.find((s) => s.id === id); - if (!src) return res.status(404).json({ error: "Source not found" }); - - src.status = "syncing"; - src.explainWhy = "Synchronization running..."; - - try { - if ( - src.repository.startsWith("http") || - src.repository.startsWith("git@") || - src.repository.startsWith("/") - ) { - const { stdout } = await execAsync( - `GIT_TERMINAL_PROMPT=0 git ls-remote ${src.repository} ${src.ref || "HEAD"}`, - { timeout: 10000 }, - ); - const match = stdout.trim().split("\\t")[0]; - if (match) { - src.status = "connected"; - src.reachability = "Healthy"; - src.lastResolvedCommit = match; - src.lastSyncedAt = new Date().toISOString(); - - // Actually attempt to clone the repo into /tmp/labyricorn-cache to read real artifacts - const targetDir = `/tmp/labyricorn-cache/${id}`; - await execAsync( - `rm -rf ${targetDir} && git clone --depth 1 -b ${src.ref || "main"} ${src.repository} ${targetDir}`, - ); - - // Check for site.yml - if (fs.existsSync(`${targetDir}/.labyricorn/site.yml`)) { - src.configurationState = "Valid"; - src.projectionEnabled = true; - src.explainWhy = - "Repository synchronized and valid site.yml detected."; - src.health = "Healthy"; - } else { - src.configurationState = "Not Configured"; - src.projectionEnabled = false; - src.explainWhy = - "Repository synchronized, but missing .labyricorn/site.yml metadata."; - src.health = "Warning"; - } - - return res.json({ - success: true, - message: `Successfully connected and synchronized ${src.repository}!`, - resolvedCommit: src.lastResolvedCommit, - syncedAt: src.lastSyncedAt, - }); - } else { - src.status = "error"; - src.explainWhy = "Ref not found in repository."; - return res.status(400).json({ error: src.explainWhy }); - } - } else { - src.status = "offline"; - src.explainWhy = - "Backend Not Yet Implemented for non-http/git repositories. (Development Mode)"; - return res.status(501).json({ error: src.explainWhy }); - } - } catch (e: any) { - src.status = "error"; - src.health = "Invalid"; - src.explainWhy = `Git sync failed: ${e.message.split("\\n")[0]}`; - return res.status(400).json({ error: src.explainWhy }); - } - }); - - app.get("/api/git-sources/:id/tree", async (req, res) => { - const { id } = req.params; - const src = store.gitSources.find((s) => s.id === id); - if (!src) return res.status(404).json({ error: "Source not found" }); - - const targetDir = `/tmp/labyricorn-cache/${id}`; - if (!fs.existsSync(targetDir)) { - return res.json({ sourceId: id, files: [] }); - } - - try { - const { stdout } = await execAsync( - `find . -type f -not -path "*/.git/*" | head -n 50`, - { cwd: targetDir }, - ); - const files = stdout - .split("\\n") - .filter(Boolean) - .map((f) => { - const path = f.replace("./", ""); - return { - path, - type: "file", - size: "Unknown", // Not optimizing for accurate size right now - }; - }); - res.json({ sourceId: id, files }); - } catch (e) { - res.json({ sourceId: id, files: [] }); - } - }); - - // Style Packages & Instances - app.get("/api/style-packages", (req, res) => { - res.json(store.stylePackages); - }); - - app.get("/api/style-instances", (req, res) => { - res.json(store.styleInstances); - }); - - app.post("/api/style-instances", (req, res) => { - const inst = req.body; - store.styleInstances.push(inst); - store.addAudit( - "Add Style Instance", - "config", - `Created style instance '${inst.id}' using '${inst.uses}'`, - ); - res.json({ success: true, instance: inst }); - }); - - app.put("/api/style-instances/:id", (req, res) => { - const { id } = req.params; - const idx = store.styleInstances.findIndex((i) => i.id === id); - if (idx !== -1) { - store.styleInstances[idx] = { ...store.styleInstances[idx], ...req.body }; - store.addAudit( - "Update Style Instance", - "config", - `Updated style instance '${id}'`, - ); - res.json({ success: true, instance: store.styleInstances[idx] }); - } else { - res.status(404).json({ error: "Instance not found" }); - } - }); - - // Content - app.get("/api/content", (req, res) => { - res.json(store.contentItems); - }); - - app.post("/api/content/discover", async (req, res) => { - store.addAudit( - "Discover Content", - "config", - "Executed content discovery scan across all Git sources", - ); - - // Clear old items - store.contentItems = []; - - for (const source of store.gitSources) { - if (source.health !== "Healthy" || !source.projectionEnabled) continue; - - const targetDir = `/tmp/labyricorn-cache/${source.id}`; - if (!fs.existsSync(targetDir)) continue; - - try { - // Find markdown files - const { stdout } = await execAsync( - `find . -type f -name "*.md" -not -path "*/.git/*"`, - { cwd: targetDir }, - ); - const mdFiles = stdout.split("\\n").filter(Boolean); - - mdFiles.forEach((file, idx) => { - const relativePath = file.replace("./", "").trim(); - const title = - relativePath.split("/").pop()?.replace(".md", "").trim() || - "Untitled"; - - // Determine artifact type based on repository declaration or fallback to first available content model - const declaredModels = source.produces || []; - const defaultModel = - store.siteConfig.contentModels.length > 0 - ? store.siteConfig.contentModels[0].id - : "page"; - const artifactType = - declaredModels.length > 0 ? declaredModels[0] : defaultModel; - const slug = title.toLowerCase(); - - const contentModelDef = store.siteConfig.contentModels.find( - (m) => m.id === artifactType, - ); - let itemRoute = "/{slug}/"; - if (contentModelDef && contentModelDef.routing && contentModelDef.routing.detail) { - itemRoute = contentModelDef.routing.detail; - } else { - const navEntry = store.siteConfig.navigation.find( - (nav) => nav.contentModel === artifactType, - ); - itemRoute = navEntry ? `${navEntry.route}/{slug}/` : `/${artifactType}s/{slug}/`; - } - - store.contentItems.push({ - id: `art-${source.id}-${idx}`, - title: title - .replace(/-/g, " ") - .replace(/\\b\\w/g, (l) => l.toUpperCase()), - slug, - published: new Date().toISOString(), - status: "published", - artifactType: artifactType, - summary: `Discovered artifact from ${relativePath}`, - tags: [source.id], - aliases: [], - sourceRepo: source.id, - path: relativePath, - contentMarkdown: `Content loaded from ${relativePath}`, - mediaReferences: [], - youtubeDirectives: [], - wikipediaLinks: [], - validationStatus: "valid", - validationMessages: [], - route: itemRoute.replace("{slug}", slug).replace("//", "/"), - styleInstanceId: "default", - }); - }); - } catch (e) { - // No files found or error - } - } - - res.json({ - success: true, - discoveredCount: store.contentItems.length, - mediaCount: store.mediaAssets.length, - items: store.contentItems, - }); - }); - - // Media - app.get("/api/media", (req, res) => { - res.json(store.mediaAssets); - }); - - // Theme - app.get("/api/theme", (req, res) => { - res.json(store.themeConfig); - }); - - app.post("/api/theme/validate", (_req, res) => { - const theme = store.validateTheme(); - res.status(theme.status === "valid" ? 200 : 422).json({ - success: theme.status === "valid", - theme, - commits: theme.commit ? { [theme.sourceId ?? "theme"]: theme.commit } : {}, - }); - }); - - // Builds - app.get("/api/builds", (req, res) => { - res.json(store.builds); - }); - - app.post("/api/builds/trigger", (req, res) => { - const newBuild = store.runBuild(); - res.json(newBuild); - }); - - // Nginx Local Hosting - app.get("/api/nginx", (req, res) => { - res.json(store.nginxStatus); - }); - - app.post("/api/nginx/test", (req, res) => { - store.nginxStatus.configValid = true; - store.addAudit( - "Test Nginx Config", - "nginx", - "Executed nginx -t: configuration syntax is ok, test is successful", - ); - res.json({ - success: true, - message: - "nginx: the configuration file /etc/nginx/nginx.conf syntax is ok\nnginx: configuration file /etc/nginx/nginx.conf test is successful", - }); - }); - - app.post("/api/nginx/activate", (req, res) => { - const { buildId } = req.body; - const result = store.activateReleaseLocally(buildId); - res.json(result); - }); - - // Push Targets - app.get("/api/push-targets", (req, res) => { - res.json(store.pushTargets); - }); - - app.post("/api/push-targets/:id/push", (req, res) => { - const { id } = req.params; - const { buildId } = req.body; - const result = store.pushToTarget(id, buildId); - res.json(result); - }); - - app.post("/api/push-targets/:id/test", (req, res) => { - const { id } = req.params; - const target = store.pushTargets.find((t) => t.id === id); - if (!target) return res.status(404).json({ error: "Target not found" }); - - res.json({ - success: true, - message: `SSH test to ${target.host}:22 succeeded! Verification key fingerprint: SHA256:p92kALm10x9. Rsync binary detected at /usr/bin/rsync.`, - }); - }); - - // Credentials - app.get("/api/credentials", (req, res) => { - res.json(store.credentials); - }); - - app.post("/api/credentials", (req, res) => { - const { name, type, secretValue } = req.body; - const id = `secret:${name.toLowerCase().replace(/[^a-z0-9]/g, "-")}`; - const masked = - type === "ssh_key" - ? `${secretValue.substring(0, 20)}... (RSA 4096)` - : `${secretValue.substring(0, 4)}********************`; - - const cred: CredentialRef = { - id, - name, - type, - maskedValue: masked, - createdAt: new Date().toISOString(), - lastUsedAt: new Date().toISOString(), - usageCount: 0, - }; - store.credentials.push(cred); - store.addAudit( - "Add Credential", - "credential", - `Stored credential reference ${id}`, - ); - res.json({ success: true, credential: cred }); - }); - - // Audit Logs - app.get("/api/audit-logs", (req, res) => { - res.json(store.auditLogs); - }); - - // --- RELEASE-ONLY SITE PREVIEW ENDPOINT --- - app.get("/api/live-site/html", (req, res) => { - const rootDir = store.getPreviewDirectory(); - if (!rootDir) { - return res.status(503).json({ - code: "E_RELEASE_UNAVAILABLE", - message: "No verified staging or live release is selected.", - }); - } - - let requested: string; - try { - requested = decodeURIComponent(String(req.query.route ?? "/").split(/[?#]/, 1)[0]); - } catch { - return res.status(400).json({ code: "E_ROUTE_INVALID" }); - } - if (!requested.startsWith("/") || requested.includes("\\") || requested.split("/").includes("..")) { - return res.status(400).json({ code: "E_ROUTE_INVALID" }); - } - - const relative = requested.replace(/^\/+/, ""); - const outputFile = relative && path.posix.extname(relative) ? relative : path.posix.join(relative, "index.html"); - const rootPath = `${path.resolve(rootDir)}${path.sep}`; - const target = path.resolve(rootDir, ...outputFile.split("/")); - if (!target.startsWith(rootPath)) return res.status(400).json({ code: "E_ROUTE_INVALID" }); - - res.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'self'"); - if (fs.existsSync(target) && fs.statSync(target).isFile()) return res.type(path.extname(target)).send(fs.readFileSync(target)); - const notFound = path.join(rootDir, "404.html"); - if (fs.existsSync(notFound)) return res.status(404).type("html").send(fs.readFileSync(notFound)); - return res.status(404).json({ code: "E_ROUTE_NOT_FOUND" }); - }); - - // --- VITE / PRODUCTION SERVING --- if (process.env.NODE_ENV !== "production") { const vite = await createViteServer({ server: { middlewareMode: true }, @@ -579,7 +37,7 @@ async function startServer() { } else { const distPath = path.join(process.cwd(), "dist"); app.use(express.static(distPath)); - app.get("*", (req, res) => { + app.get("*", (_req, res) => { res.sendFile(path.join(distPath, "index.html")); }); } @@ -589,4 +47,7 @@ async function startServer() { }); } -startServer(); +startServer().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/src/backend/buildEngine.test.ts b/src/backend/buildEngine.test.ts index 14be5bd..f05deb4 100644 --- a/src/backend/buildEngine.test.ts +++ b/src/backend/buildEngine.test.ts @@ -79,10 +79,14 @@ test("equivalent resolved inputs are byte-identical despite different run IDs", assert.equal(first.input.artifactBuildId, second.input.artifactBuildId); const engine = new BuildEngine(output); const one = engine.build(first.input); - assert.equal(fs.statSync(one.outputDirectory).mode & 0o777, 0o750); + if (process.platform !== "win32") { + assert.equal(fs.statSync(one.outputDirectory).mode & 0o777, 0o750); + } fs.chmodSync(one.outputDirectory, 0o700); const two = engine.build(second.input); - assert.equal(fs.statSync(two.outputDirectory).mode & 0o777, 0o750); + if (process.platform !== "win32") { + assert.equal(fs.statSync(two.outputDirectory).mode & 0o777, 0o750); + } assert.equal(one.artifactChecksum, two.artifactChecksum); assert.equal(one.outputDirectory, two.outputDirectory); assert.deepEqual(one.generatedFiles, two.generatedFiles); @@ -210,9 +214,17 @@ test("markdown output is escaped and heading IDs are deterministic", () => { }); test("production sources contain no embedded renderer or synthesized preview fallback", () => { - assert.equal(fs.readFileSync(path.resolve("src/backend/buildEngine.ts"), "utf8").trim(), 'export * from "./repositoryBuildEngine";'); + const boundary = fs.readFileSync(path.resolve("src/backend/buildEngine.ts"), "utf8"); + assert.match(boundary, /AI or other open-schema output is never a release input/); + assert.match(boundary, /export \* from ".\/repositoryBuildEngine"/); const server = fs.readFileSync(path.resolve("server.ts"), "utf8"); assert.doesNotMatch(server, /cdn\.tailwindcss\.com|Simple HTML renderer|SITE HEADER/); - assert.match(server, /E_RELEASE_UNAVAILABLE/); - assert.match(server, /E_CONFIG_READ_ONLY/); + assert.doesNotMatch(server, /child_process|execAsync/); + const preview = fs.readFileSync(path.resolve("src/backend/http/previewRouter.ts"), "utf8"); + const control = fs.readFileSync(path.resolve("src/backend/http/controlRouter.ts"), "utf8"); + const storeSource = fs.readFileSync(path.resolve("src/backend/store.ts"), "utf8"); + assert.match(preview, /E_RELEASE_UNAVAILABLE/); + assert.match(control, /E_CONFIG_READ_ONLY/); + assert.match(storeSource, /REMOTE_PUBLICATION_IMPLEMENTED = false as const/); + assert.doesNotMatch(storeSource, /REMOTE_PUBLICATION_IMPLEMENTED\s*=\s*process\.env/); }); diff --git a/src/backend/buildEngine.ts b/src/backend/buildEngine.ts index 143201b..8973f30 100644 --- a/src/backend/buildEngine.ts +++ b/src/backend/buildEngine.ts @@ -1 +1,2 @@ +// Security boundary: AI or other open-schema output is never a release input. export * from "./repositoryBuildEngine"; diff --git a/src/backend/gitOperations.ts b/src/backend/gitOperations.ts new file mode 100644 index 0000000..25828ed --- /dev/null +++ b/src/backend/gitOperations.ts @@ -0,0 +1,180 @@ +import { execFile } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { promisify } from "node:util"; + +const execFileAsync = promisify(execFile); +const cacheRoot = path.join(os.tmpdir(), "labyricorn-cache"); +const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/; + +const allowedHosts = (): Set => { + const configured = (process.env.GIT_ALLOWED_HOSTS ?? "") + .split(",") + .map((host) => host.trim().toLowerCase()) + .filter(Boolean); + if (process.env.GITEA_BASE_URL) { + configured.push(new URL(process.env.GITEA_BASE_URL).hostname.toLowerCase()); + } + return new Set(configured); +}; + +const isWithin = (root: string, candidate: string): boolean => { + const relative = path.relative(root, candidate); + return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)); +}; + +const validateLocalRepository = (repository: string): string => { + const resolved = path.resolve(repository); + const configuredRoots = (process.env.GIT_ALLOWED_LOCAL_ROOTS ?? process.cwd()) + .split(path.delimiter) + .map((entry) => path.resolve(entry)); + if (!configuredRoots.some((root) => isWithin(root, resolved))) { + throw new Error("Local repository is outside GIT_ALLOWED_LOCAL_ROOTS."); + } + return resolved; +}; + +export const validateRepository = (repository: string): string => { + const value = repository.trim(); + if (path.isAbsolute(value)) return validateLocalRepository(value); + + if (!value.includes("://")) { + const scpLike = /^(?:[^@\s]+@)?([^:/\s]+):(.+)$/.exec(value); + if (scpLike) { + if (!allowedHosts().has(scpLike[1].toLowerCase())) { + throw new Error(`Git host '${scpLike[1]}' is not allow-listed.`); + } + return value; + } + } + + const url = new URL(value); + if (!["https:", "ssh:"].includes(url.protocol) || url.username === "root") { + throw new Error("Repository URL must use HTTPS or SSH."); + } + if ( + (url.protocol === "https:" && (url.username || url.password)) || + (url.protocol === "ssh:" && url.password) + ) throw new Error("Repository URL contains disallowed credentials."); + if (!allowedHosts().has(url.hostname.toLowerCase())) { + throw new Error(`Git host '${url.hostname}' is not allow-listed.`); + } + return value; +}; + +const validateRef = (ref: string): string => { + const value = ref.trim(); + if ( + value.length === 0 || + value.length > 255 || + /[\s~^:?*[\]\\]/.test(value) || + value.includes("..") || + value.includes("@{") || + value.startsWith("-") + ) { + throw new Error("Git ref is invalid."); + } + return value; +}; + +const validateId = (id: string): string => { + if (!SAFE_ID.test(id)) throw new Error("Git source ID is invalid."); + return id; +}; + +const gitEnvironment = { + ...process.env, + GIT_TERMINAL_PROMPT: "0", +}; + +export const resolveRemote = async ( + repository: string, + ref: string, +): Promise => { + const safeRepository = validateRepository(repository); + const safeRef = validateRef(ref || "HEAD"); + const { stdout } = await execFileAsync( + "git", + ["-c", "protocol.ext.allow=never", "ls-remote", "--", safeRepository, safeRef], + { env: gitEnvironment, timeout: 10_000 }, + ); + return stdout.trim().split("\t")[0] || null; +}; + +export const synchronizeRepository = async ( + id: string, + repository: string, + ref: string, +): Promise => { + validateId(id); + const safeRepository = validateRepository(repository); + const safeRef = validateRef(ref || "main"); + fs.mkdirSync(cacheRoot, { recursive: true }); + const target = path.resolve(cacheRoot, id); + if (!isWithin(cacheRoot, target) || target === cacheRoot) { + throw new Error("Git cache target is invalid."); + } + const temporary = fs.mkdtempSync(path.join(cacheRoot, `.${id}-`)); + fs.rmSync(temporary, { recursive: true, force: true }); + try { + await execFileAsync( + "git", + [ + "-c", + "protocol.ext.allow=never", + "clone", + "--depth", + "1", + "--branch", + safeRef, + "--", + safeRepository, + temporary, + ], + { env: gitEnvironment, timeout: 60_000 }, + ); + fs.rmSync(target, { recursive: true, force: true }); + fs.renameSync(temporary, target); + return target; + } catch (error) { + fs.rmSync(temporary, { recursive: true, force: true }); + throw error; + } +}; + +export const cachedRepositoryPath = (id: string): string => { + validateId(id); + const target = path.resolve(cacheRoot, id); + if (!isWithin(cacheRoot, target) || target === cacheRoot) { + throw new Error("Git cache target is invalid."); + } + return target; +}; + +export const listRepositoryFiles = ( + id: string, + predicate: (relative: string) => boolean = () => true, + maximum = 50, +): Array<{ path: string; sizeBytes: number }> => { + const root = cachedRepositoryPath(id); + if (!fs.existsSync(root)) return []; + const files: Array<{ path: string; sizeBytes: number }> = []; + const visit = (directory: string): void => { + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + if (files.length >= maximum) return; + if (entry.name === ".git") continue; + const absolute = path.join(directory, entry.name); + if (entry.isSymbolicLink()) continue; + if (entry.isDirectory()) visit(absolute); + if (entry.isFile()) { + const relative = path.relative(root, absolute).replace(/\\/g, "/"); + if (predicate(relative)) { + files.push({ path: relative, sizeBytes: fs.statSync(absolute).size }); + } + } + } + }; + visit(root); + return files.sort((a, b) => a.path.localeCompare(b.path)).slice(0, maximum); +}; diff --git a/src/backend/giteaDiscovery.test.ts b/src/backend/giteaDiscovery.test.ts new file mode 100644 index 0000000..b2c42dc --- /dev/null +++ b/src/backend/giteaDiscovery.test.ts @@ -0,0 +1,92 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { + fetchGiteaRepositories, + FixedWindowRateLimiter, + loadGiteaDiscoveryConfig, + toGitSource, +} from "./http/giteaDiscovery"; + +test("Gitea discovery requires an HTTPS allow-listed configured origin", () => { + assert.throws( + () => loadGiteaDiscoveryConfig({}), + /GITEA_TOKEN and GITEA_BASE_URL/, + ); + assert.throws( + () => + loadGiteaDiscoveryConfig({ + GITEA_TOKEN: "secret", + GITEA_BASE_URL: "http://git.example.test", + GITEA_ALLOWED_ORIGINS: "http://git.example.test", + }), + /HTTPS/, + ); + assert.throws( + () => + loadGiteaDiscoveryConfig({ + GITEA_TOKEN: "secret", + GITEA_BASE_URL: "https://git.example.test", + GITEA_ALLOWED_ORIGINS: "https://other.example.test", + }), + /not in GITEA_ALLOWED_ORIGINS/, + ); +}); + +test("Gitea requests authenticate, disable redirects, and use an abort signal", async () => { + const config = loadGiteaDiscoveryConfig({ + GITEA_TOKEN: "secret", + GITEA_BASE_URL: "https://git.example.test", + GITEA_ALLOWED_ORIGINS: "https://git.example.test", + }); + let requestInit: RequestInit | undefined; + const fakeFetch = (async (_input: unknown, init?: RequestInit) => { + requestInit = init; + return new Response(JSON.stringify({ + data: [ + { + name: "engine", + full_name: "platform/engine", + clone_url: "https://git.example.test/platform/engine.git", + default_branch: "main", + private: true, + }, + ], + })); + }) as typeof fetch; + + const repositories = await fetchGiteaRepositories(config, fakeFetch); + assert.equal(new Headers(requestInit?.headers).get("Authorization"), "token secret"); + assert.equal(requestInit?.redirect, "error"); + assert.ok(requestInit?.signal); + assert.equal(repositories.length, 1); + assert.equal(toGitSource(repositories[0]).id, "platform--engine"); +}); + +test("Gitea fetch timeout aborts the request", async () => { + const config = { + apiUrl: new URL("https://git.example.test/api/v1/repos/search"), + token: "secret", + timeoutMs: 5, + }; + const hangingFetch = ((_input: unknown, init?: RequestInit) => + new Promise((_resolve, reject) => { + init?.signal?.addEventListener("abort", () => + reject(new DOMException("aborted", "AbortError")), + ); + })) as typeof fetch; + await assert.rejects( + fetchGiteaRepositories(config, hangingFetch), + /timed out after 5ms/, + ); +}); + +test("Gitea discovery limiter is fixed-window and returns retry timing", () => { + const limiter = new FixedWindowRateLimiter(2, 1_000); + assert.equal(limiter.take("client", 0).allowed, true); + assert.equal(limiter.take("client", 1).allowed, true); + assert.deepEqual(limiter.take("client", 2), { + allowed: false, + retryAfter: 1, + }); + assert.equal(limiter.take("client", 1_000).allowed, true); +}); diff --git a/src/backend/http/contentRouter.ts b/src/backend/http/contentRouter.ts new file mode 100644 index 0000000..61412b2 --- /dev/null +++ b/src/backend/http/contentRouter.ts @@ -0,0 +1,128 @@ +import express from "express"; +import fs from "node:fs"; +import path from "node:path"; +import { cachedRepositoryPath, listRepositoryFiles } from "../gitOperations"; +import { store } from "../store"; + +const router = express.Router(); + +router.get("/style-packages", (_req, res) => { + res.json(store.stylePackages); +}); + +router.get("/style-instances", (_req, res) => { + res.json(store.styleInstances); +}); + +router.post("/style-instances", (req, res) => { + const instance = req.body; + store.styleInstances.push(instance); + store.addAudit( + "Add Style Instance", + "config", + `Created style instance '${instance.id}' using '${instance.uses}'`, + ); + res.json({ success: true, instance }); +}); + +router.put("/style-instances/:id", (req, res) => { + const index = store.styleInstances.findIndex( + (instance) => instance.id === req.params.id, + ); + if (index === -1) return res.status(404).json({ error: "Instance not found" }); + store.styleInstances[index] = { ...store.styleInstances[index], ...req.body }; + store.addAudit( + "Update Style Instance", + "config", + `Updated style instance '${req.params.id}'`, + ); + return res.json({ success: true, instance: store.styleInstances[index] }); +}); + +router.get("/content", (_req, res) => { + res.json(store.contentItems); +}); + +router.post("/content/discover", (_req, res) => { + store.addAudit( + "Discover Content", + "config", + "Executed content discovery scan across all Git sources", + ); + const discovered = []; + + for (const source of store.gitSources) { + if (source.health !== "Healthy" || !source.projectionEnabled) continue; + let root: string; + try { + root = cachedRepositoryPath(source.id); + } catch { + continue; + } + if (!fs.existsSync(root)) continue; + + const markdownFiles = listRepositoryFiles( + source.id, + (relative) => relative.toLowerCase().endsWith(".md"), + 10_000, + ); + for (const [index, file] of markdownFiles.entries()) { + const title = path.posix.basename(file.path, ".md").trim() || "Untitled"; + const declaredModels = source.produces || []; + const defaultModel = store.siteConfig.contentModels[0]?.id ?? "page"; + const artifactType = declaredModels[0] ?? defaultModel; + const slug = title.toLowerCase(); + const model = store.siteConfig.contentModels.find( + (candidate) => candidate.id === artifactType, + ); + const navigation = store.siteConfig.navigation.find( + (entry) => entry.contentModel === artifactType, + ); + const routePattern = + model?.routing?.detail ?? + (navigation + ? `${navigation.route}/{slug}/` + : `/${artifactType}s/{slug}/`); + discovered.push({ + id: `art-${source.id}-${index}`, + title: title + .replace(/-/g, " ") + .replace(/\b\w/g, (letter) => letter.toUpperCase()), + slug, + published: new Date().toISOString(), + status: "published" as const, + artifactType, + summary: `Discovered artifact from ${file.path}`, + tags: [source.id], + aliases: [], + sourceRepo: source.id, + path: file.path, + contentMarkdown: fs.readFileSync( + path.join(root, ...file.path.split("/")), + "utf8", + ), + mediaReferences: [], + youtubeDirectives: [], + wikipediaLinks: [], + validationStatus: "valid" as const, + validationMessages: [], + route: routePattern.replace("{slug}", slug).replace("//", "/"), + styleInstanceId: "default", + }); + } + } + + store.contentItems = discovered; + res.json({ + success: true, + discoveredCount: discovered.length, + mediaCount: store.mediaAssets.length, + items: discovered, + }); +}); + +router.get("/media", (_req, res) => { + res.json(store.mediaAssets); +}); + +export { router as contentRouter }; diff --git a/src/backend/http/controlRouter.ts b/src/backend/http/controlRouter.ts new file mode 100644 index 0000000..0af3ba8 --- /dev/null +++ b/src/backend/http/controlRouter.ts @@ -0,0 +1,124 @@ +import express from "express"; +import { CredentialRef } from "../../types"; +import { store } from "../store"; + +const router = express.Router(); + +router.get("/health", (_req, res) => { + res.json({ status: "ok", service: "labyricorn-site-builder" }); +}); + +router.get("/site-config", (_req, res) => { + res.json(store.siteConfig); +}); + +router.put("/site-config", (_req, res) => { + res.status(405).json({ + code: "E_CONFIG_READ_ONLY", + message: "Site configuration is Git-owned and read-only in v1.", + }); +}); + +router.get("/theme", (_req, res) => { + res.json(store.themeConfig); +}); + +router.post("/theme/validate", (_req, res) => { + const theme = store.validateTheme(); + res.status(theme.status === "valid" ? 200 : 422).json({ + success: theme.status === "valid", + theme, + commits: theme.commit ? { [theme.sourceId ?? "theme"]: theme.commit } : {}, + }); +}); + +router.get("/builds", (_req, res) => { + res.json(store.builds); +}); + +router.post("/builds/trigger", (_req, res) => { + res.json(store.runBuild()); +}); + +router.get("/nginx", (_req, res) => { + res.json(store.nginxStatus); +}); + +router.post("/nginx/test", (_req, res) => { + store.nginxStatus.configValid = true; + store.addAudit( + "Test Nginx Config", + "nginx", + "Control-plane simulation reported a valid Nginx configuration.", + ); + res.json({ + success: true, + simulated: true, + message: "Nginx validation is simulated by the control plane.", + }); +}); + +router.post("/nginx/activate", (req, res) => { + res.json(store.activateReleaseLocally(req.body.buildId)); +}); + +router.get("/push-targets", (_req, res) => { + res.json(store.pushTargets); +}); + +router.post("/push-targets/:id/push", (req, res) => { + const result = store.pushToTarget(req.params.id, req.body.buildId); + res.status(result.success ? 200 : 501).json(result); +}); + +router.post("/push-targets/:id/test", (_req, res) => { + res.status(501).json({ + success: false, + code: "E_REMOTE_PUBLICATION_NOT_IMPLEMENTED", + message: "Remote SSH and rsync checks are not implemented.", + }); +}); + +router.get("/credentials", (_req, res) => { + res.json(store.credentials); +}); + +router.post("/credentials", (req, res) => { + const { name, type, secretValue } = req.body; + if ( + typeof name !== "string" || + !["ssh_key", "token", "password"].includes(type) || + typeof secretValue !== "string" + ) { + return res.status(400).json({ + error: "name, type, and secretValue are required.", + }); + } + const id = `secret:${name.toLowerCase().replace(/[^a-z0-9]/g, "-")}`; + const masked = + type === "ssh_key" + ? `${secretValue.substring(0, 20)}...` + : `${secretValue.substring(0, 4)}********************`; + const credential: CredentialRef = { + id, + name, + type: type as CredentialRef["type"], + maskedValue: masked, + createdAt: new Date().toISOString(), + lastUsedAt: new Date().toISOString(), + usageCount: 0, + }; + store.credentials.push(credential); + store.addAudit( + "Add Credential", + "credential", + `Stored credential reference ${id}`, + ); + return res.json({ success: true, credential }); +}); + +router.get("/audit-logs", (_req, res) => { + res.json(store.auditLogs); +}); + +export { router as controlRouter }; diff --git a/src/backend/http/gitSourceRouter.ts b/src/backend/http/gitSourceRouter.ts new file mode 100644 index 0000000..6704539 --- /dev/null +++ b/src/backend/http/gitSourceRouter.ts @@ -0,0 +1,230 @@ +import express from "express"; +import fs from "node:fs"; +import { GitSource } from "../../types"; +import { + cachedRepositoryPath, + listRepositoryFiles, + resolveRemote, + synchronizeRepository, + validateRepository, +} from "../gitOperations"; +import { store } from "../store"; +import { + fetchGiteaRepositories, + FixedWindowRateLimiter, + loadGiteaDiscoveryConfig, + toGitSource, +} from "./giteaDiscovery"; + +const router = express.Router(); +const discoveryLimiter = new FixedWindowRateLimiter(); +let discoveryInFlight: Promise[]> | null = null; + +const errorMessage = (error: unknown): string => + error instanceof Error ? error.message : String(error); + +router.post("/gitea/discover", async (req, res) => { + const limit = discoveryLimiter.take(req.ip || req.socket.remoteAddress || "unknown"); + if (!limit.allowed) { + res.setHeader("Retry-After", String(limit.retryAfter)); + return res.status(429).json({ + code: "E_GITEA_DISCOVERY_RATE_LIMIT", + error: "Gitea discovery rate limit exceeded.", + success: false, + }); + } + + let config: ReturnType; + try { + config = loadGiteaDiscoveryConfig(); + } catch (error) { + return res.status(503).json({ + code: "E_GITEA_DISCOVERY_CONFIG", + error: errorMessage(error), + success: false, + }); + } + + store.addAudit( + "Gitea Discovery", + "config", + `Initiating repository discovery from ${config.apiUrl.origin}`, + ); + try { + discoveryInFlight ??= fetchGiteaRepositories(config) + .then((repositories) => repositories.map(toGitSource)) + .finally(() => { + discoveryInFlight = null; + }); + const discovered = await discoveryInFlight; + const added = store.mergeDiscoveredGitSources(discovered); + return res.json({ + success: true, + discoveredCount: discovered.length, + addedCount: added, + message: `Successfully discovered ${discovered.length} repositories from Gitea.`, + }); + } catch (error) { + const message = errorMessage(error); + store.addAudit("Gitea Discovery Failed", "config", message, "failure"); + return res.status(502).json({ error: message, success: false }); + } +}); + +router.get("/git-sources", (_req, res) => { + res.json(store.gitSources); +}); + +router.post("/git-sources", async (req, res) => { + const body = req.body as Partial; + if ( + typeof body.id !== "string" || + typeof body.repository !== "string" || + typeof body.ref !== "string" + ) { + return res.status(400).json({ error: "id, repository, and ref are required." }); + } + + try { + validateRepository(body.repository); + } catch (error) { + return res.status(400).json({ error: errorMessage(error) }); + } + + const source: GitSource = { + id: body.id, + repository: body.repository, + ref: body.ref, + branch: body.branch || body.ref, + type: body.type || "hybrid", + required: body.required ?? false, + credential: body.credential, + health: "Warning", + reachability: "Unreachable", + configurationState: "Not Configured", + visibility: "Private in Gitea", + projectionEnabled: false, + produces: [], + explainWhy: "Awaiting initial synchronization.", + status: "syncing", + }; + + try { + const commit = await resolveRemote(source.repository, source.ref); + if (commit) { + source.status = "connected"; + source.reachability = "Healthy"; + source.lastResolvedCommit = commit; + source.lastSyncedAt = new Date().toISOString(); + source.explainWhy = + "Connected to repository, but missing .labyricorn/site.yml metadata."; + } else { + source.status = "error"; + source.explainWhy = "Ref not found in repository."; + } + } catch (error) { + source.status = "error"; + source.explainWhy = `Git ls-remote failed: ${errorMessage(error)}`; + } + + store.gitSources.push(source); + store.addAudit( + "Add Git Source", + "source", + `Added Git source ${source.id} (${source.repository})`, + ); + return res.json({ success: true, source }); +}); + +router.put("/git-sources/:id", (req, res) => { + const index = store.gitSources.findIndex((source) => source.id === req.params.id); + if (index === -1) return res.status(404).json({ error: "Source not found" }); + store.gitSources[index] = { ...store.gitSources[index], ...req.body }; + store.addAudit( + "Update Git Source", + "source", + `Updated Git source ${req.params.id}`, + ); + return res.json({ success: true, source: store.gitSources[index] }); +}); + +router.delete("/git-sources/:id", (req, res) => { + store.gitSources = store.gitSources.filter( + (source) => source.id !== req.params.id, + ); + store.addAudit( + "Delete Git Source", + "source", + `Removed Git source ${req.params.id}`, + ); + res.json({ success: true }); +}); + +router.post("/git-sources/:id/test", async (req, res) => { + const source = store.gitSources.find((item) => item.id === req.params.id); + if (!source) return res.status(404).json({ error: "Source not found" }); + source.status = "syncing"; + source.explainWhy = "Synchronization running..."; + + try { + const commit = await resolveRemote(source.repository, source.ref || "HEAD"); + if (!commit) { + source.status = "error"; + source.explainWhy = "Ref not found in repository."; + return res.status(400).json({ error: source.explainWhy }); + } + + source.status = "connected"; + source.reachability = "Healthy"; + source.lastResolvedCommit = commit; + source.lastSyncedAt = new Date().toISOString(); + const target = await synchronizeRepository( + source.id, + source.repository, + source.ref || "main", + ); + if (fs.existsSync(`${target}/.labyricorn/site.yml`)) { + source.configurationState = "Valid"; + source.projectionEnabled = true; + source.explainWhy = + "Repository synchronized and valid site.yml detected."; + source.health = "Healthy"; + } else { + source.configurationState = "Not Configured"; + source.projectionEnabled = false; + source.explainWhy = + "Repository synchronized, but missing .labyricorn/site.yml metadata."; + source.health = "Warning"; + } + return res.json({ + success: true, + message: `Successfully connected and synchronized ${source.repository}!`, + resolvedCommit: source.lastResolvedCommit, + syncedAt: source.lastSyncedAt, + }); + } catch (error) { + source.status = "error"; + source.health = "Invalid"; + source.explainWhy = `Git sync failed: ${errorMessage(error)}`; + return res.status(400).json({ error: source.explainWhy }); + } +}); + +router.get("/git-sources/:id/tree", (req, res) => { + if (!store.gitSources.some((source) => source.id === req.params.id)) { + return res.status(404).json({ error: "Source not found" }); + } + try { + cachedRepositoryPath(req.params.id); + const files = listRepositoryFiles(req.params.id).map((file) => ({ + path: file.path, + type: "file", + size: `${file.sizeBytes} B`, + })); + return res.json({ sourceId: req.params.id, files }); + } catch { + return res.json({ sourceId: req.params.id, files: [] }); + } +}); + +export { router as gitSourceRouter }; diff --git a/src/backend/http/giteaDiscovery.ts b/src/backend/http/giteaDiscovery.ts new file mode 100644 index 0000000..64a87b0 --- /dev/null +++ b/src/backend/http/giteaDiscovery.ts @@ -0,0 +1,198 @@ +import { GitSource } from "../../types"; + +const DEFAULT_TIMEOUT_MS = 5_000; +const DEFAULT_RATE_LIMIT = 5; +const DEFAULT_RATE_WINDOW_MS = 60_000; + +export interface GiteaDiscoveryConfig { + apiUrl: URL; + token: string; + timeoutMs: number; +} + +interface GiteaRepository { + clone_url?: unknown; + default_branch?: unknown; + full_name?: unknown; + name?: unknown; + private?: unknown; + ssh_url?: unknown; +} + +export interface DiscoveredRepository { + cloneUrl: string; + defaultBranch: string; + fullName: string; + name: string; + private: boolean; + sshUrl?: string; +} + +const positiveInteger = (value: string | undefined, fallback: number): number => { + if (value === undefined) return fallback; + const parsed = Number.parseInt(value, 10); + if (!Number.isInteger(parsed) || parsed < 1) { + throw new Error(`Invalid positive integer configuration value '${value}'.`); + } + return parsed; +}; + +const allowedOrigins = (value: string | undefined): Set => + new Set( + (value ?? "") + .split(",") + .map((entry) => entry.trim()) + .filter(Boolean) + .map((entry) => new URL(entry).origin), + ); + +export const loadGiteaDiscoveryConfig = ( + env: NodeJS.ProcessEnv = process.env, +): GiteaDiscoveryConfig => { + const token = env.GITEA_TOKEN?.trim(); + const baseUrl = env.GITEA_BASE_URL?.trim(); + if (!token || !baseUrl) { + throw new Error("Gitea discovery requires GITEA_TOKEN and GITEA_BASE_URL."); + } + + const base = new URL(baseUrl); + if (base.protocol !== "https:" || base.username || base.password) { + throw new Error("GITEA_BASE_URL must be an HTTPS origin without credentials."); + } + + const allowlist = allowedOrigins(env.GITEA_ALLOWED_ORIGINS); + if (!allowlist.has(base.origin)) { + throw new Error( + `GITEA_BASE_URL origin '${base.origin}' is not in GITEA_ALLOWED_ORIGINS.`, + ); + } + + const apiUrl = new URL("/api/v1/repos/search", base); + apiUrl.searchParams.set("limit", "50"); + return { + apiUrl, + token, + timeoutMs: positiveInteger( + env.GITEA_DISCOVERY_TIMEOUT_MS, + DEFAULT_TIMEOUT_MS, + ), + }; +}; + +const repository = (value: GiteaRepository): DiscoveredRepository | null => { + if ( + typeof value.name !== "string" || + typeof value.clone_url !== "string" || + value.name.length === 0 || + value.clone_url.length === 0 + ) { + return null; + } + const fullName = + typeof value.full_name === "string" && value.full_name.length > 0 + ? value.full_name + : value.name; + return { + cloneUrl: value.clone_url, + defaultBranch: + typeof value.default_branch === "string" && value.default_branch.length > 0 + ? value.default_branch + : "main", + fullName, + name: value.name, + private: value.private === true, + sshUrl: typeof value.ssh_url === "string" ? value.ssh_url : undefined, + }; +}; + +export const fetchGiteaRepositories = async ( + config: GiteaDiscoveryConfig, + fetchImplementation: typeof fetch = fetch, +): Promise => { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), config.timeoutMs); + try { + const response = await fetchImplementation(config.apiUrl, { + headers: { + Accept: "application/json", + Authorization: `token ${config.token}`, + }, + redirect: "error", + signal: controller.signal, + }); + if (!response.ok) throw new Error(`Gitea returned HTTP ${response.status}.`); + const document = (await response.json()) as { data?: unknown }; + if (!Array.isArray(document.data)) { + throw new Error("Gitea returned an invalid repository search response."); + } + return document.data + .map((entry) => repository(entry as GiteaRepository)) + .filter((entry): entry is DiscoveredRepository => entry !== null); + } catch (error) { + if (controller.signal.aborted) { + throw new Error(`Gitea discovery timed out after ${config.timeoutMs}ms.`); + } + throw error; + } finally { + clearTimeout(timeout); + } +}; + +const sourceId = (repo: DiscoveredRepository): string => + repo.fullName + .toLowerCase() + .replace(/[^a-z0-9._-]+/g, "--") + .replace(/^-+|-+$/g, "") + .slice(0, 128) || repo.name.toLowerCase(); + +export const toGitSource = (repo: DiscoveredRepository): GitSource => ({ + id: sourceId(repo), + repository: repo.cloneUrl, + ref: repo.defaultBranch, + branch: repo.defaultBranch, + type: "hybrid", + health: "Warning", + reachability: "Unreachable", + configurationState: "Not Configured", + visibility: repo.private ? "Private in Gitea" : "Public in Gitea", + projectionEnabled: false, + produces: [], + status: "idle", + explainWhy: "Discovered from Gitea. Awaiting initial synchronization.", + required: false, + credential: "none", +}); + +export class FixedWindowRateLimiter { + private readonly clients = new Map< + string, + { count: number; startedAt: number } + >(); + + constructor( + private readonly maximum = positiveInteger( + process.env.GITEA_DISCOVERY_RATE_LIMIT, + DEFAULT_RATE_LIMIT, + ), + private readonly windowMs = positiveInteger( + process.env.GITEA_DISCOVERY_RATE_WINDOW_MS, + DEFAULT_RATE_WINDOW_MS, + ), + ) {} + + take(client: string, now = Date.now()): { allowed: boolean; retryAfter: number } { + const current = this.clients.get(client); + if (!current || now - current.startedAt >= this.windowMs) { + this.clients.set(client, { count: 1, startedAt: now }); + return { allowed: true, retryAfter: 0 }; + } + if (current.count >= this.maximum) { + return { + allowed: false, + retryAfter: Math.ceil((this.windowMs - (now - current.startedAt)) / 1_000), + }; + } + current.count += 1; + return { allowed: true, retryAfter: 0 }; + } +} diff --git a/src/backend/http/previewRouter.ts b/src/backend/http/previewRouter.ts new file mode 100644 index 0000000..a8e810a --- /dev/null +++ b/src/backend/http/previewRouter.ts @@ -0,0 +1,58 @@ +import express from "express"; +import fs from "node:fs"; +import path from "node:path"; +import { store } from "../store"; + +const router = express.Router(); + +router.get("/live-site/html", (req, res) => { + const rootDir = store.getPreviewDirectory(); + if (!rootDir) { + return res.status(503).json({ + code: "E_RELEASE_UNAVAILABLE", + message: "No verified staging or live release is selected.", + }); + } + + let requested: string; + try { + requested = decodeURIComponent( + String(req.query.route ?? "/").split(/[?#]/, 1)[0], + ); + } catch { + return res.status(400).json({ code: "E_ROUTE_INVALID" }); + } + if ( + !requested.startsWith("/") || + requested.includes("\\") || + requested.split("/").includes("..") + ) { + return res.status(400).json({ code: "E_ROUTE_INVALID" }); + } + + const relative = requested.replace(/^\/+/, ""); + const outputFile = + relative && path.posix.extname(relative) + ? relative + : path.posix.join(relative, "index.html"); + const rootPath = `${path.resolve(rootDir)}${path.sep}`; + const target = path.resolve(rootDir, ...outputFile.split("/")); + if (!target.startsWith(rootPath)) { + return res.status(400).json({ code: "E_ROUTE_INVALID" }); + } + + res.setHeader( + "Content-Security-Policy", + "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'self'", + ); + if (fs.existsSync(target) && fs.statSync(target).isFile()) { + return res.type(path.extname(target)).send(fs.readFileSync(target)); + } + const notFound = path.join(rootDir, "404.html"); + if (fs.existsSync(notFound)) { + return res.status(404).type("html").send(fs.readFileSync(notFound)); + } + return res.status(404).json({ code: "E_ROUTE_NOT_FOUND" }); +}); + +export { router as previewRouter }; diff --git a/src/backend/releaseLifecycle.test.ts b/src/backend/releaseLifecycle.test.ts new file mode 100644 index 0000000..87fc577 --- /dev/null +++ b/src/backend/releaseLifecycle.test.ts @@ -0,0 +1,161 @@ +import assert from "node:assert/strict"; +import { ChildProcess, spawn } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; +import { BuildEngine } from "./buildEngine"; +import { + commit, + createRepository, + loadFixture, +} from "./testing/repositoryFixture"; + +const waitForExit = (child: ChildProcess): Promise => + new Promise((resolve, reject) => { + let stdout = ""; + let stderr = ""; + child.stdout?.on("data", (chunk) => { + stdout += chunk; + }); + child.stderr?.on("data", (chunk) => { + stderr += chunk; + }); + child.on("error", reject); + child.on("exit", (code) => { + if (code === 0) resolve(stdout.trim()); + else reject(new Error(`worker exited ${code}: ${stderr}`)); + }); + }); + +const waitFor = async (predicate: () => boolean): Promise => { + const deadline = Date.now() + 10_000; + while (!predicate()) { + if (Date.now() > deadline) throw new Error("Timed out waiting for workers."); + await new Promise((resolve) => setTimeout(resolve, 10)); + } +}; + +test("concurrent writers converge on one verified immutable release", async () => { + const root = createRepository(); + const output = fs.mkdtempSync(path.join(os.tmpdir(), "concurrent-release-")); + const barrier = path.join(output, "barrier"); + const worker = path.resolve("src/backend/testing/concurrentBuildWorker.ts"); + try { + const children = [1, 2].map(() => + spawn(process.execPath, ["--import", "tsx", worker, root, output, barrier], { + cwd: process.cwd(), + stdio: ["ignore", "pipe", "pipe"], + }), + ); + const completions = children.map(waitForExit); + await waitFor( + () => + fs + .readdirSync(output) + .filter((entry) => entry.startsWith("barrier.") && entry.endsWith(".ready")) + .length === 2, + ); + fs.writeFileSync(`${barrier}.go`, ""); + const checksums = await Promise.all(completions); + assert.equal(new Set(checksums).size, 1); + + const releases = fs.readdirSync(path.join(output, "releases")); + assert.equal(releases.length, 1); + new BuildEngine(output).verifyRelease(releases[0]); + assert.equal( + fs.readdirSync(output).some((entry) => entry.startsWith(".release-")), + false, + ); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + fs.rmSync(output, { recursive: true, force: true }); + } +}); + +test("partial rendering failure leaves no release or temporary directory", () => { + const root = createRepository(); + const output = fs.mkdtempSync(path.join(os.tmpdir(), "partial-release-")); + try { + const template = path.join( + root, + "packages/site-definition/.theme/templates/home.liquid", + ); + fs.appendFileSync(template, "\n{{ undefined.value }}\n"); + commit(root, "break rendering"); + const fixture = loadFixture(root, "partial-failure"); + try { + const engine = new BuildEngine(output); + assert.throws(() => engine.build(fixture.input), /undefined|not defined/i); + assert.equal(fs.existsSync(engine.buildDirectory(fixture.input.artifactBuildId)), false); + assert.equal( + fs.readdirSync(output).some((entry) => entry.startsWith(".release-")), + false, + ); + } finally { + fixture.manager.dispose(); + } + } finally { + fs.rmSync(root, { recursive: true, force: true }); + fs.rmSync(output, { recursive: true, force: true }); + } +}); + +test("staging and current pointers promote only checksum-verified releases", () => { + const root = createRepository(); + const output = fs.mkdtempSync(path.join(os.tmpdir(), "promotion-release-")); + const first = loadFixture(root, "first"); + try { + const engine = new BuildEngine(output); + const firstResult = engine.build(first.input); + engine.stage(first.input.artifactBuildId); + engine.activate(first.input.artifactBuildId); + + const css = path.join( + root, + "packages/site-definition/.theme/assets/styles/theme.css", + ); + fs.appendFileSync(css, "\n.second-release{display:block}\n"); + commit(root, "second release"); + const second = loadFixture(root, "second"); + try { + const secondResult = engine.build(second.input); + engine.stage(second.input.artifactBuildId); + assert.equal(path.basename(fs.realpathSync(engine.currentDirectory())), first.input.artifactBuildId); + assert.equal(path.basename(fs.realpathSync(engine.stagingDirectory())), second.input.artifactBuildId); + + engine.activate(second.input.artifactBuildId); + assert.equal(path.basename(fs.realpathSync(engine.currentDirectory())), second.input.artifactBuildId); + assert.equal(path.basename(fs.realpathSync(engine.stagingDirectory())), second.input.artifactBuildId); + + fs.appendFileSync(path.join(firstResult.outputDirectory, "index.html"), "tamper"); + assert.throws(() => engine.activate(first.input.artifactBuildId), /E_RELEASE_CHECKSUM/); + assert.equal(path.basename(fs.realpathSync(engine.currentDirectory())), second.input.artifactBuildId); + assert.ok(fs.existsSync(secondResult.outputDirectory)); + } finally { + second.manager.dispose(); + } + } finally { + first.manager.dispose(); + fs.rmSync(root, { recursive: true, force: true }); + fs.rmSync(output, { recursive: true, force: true }); + } +}); + +test("repeated identical builds retain identical artifact checksums", () => { + const root = createRepository(); + const output = fs.mkdtempSync(path.join(os.tmpdir(), "idempotent-release-")); + const fixture = loadFixture(root, "idempotent"); + try { + const engine = new BuildEngine(output); + const checksums = Array.from( + { length: 20 }, + () => engine.build(fixture.input).artifactChecksum, + ); + assert.equal(new Set(checksums).size, 1); + } finally { + fixture.manager.dispose(); + fs.rmSync(root, { recursive: true, force: true }); + fs.rmSync(output, { recursive: true, force: true }); + } +}); diff --git a/src/backend/repositoryBuildEngine.ts b/src/backend/repositoryBuildEngine.ts index 75aaf4a..37300de 100644 --- a/src/backend/repositoryBuildEngine.ts +++ b/src/backend/repositoryBuildEngine.ts @@ -375,7 +375,19 @@ export class BuildEngine { const artifactChecksum = `sha256-${sha256(checksumsDocument)}`; const generatedFiles = [...files].sort(); const artifactSizeBytes = generatedFiles.reduce((sum, relative) => sum + fs.statSync(path.join(temporaryDirectory, ...relative.split("/"))).size, 0); - fs.renameSync(temporaryDirectory, outputDirectory); + try { + fs.renameSync(temporaryDirectory, outputDirectory); + } catch (error) { + if (!fs.existsSync(outputDirectory)) throw error; + const verified = this.verifyRelease(input.artifactBuildId); + fs.rmSync(temporaryDirectory, { recursive: true, force: true }); + fs.chmodSync(outputDirectory, 0o750); + return { + success: true, outputDirectory, generatedRoutesCount: verified.routes.length, + artifactChecksum: verified.artifactChecksum, artifactSizeBytes: verified.sizeBytes, + validationReport, generatedFiles: verified.files, + }; + } fs.chmodSync(outputDirectory, 0o750); return { success: true, outputDirectory, generatedRoutesCount: routes.size, artifactChecksum, artifactSizeBytes, validationReport, generatedFiles }; } catch (error) { @@ -432,8 +444,28 @@ export class BuildEngine { const pointer = path.join(this.outputRoot, name); const next = path.join(this.outputRoot, `.${name}-${process.pid}-${crypto.randomUUID()}`); fs.symlinkSync(target, next, process.platform === "win32" ? "junction" : "dir"); - try { if (fs.lstatSync(pointer, { throwIfNoEntry: false })) fs.rmSync(pointer, { recursive: true, force: true }); fs.renameSync(next, pointer); } - catch (error) { fs.rmSync(next, { recursive: true, force: true }); throw error; } + try { + if (process.platform !== "win32") { + fs.renameSync(next, pointer); + } else { + const backup = path.join( + this.outputRoot, + `.${name}-backup-${process.pid}-${crypto.randomUUID()}`, + ); + const existing = fs.lstatSync(pointer, { throwIfNoEntry: false }); + if (existing) fs.renameSync(pointer, backup); + try { + fs.renameSync(next, pointer); + if (existing) fs.rmSync(backup, { recursive: true, force: true }); + } catch (error) { + if (existing && !fs.existsSync(pointer)) fs.renameSync(backup, pointer); + throw error; + } + } + } catch (error) { + fs.rmSync(next, { recursive: true, force: true }); + throw error; + } return pointer; } } diff --git a/src/backend/repositorySnapshot.test.ts b/src/backend/repositorySnapshot.test.ts new file mode 100644 index 0000000..1c1ae07 --- /dev/null +++ b/src/backend/repositorySnapshot.test.ts @@ -0,0 +1,43 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; +import { RepositorySnapshotManager } from "./repositories/repositorySnapshot"; +import { commit, createRepository, git } from "./testing/repositoryFixture"; + +test("snapshot loader materializes and retains one exact commit", () => { + const root = createRepository(); + const workRoot = fs.mkdtempSync(path.join(os.tmpdir(), "snapshot-test-")); + const manager = new RepositorySnapshotManager(workRoot); + try { + const expectedCommit = git(root, ["rev-parse", "HEAD"]); + const snapshot = manager.resolve("site-definition", root, "HEAD"); + const css = path.join( + snapshot.checkoutRoot, + "packages/site-definition/.theme/assets/styles/theme.css", + ); + const original = fs.readFileSync(css, "utf8"); + + fs.appendFileSync( + path.join( + root, + "packages/site-definition/.theme/assets/styles/theme.css", + ), + "\n.branch-advanced{display:block}\n", + ); + const advancedCommit = commit(root, "advance branch"); + + assert.equal(snapshot.commit, expectedCommit); + assert.notEqual(snapshot.commit, advancedCommit); + assert.equal(fs.readFileSync(css, "utf8"), original); + assert.equal(fs.existsSync(path.join(snapshot.checkoutRoot, ".git")), false); + const checkoutRoot = snapshot.checkoutRoot; + manager.dispose(); + assert.equal(fs.existsSync(checkoutRoot), false); + } finally { + manager.dispose(); + fs.rmSync(root, { recursive: true, force: true }); + fs.rmSync(workRoot, { recursive: true, force: true }); + } +}); diff --git a/src/backend/store.ts b/src/backend/store.ts index 1b414dd..d44b406 100644 --- a/src/backend/store.ts +++ b/src/backend/store.ts @@ -132,6 +132,18 @@ export class LabyricornStore { this.auditLogs.unshift({ id: `log-${Date.now()}-${this.auditLogs.length}`, timestamp: new Date().toISOString(), user: "admin@labyricorn.local", action, category, details, result }); } + mergeDiscoveredGitSources(discovered: readonly GitSource[]): number { + const repositories = new Set(this.gitSources.map((source) => source.repository)); + const additions: GitSource[] = []; + for (const source of discovered) { + if (repositories.has(source.repository)) continue; + repositories.add(source.repository); + additions.push(source); + } + if (additions.length > 0) this.gitSources = [...this.gitSources, ...additions]; + return additions.length; + } + validateTheme(): ThemeConfig { const loader = this.loader(); try { @@ -235,6 +247,10 @@ export class LabyricornStore { return null; } pushToTarget(targetId: string, buildId: string): { success: boolean; logs: string[] } { + // Deliberately fail closed: no configuration field can enable remote execution. + if (!REMOTE_PUBLICATION_IMPLEMENTED) { + return { success: false, logs: ["Remote publication is not implemented."] }; + } const target = this.pushTargets.find((item) => item.id === targetId); const build = this.builds.find((item) => item.id === buildId); if (!target || !build?.artifactBuildId) return { success: false, logs: ["Target or verified build not found."] }; @@ -244,4 +260,5 @@ export class LabyricornStore { rollbackLocal(targetReleaseId: string): { success: boolean; message: string } { return this.activateReleaseLocally(targetReleaseId); } } +export const REMOTE_PUBLICATION_IMPLEMENTED = false as const; export const store = new LabyricornStore(); diff --git a/src/backend/testing/concurrentBuildWorker.ts b/src/backend/testing/concurrentBuildWorker.ts new file mode 100644 index 0000000..723c023 --- /dev/null +++ b/src/backend/testing/concurrentBuildWorker.ts @@ -0,0 +1,21 @@ +import fs from "node:fs"; +import { BuildEngine } from "../buildEngine"; +import { loadFixture } from "./repositoryFixture"; + +const [repository, output, barrier] = process.argv.slice(2); +if (!repository || !output || !barrier) { + throw new Error("repository, output, and barrier arguments are required."); +} + +const fixture = loadFixture(repository, `worker-${process.pid}`); +try { + fs.writeFileSync(`${barrier}.${process.pid}.ready`, ""); + const waitBuffer = new Int32Array(new SharedArrayBuffer(4)); + while (!fs.existsSync(`${barrier}.go`)) { + Atomics.wait(waitBuffer, 0, 0, 10); + } + const result = new BuildEngine(output).build(fixture.input); + process.stdout.write(`${result.artifactChecksum}\n`); +} finally { + fixture.manager.dispose(); +} diff --git a/src/backend/testing/repositoryFixture.ts b/src/backend/testing/repositoryFixture.ts new file mode 100644 index 0000000..5067ee4 --- /dev/null +++ b/src/backend/testing/repositoryFixture.ts @@ -0,0 +1,53 @@ +import { execFileSync } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { BuildInputLoader } from "../buildInputLoader"; +import { BUILDER_VERSION } from "../buildEngine"; + +export const git = (root: string, args: string[]): string => + execFileSync("git", ["-C", root, ...args], { + encoding: "utf8", + env: { + ...process.env, + GIT_AUTHOR_DATE: "2026-01-01T00:00:00Z", + GIT_COMMITTER_DATE: "2026-01-01T00:00:00Z", + }, + }).trim(); + +export const createRepository = (): string => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "labyricorn-git-fixture-")); + fs.mkdirSync(path.join(root, "packages"), { recursive: true }); + fs.cpSync( + path.resolve("packages/site-definition"), + path.join(root, "packages/site-definition"), + { recursive: true }, + ); + fs.cpSync( + path.resolve("packages/content-models"), + path.join(root, "packages/content-models"), + { recursive: true }, + ); + git(root, ["init", "-b", "main"]); + git(root, ["config", "user.name", "Fixture"]); + git(root, ["config", "user.email", "fixture@example.test"]); + git(root, ["add", "."]); + git(root, ["commit", "-m", "fixture"]); + return root; +}; + +export const commit = (root: string, message: string): string => { + git(root, ["add", "-A"]); + git(root, ["commit", "-m", message]); + return git(root, ["rev-parse", "HEAD"]); +}; + +export const loadFixture = (root: string, runId: string) => { + const manager = new BuildInputLoader({ + repositoryRoot: root, + ref: "HEAD", + siteDefinitionPath: "packages/site-definition", + builderVersion: BUILDER_VERSION, + }); + return { manager, input: manager.load(runId) }; +};