diff --git a/baas-control-plane/.env.example b/baas-control-plane/.env.example index c691894..5ea2417 100644 --- a/baas-control-plane/.env.example +++ b/baas-control-plane/.env.example @@ -1,18 +1,5 @@ -NODE_ENV=development -BACKEND_PORT=4000 -ADMIN_API_TOKEN=change-me -DATA_DIR=./data - -# Appwrite admin credentials (to create projects) -APPWRITE_ADMIN_ENDPOINT=https://cloud.appwrite.io/v1 -APPWRITE_ADMIN_PROJECT_ID=console -APPWRITE_ADMIN_API_KEY= - -# Defaults for new tenant projects -DEFAULT_APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1 -APPWRITE_DEFAULT_RUNTIME=deno-1.35 - -# Optional setup script target -APPWRITE_SETUP_ENDPOINT= -APPWRITE_SETUP_PROJECT_ID= -APPWRITE_SETUP_API_KEY= +PORT=4000 +APPWRITE_ENDPOINT=https://cloud.appwrite.io +APPWRITE_API_KEY=replace-with-appwrite-key +SUPABASE_ENDPOINT=https://api.supabase.com +SUPABASE_SERVICE_KEY=replace-with-supabase-key diff --git a/baas-control-plane/README.md b/baas-control-plane/README.md new file mode 100644 index 0000000..59a6e51 --- /dev/null +++ b/baas-control-plane/README.md @@ -0,0 +1,56 @@ +# baas-control-plane + +Control plane multi-tenant para orquestrar provedores BaaS (Appwrite, Supabase) com foco em provisioning, schema, secrets, métricas e auditoria. + +## Visão geral +- Backend Node.js + TypeScript com Fastify +- Multi-tenant com isolamento lógico por tenant +- Providers plugáveis sem lógica de negócio +- Serviços centrais para provisioning, schema, secrets, finops e audit + +## Arquitetura +``` +/src + /core + /providers + /modules + /lib + main.ts +``` + +Detalhes adicionais em [docs/architecture.md](docs/architecture.md). + +## Fluxo multi-tenant +1. Criar tenant (`POST /tenants`) +2. Criar projeto para o tenant (`POST /tenants/:id/projects`) +3. Provisionar projeto no provider (`POST /projects/:id/provision`) +4. Sincronizar schema (`POST /projects/:id/schema/sync`) +5. Coletar métricas (`GET /projects/:id/metrics`) + +## Como adicionar um novo provider +1. Criar pasta em `src/providers/` +2. Implementar `client`, `provisioning`, `schema`, `metrics` +3. Registrar no `provider.factory.ts` +4. Adicionar variáveis em `.env.example` e no `SecretsService` + +## Como subir localmente +```bash +cp .env.example .env +npm install +npm run dev +``` + +### Docker +```bash +docker compose up --build +``` + +## API mínima +- `POST /tenants` +- `GET /tenants` +- `POST /tenants/:id/projects` +- `GET /tenants/:id/projects` +- `POST /projects/:id/provision` +- `POST /projects/:id/schema/sync` +- `GET /projects/:id/metrics` +- `GET /health` diff --git a/baas-control-plane/docs/architecture.md b/baas-control-plane/docs/architecture.md new file mode 100644 index 0000000..29ddcfd --- /dev/null +++ b/baas-control-plane/docs/architecture.md @@ -0,0 +1,17 @@ +# Arquitetura + +O `baas-control-plane` implementa um control plane modular para gerenciar múltiplos provedores BaaS de forma multi-tenant. Ele centraliza provisioning, schema, secrets, métricas e auditoria sem executar workloads de clientes. + +## Camadas +- **core**: tipos e interface dos providers. +- **providers**: implementações técnicas de Appwrite e Supabase. +- **modules**: serviços de negócio (tenants, projects, provisioning, schema, secrets, finops, audit). +- **lib**: utilitários de ambiente, logger e HTTP. + +## Fluxo básico +1. Tenant é criado e armazenado. +2. Projeto é criado e vinculado a um provider. +3. Provisioning aciona o provider e salva o `externalId`. +4. Schema é versionado e aplicado via provider. +5. FinOps coleta métricas normalizadas. +6. Auditoria registra eventos relevantes. diff --git a/baas-control-plane/docs/providers.md b/baas-control-plane/docs/providers.md new file mode 100644 index 0000000..473ad3a --- /dev/null +++ b/baas-control-plane/docs/providers.md @@ -0,0 +1,21 @@ +# Providers + +Os providers implementam apenas comandos técnicos e não contêm regras de negócio. + +## Interface obrigatória +- `createProject` +- `deleteProject` +- `applySchema` +- `collectMetrics` +- `rotateSecrets` +- `healthCheck` + +## Implementações iniciais +- Appwrite: `src/providers/appwrite` +- Supabase: `src/providers/supabase` + +## Extensão +1. Crie `src/providers/` +2. Implemente `ProviderInterface` +3. Registre no `provider.factory.ts` +4. Configure secrets no `SecretsService` diff --git a/baas-control-plane/docs/security.md b/baas-control-plane/docs/security.md new file mode 100644 index 0000000..5611fcf --- /dev/null +++ b/baas-control-plane/docs/security.md @@ -0,0 +1,11 @@ +# Segurança + +## Princípios +- Providers não acessam `.env` diretamente. +- Secrets são entregues via `SecretsService`. +- Preparado para integração com Vault/Infisical. + +## Boas práticas +- Não faça hardcode de credenciais. +- Rotacione secrets via `rotateSecrets`. +- Audite eventos críticos (tenant, projeto, schema, secrets). diff --git a/baas-control-plane/package-lock.json b/baas-control-plane/package-lock.json index c44f05b..e97e7aa 100644 --- a/baas-control-plane/package-lock.json +++ b/baas-control-plane/package-lock.json @@ -1,22 +1,19 @@ { - "name": "core-backend", + "name": "baas-control-plane", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "core-backend", + "name": "baas-control-plane", "version": "1.0.0", "dependencies": { - "cors": "^2.8.5", + "@fastify/cors": "^9.0.1", "dotenv": "^16.4.5", - "express": "^4.19.2", - "node-appwrite": "^14.1.0", + "fastify": "^4.27.0", "zod": "^3.23.8" }, "devDependencies": { - "@types/cors": "^2.8.17", - "@types/express": "^4.17.21", "@types/node": "^20.12.12", "tsx": "^4.15.7", "typescript": "^5.4.5" @@ -464,75 +461,55 @@ "node": ">=18" } }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, + "node_modules/@fastify/ajv-compiler": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-3.6.0.tgz", + "integrity": "sha512-LwdXQJjmMD+GwLOkP7TVC68qa+pSSogeWWmznRJ/coyTcfe9qA05AHFSe1eZFwK6q+xVRpChnvFUkf1iYaSZsQ==", "license": "MIT", "dependencies": { - "@types/connect": "*", - "@types/node": "*" + "ajv": "^8.11.0", + "ajv-formats": "^2.1.1", + "fast-uri": "^2.0.0" } }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, + "node_modules/@fastify/cors": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-9.0.1.tgz", + "integrity": "sha512-YY9Ho3ovI+QHIL2hW+9X4XqQjXLjJqsU+sMV/xFsxZkE8p3GNnYVFpoOxF7SsP5ZL76gwvbo3V9L+FIekBGU4Q==", "license": "MIT", "dependencies": { - "@types/node": "*" + "fastify-plugin": "^4.0.0", + "mnemonist": "0.39.6" } }, - "node_modules/@types/cors": { - "version": "2.8.19", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", - "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/express": { - "version": "4.17.25", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", - "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "^1" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.19.7", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", - "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true, + "node_modules/@fastify/error": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-3.4.1.tgz", + "integrity": "sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==", "license": "MIT" }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-4.3.0.tgz", + "integrity": "sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==", + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^5.7.0" + } + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz", + "integrity": "sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", "license": "MIT" }, "node_modules/@types/node": { @@ -545,153 +522,78 @@ "undici-types": "~6.21.0" } }, - "node_modules/@types/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", - "dev": true, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", "license": "MIT" }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", - "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", - "dev": true, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", - "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "<1" - } - }, - "node_modules/@types/serve-static/node_modules/@types/send": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", - "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, - "node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", - "license": "MIT", - "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.14.0", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "license": "MIT", "dependencies": { - "safe-buffer": "5.2.1" + "ajv": "^8.0.0" }, - "engines": { - "node": ">= 0.6" + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } } }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "node_modules/ajv/node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-8.4.0.tgz", + "integrity": "sha512-CDSwaxINFy59iNwhYnkvALBwZiTydGkOecZyPkqBpABYR1KqGEsET0VOOYDwtleZSUIdeY36DC2bSZ24CO1igA==", + "license": "MIT", + "dependencies": { + "@fastify/error": "^3.3.0", + "fastq": "^1.17.1" } }, "node_modules/cookie": { @@ -703,53 +605,6 @@ "node": ">= 0.6" } }, - "node_modules/cookie-signature": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "license": "MIT" - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", @@ -762,65 +617,6 @@ "url": "https://dotenvx.com" } }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/esbuild": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", @@ -863,83 +659,132 @@ "@esbuild/win32-x64": "0.27.2" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "node_modules/fast-content-type-parse": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz", + "integrity": "sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==", "license": "MIT" }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" }, - "node_modules/express": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "5.16.1", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-5.16.1.tgz", + "integrity": "sha512-KAdnLvy1yu/XrRtP+LJnxbBGrhN+xXu+gt3EUvZhYGKCr3lFHq/7UFJHHFgmJKoqlh6B40bZLEv7w46B0mqn1g==", "license": "MIT", "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "~1.20.3", - "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.14.0", - "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" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "@fastify/merge-json-schemas": "^0.1.0", + "ajv": "^8.10.0", + "ajv-formats": "^3.0.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^2.1.0", + "json-schema-ref-resolver": "^1.0.1", + "rfdc": "^1.2.0" } }, - "node_modules/finalhandler": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "node_modules/fast-json-stringify/node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "license": "MIT", "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" + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-uri": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.4.0.tgz", + "integrity": "sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA==", + "license": "MIT" + }, + "node_modules/fastify": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.29.1.tgz", + "integrity": "sha512-m2kMNHIG92tSNWv+Z3UeTR9AWLLuo7KctC7mlFPtMEVrfjIhmQhkQnT9v15qA/BfVq3vvj134Y0jl9SBje3jXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^3.5.0", + "@fastify/error": "^3.4.0", + "@fastify/fast-json-stringify-compiler": "^4.3.0", + "abstract-logging": "^2.0.1", + "avvio": "^8.3.0", + "fast-content-type-parse": "^1.1.0", + "fast-json-stringify": "^5.8.0", + "find-my-way": "^8.0.0", + "light-my-request": "^5.11.0", + "pino": "^9.0.0", + "process-warning": "^3.0.0", + "proxy-addr": "^2.0.7", + "rfdc": "^1.3.0", + "secure-json-parse": "^2.7.0", + "semver": "^7.5.4", + "toad-cache": "^3.3.0" + } + }, + "node_modules/fastify-plugin": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", + "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==", + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/find-my-way": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-8.2.2.tgz", + "integrity": "sha512-Dobi7gcTEq8yszimcfp/R7+owiT4WncAJ7VTTgFH1jYJ5GaG1FbhjwDG820hptN0QDFvzVY3RfCzdInvGPGzjA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^3.1.0" }, "engines": { - "node": ">= 0.8" + "node": ">=14" } }, "node_modules/forwarded": { @@ -951,15 +796,6 @@ "node": ">= 0.6" } }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -975,52 +811,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/get-tsconfig": { "version": "4.13.0", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", @@ -1034,80 +824,6 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -1117,151 +833,113 @@ "node": ">= 0.10" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "node_modules/json-schema-ref-resolver": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz", + "integrity": "sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==", "license": "MIT", "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" + "fast-deep-equal": "^3.1.3" } }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-appwrite": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/node-appwrite/-/node-appwrite-14.2.0.tgz", - "integrity": "sha512-sPPA+JzdBJRS+lM6azX85y3/6iyKQYlHcXCbjMuWLROh6IiU9EfXRW3XSUTa5HDoBrlo8ve+AnVA6BIjQfUs1g==", + "node_modules/light-my-request": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.14.0.tgz", + "integrity": "sha512-aORPWntbpH5esaYpGOOmri0OHDOe3wC5M2MQxZ9dvMLZm6DnaAn0kJlcbU9hwsQgLzmZyReKwFwwPkR+nHu5kA==", "license": "BSD-3-Clause", "dependencies": { - "node-fetch-native-with-agent": "1.7.2" + "cookie": "^0.7.0", + "process-warning": "^3.0.0", + "set-cookie-parser": "^2.4.1" } }, - "node_modules/node-fetch-native-with-agent": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/node-fetch-native-with-agent/-/node-fetch-native-with-agent-1.7.2.tgz", - "integrity": "sha512-5MaOOCuJEvcckoz7/tjdx1M6OusOY6Xc5f459IaruGStWnKzlI1qpNgaAwmn4LmFYcsSlj+jBMk84wmmRxfk5g==", - "license": "MIT" - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "node_modules/mnemonist": { + "version": "0.39.6", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.6.tgz", + "integrity": "sha512-A/0v5Z59y63US00cRSLiloEIw3t5G+MiKz4BhX21FI+YBJXBOGW0ohFxTxO08dsOYlzxo87T7vGfZKYp2bcAWA==", "license": "MIT", "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" + "obliterator": "^2.0.1" } }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "node_modules/obliterator": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz", + "integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==", + "license": "MIT" + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">=14.0.0" } }, - "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "node_modules/pino": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", + "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", + "license": "MIT" + }, + "node_modules/pino/node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/process-warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==", "license": "MIT" }, "node_modules/proxy-addr": { @@ -1277,43 +955,28 @@ "node": ">= 0.10" } }, - "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 12.13.0" } }, - "node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "unpipe": "~1.0.0" - }, "engines": { - "node": ">= 0.8" + "node": ">=0.10.0" } }, "node_modules/resolve-pkg-maps": { @@ -1326,171 +989,107 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "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/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/send": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", - "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "node_modules/ret": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.4.3.tgz", + "integrity": "sha512-0f4Memo5QP7WQyUEAYUO3esD/XjOc3Zjjg5CPsAq1p8sIu0XPeMbHJemKA0BO7tV0X7+A0FoEpbmHXWxPyD3wQ==", "license": "MIT", - "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" - }, "engines": { - "node": ">= 0.8.0" + "node": ">=10" } }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/serve-static": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", - "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "~0.19.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/side-channel": { + "node_modules/reusify": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/safe-regex2": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-3.1.0.tgz", + "integrity": "sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==", "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "ret": "~0.4.0" } }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/sonic-boom": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", + "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "atomic-sleep": "^1.0.0" } }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", "license": "MIT", "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "real-require": "^0.2.0" } }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "node_modules/toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", "license": "MIT", "engines": { - "node": ">= 0.8" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" + "node": ">=12" } }, "node_modules/tsx": { @@ -1513,19 +1112,6 @@ "fsevents": "~2.3.3" } }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -1547,33 +1133,6 @@ "dev": true, "license": "MIT" }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/baas-control-plane/package.json b/baas-control-plane/package.json index 990404d..ae7526a 100644 --- a/baas-control-plane/package.json +++ b/baas-control-plane/package.json @@ -1,24 +1,20 @@ { - "name": "core-backend", + "name": "baas-control-plane", "private": true, "version": "1.0.0", "type": "module", "scripts": { "dev": "tsx watch src/main.ts", "build": "tsc", - "start": "node dist/main.js", - "setup:appwrite": "tsx src/scripts/setup-appwrite.ts" + "start": "node dist/main.js" }, "dependencies": { - "cors": "^2.8.5", + "@fastify/cors": "^9.0.1", "dotenv": "^16.4.5", - "express": "^4.19.2", - "node-appwrite": "^14.1.0", + "fastify": "^4.27.0", "zod": "^3.23.8" }, "devDependencies": { - "@types/cors": "^2.8.17", - "@types/express": "^4.17.21", "@types/node": "^20.12.12", "tsx": "^4.15.7", "typescript": "^5.4.5" diff --git a/baas-control-plane/src/config/appwrite.json b/baas-control-plane/src/config/appwrite.json deleted file mode 100644 index b06b777..0000000 --- a/baas-control-plane/src/config/appwrite.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "provider": "appwrite-cloud", - "description": "Appwrite Cloud configuration for DevOps orchestration platform", - "endpoint": "${APPWRITE_ADMIN_ENDPOINT}", - "projectId": "${APPWRITE_ADMIN_PROJECT_ID}", - "apiKey": "${APPWRITE_ADMIN_API_KEY}", - "functions": { - "defaultRuntime": "deno-1.35", - "source": "../../appwrite-functions" - } -} diff --git a/baas-control-plane/src/core/provider.factory.ts b/baas-control-plane/src/core/provider.factory.ts new file mode 100644 index 0000000..2a027ad --- /dev/null +++ b/baas-control-plane/src/core/provider.factory.ts @@ -0,0 +1,19 @@ +import { ProviderInterface } from './provider.interface.js'; +import { ProviderType } from './types.js'; +import { AppwriteProvider } from '../providers/appwrite/appwrite.provisioning.js'; +import { SupabaseProvider } from '../providers/supabase/supabase.provisioning.js'; + +const providerRegistry: Record ProviderInterface> = { + appwrite: () => new AppwriteProvider(), + supabase: () => new SupabaseProvider(), +}; + +export const providerFactory = { + create(type: ProviderType): ProviderInterface { + const providerBuilder = providerRegistry[type]; + if (!providerBuilder) { + throw new Error(`Provider ${type} is not registered`); + } + return providerBuilder(); + }, +}; diff --git a/baas-control-plane/src/core/provider.interface.ts b/baas-control-plane/src/core/provider.interface.ts new file mode 100644 index 0000000..2de4510 --- /dev/null +++ b/baas-control-plane/src/core/provider.interface.ts @@ -0,0 +1,10 @@ +import { ProviderMetrics, ProviderProject, ProviderSecrets, SchemaDefinition } from './types.js'; + +export interface ProviderInterface { + createProject(name: string, secrets: ProviderSecrets): Promise; + deleteProject(externalId: string, secrets: ProviderSecrets): Promise; + applySchema(externalId: string, schema: SchemaDefinition, secrets: ProviderSecrets): Promise; + collectMetrics(externalId: string, secrets: ProviderSecrets): Promise; + rotateSecrets(externalId: string, secrets: ProviderSecrets): Promise; + healthCheck(secrets: ProviderSecrets): Promise; +} diff --git a/baas-control-plane/src/core/types.ts b/baas-control-plane/src/core/types.ts new file mode 100644 index 0000000..d219ae9 --- /dev/null +++ b/baas-control-plane/src/core/types.ts @@ -0,0 +1,59 @@ +export type ProviderType = 'appwrite' | 'supabase'; + +export type TenantStatus = 'active' | 'suspended'; + +export type ProjectStatus = 'draft' | 'provisioning' | 'provisioned' | 'failed'; + +export interface Tenant { + id: string; + name: string; + plan: string; + status: TenantStatus; + createdAt: string; + updatedAt: string; +} + +export interface Project { + id: string; + tenantId: string; + name: string; + provider: ProviderType; + status: ProjectStatus; + externalId?: string; + createdAt: string; + updatedAt: string; +} + +export interface SchemaDefinition { + version: string; + payload: Record; +} + +export interface ProviderProject { + externalId: string; + dashboardUrl?: string; + metadata?: Record; +} + +export interface ProviderMetrics { + users: number; + storageMb: number; + requests: number; + functions: number; + capturedAt: string; +} + +export interface ProviderSecrets { + endpoint: string; + apiKey: string; + projectRef?: string; +} + +export interface AuditEvent { + id: string; + tenantId?: string; + projectId?: string; + action: string; + metadata?: Record; + createdAt: string; +} diff --git a/baas-control-plane/src/docs/BACKEND.md b/baas-control-plane/src/docs/BACKEND.md deleted file mode 100644 index 2282995..0000000 --- a/baas-control-plane/src/docs/BACKEND.md +++ /dev/null @@ -1,30 +0,0 @@ -# Backend multi-tenant Appwrite - -Este backend é a camada administrativa para gerenciar múltiplos projetos Appwrite. - -## O que ele faz - -- CRUD de tenants e projetos Appwrite (multi-tenant). -- Setup e sincronização automática de schema (database, collections, buckets e functions). -- Base para FinOps (métricas de uso) e automações DevOps. - -## Rodando localmente - -```bash -cd backend -npm install -cp .env.example .env -npm run dev -``` - -A API inicia em `http://localhost:4000`. - -## Endpoints mínimos - -- `POST /tenants` -- `GET /tenants` -- `POST /tenants/:id/appwrite-project` -- `GET /tenants/:id/appwrite-projects` -- `POST /appwrite/setup` -- `POST /appwrite/sync-schema` -- `GET /health` diff --git a/baas-control-plane/src/docs/SECURITY.md b/baas-control-plane/src/docs/SECURITY.md deleted file mode 100644 index aa9e6fc..0000000 --- a/baas-control-plane/src/docs/SECURITY.md +++ /dev/null @@ -1,30 +0,0 @@ -# Segurança e Permissões do Appwrite - -As coleções do projeto seguem uma política de Row-Level Security (RLS) para proteger os dados dos usuários e os registros de auditoria. - -## Princípios gerais -- Cada documento pertence a um usuário autenticado, identificado pelo `userId` armazenado no documento. -- Operações de leitura e gravação são restritas ao proprietário do documento (role `member`). -- Logs de auditoria globais só podem ser consultados por usuários com a role `admin`. - -## Regras por coleção - -### `cloud_accounts` -- **Leitura**: apenas o usuário que criou o documento (role `member`). -- **Criação**: apenas usuários autenticados. -- **Atualização/Exclusão**: somente o proprietário do documento. - -### `projects` -- **Leitura**: restrita ao proprietário do documento. -- **Criação**: usuários autenticados podem criar seus próprios registros. -- **Atualização/Exclusão**: apenas o proprietário do documento. - -### `audit_logs` -- **Leitura**: exclusiva para usuários com role `admin` (para auditoria global). -- **Criação**: serviços e funções podem registrar ações em nome dos usuários; cada entrada mantém o `userId` responsável. -- **Atualização/Exclusão**: não permitidas para usuários finais; apenas processos administrativos podem gerenciar retenção. - -## Considerações adicionais -- Tokens e chaves sensíveis (GitHub, Cloudflare) devem ser armazenados em `cloud_accounts` e nunca enviados ao frontend. -- Funções Cloud (como as de proxy) devem validar o `userId` associado ao documento antes de usar qualquer chave. -- Revogue ou rotacione chaves comprometidas removendo ou atualizando o documento correspondente na coleção. diff --git a/baas-control-plane/src/docs/SETUP_GUIDE.md b/baas-control-plane/src/docs/SETUP_GUIDE.md deleted file mode 100644 index 0402741..0000000 --- a/baas-control-plane/src/docs/SETUP_GUIDE.md +++ /dev/null @@ -1,23 +0,0 @@ -# Guia rápido - Setup Appwrite (Multi-tenant) - -O script `setup-appwrite.ts` aplica o schema base (database, collections, buckets e functions) -para um projeto Appwrite específico. - -## Variáveis necessárias - -Edite `backend/.env` e configure: - -```env -APPWRITE_SETUP_ENDPOINT=https://cloud.appwrite.io/v1 -APPWRITE_SETUP_PROJECT_ID= -APPWRITE_SETUP_API_KEY= -``` - -## Executar - -```bash -cd backend -npm run setup:appwrite -``` - -O script é idempotente (pode rodar várias vezes sem duplicar recursos). diff --git a/baas-control-plane/src/docs/appwrite-databases-schema.md b/baas-control-plane/src/docs/appwrite-databases-schema.md deleted file mode 100644 index a4c9e61..0000000 --- a/baas-control-plane/src/docs/appwrite-databases-schema.md +++ /dev/null @@ -1,46 +0,0 @@ -# Appwrite Database Schema (Base Multi-tenant) - -Schema base aplicado pelo backend para novos projetos Appwrite. - -## Database - -- **ID**: `core-platform` -- **Nome**: Core Platform - -## Collections - -### tenants -- `name` (string) -- `slug` (string) -- `status` (enum: active | inactive) -- `createdAt` (datetime) - -### projects -- `tenantId` (string) -- `name` (string) -- `endpoint` (string) -- `projectId` (string) -- `createdAt` (datetime) - -### finops_usage -- `tenantId` (string) -- `users` (integer) -- `documents` (integer) -- `storageBytes` (integer) -- `functionRuns` (integer) -- `capturedAt` (datetime) - -### audit_logs -- `event` (string) -- `actor` (string) -- `timestamp` (datetime) - -## Buckets - -- `tenant-assets` (storage base por tenant) - -## Functions - -- `hello-world` -- `sync-github` -- `check-cloudflare-status` diff --git a/baas-control-plane/src/lib/appwrite.ts b/baas-control-plane/src/lib/appwrite.ts deleted file mode 100644 index 4758920..0000000 --- a/baas-control-plane/src/lib/appwrite.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { - Client, - Databases, - Functions, - Storage, - ID, - Runtime, -} from 'node-appwrite'; -import { appwriteSchema } from '../modules/projects/appwriteSchema.js'; -import { logger } from './logger.js'; - -export type AppwriteConnection = { - endpoint: string; - projectId: string; - apiKey: string; -}; - -export type ApplySchemaResult = { - databaseId: string; - collections: string[]; - buckets: string[]; - functions: string[]; -}; - -export const createAppwriteClient = ({ endpoint, projectId, apiKey }: AppwriteConnection) => - new Client().setEndpoint(endpoint).setProject(projectId).setKey(apiKey); - -const isConflict = (error: unknown) => - typeof error === 'object' && error !== null && 'code' in error && (error as { code?: number }).code === 409; - -export const applySchema = async ( - connection: AppwriteConnection, - options?: { logPrefix?: string } -): Promise => { - const client = createAppwriteClient(connection); - const databases = new Databases(client); - const storage = new Storage(client); - const functions = new Functions(client); - const logPrefix = options?.logPrefix ?? 'appwrite'; - - logger.info(`[${logPrefix}] Applying Appwrite schema`, { - endpoint: connection.endpoint, - projectId: connection.projectId, - }); - - const { database } = appwriteSchema; - - try { - await databases.create(database.id, database.name, database.enabled); - logger.info(`[${logPrefix}] Database created`, { databaseId: database.id }); - } catch (error) { - if (!isConflict(error)) { - throw error; - } - logger.info(`[${logPrefix}] Database already exists`, { databaseId: database.id }); - } - - for (const collection of appwriteSchema.collections) { - try { - await databases.createCollection(database.id, collection.id, collection.name, collection.permissions); - logger.info(`[${logPrefix}] Collection created`, { collectionId: collection.id }); - } catch (error) { - if (!isConflict(error)) { - throw error; - } - logger.info(`[${logPrefix}] Collection already exists`, { collectionId: collection.id }); - } - - for (const attribute of collection.attributes) { - try { - switch (attribute.type) { - case 'string': - await databases.createStringAttribute( - database.id, - collection.id, - attribute.key, - attribute.size, - attribute.required, - attribute.array ?? false - ); - break; - case 'enum': - await databases.createEnumAttribute( - database.id, - collection.id, - attribute.key, - attribute.elements, - attribute.required - ); - break; - case 'integer': - await databases.createIntegerAttribute( - database.id, - collection.id, - attribute.key, - attribute.required - ); - break; - case 'datetime': - await databases.createDatetimeAttribute( - database.id, - collection.id, - attribute.key, - attribute.required - ); - break; - case 'boolean': - await databases.createBooleanAttribute( - database.id, - collection.id, - attribute.key, - attribute.required - ); - break; - case 'url': - await databases.createUrlAttribute( - database.id, - collection.id, - attribute.key, - attribute.required - ); - break; - default: - break; - } - logger.info(`[${logPrefix}] Attribute ensured`, { - collectionId: collection.id, - attribute: attribute.key, - }); - } catch (error) { - if (!isConflict(error)) { - throw error; - } - } - } - } - - for (const bucket of appwriteSchema.buckets) { - try { - await storage.createBucket(bucket.id, bucket.name, bucket.permissions, bucket.fileSecurity ?? false); - logger.info(`[${logPrefix}] Bucket created`, { bucketId: bucket.id }); - } catch (error) { - if (!isConflict(error)) { - throw error; - } - logger.info(`[${logPrefix}] Bucket already exists`, { bucketId: bucket.id }); - } - } - - for (const func of appwriteSchema.functions) { - try { - await functions.get(func.id); - logger.info(`[${logPrefix}] Function already exists`, { functionId: func.id }); - } catch (error) { - const code = typeof error === 'object' && error !== null && 'code' in error ? (error as { code?: number }).code : null; - if (code !== 404 && code !== 409) { - throw error; - } - await functions.create(func.id, func.name, func.runtime as Runtime, func.execute); - logger.info(`[${logPrefix}] Function created`, { functionId: func.id }); - } - } - - return { - databaseId: database.id, - collections: appwriteSchema.collections.map((collection) => collection.id), - buckets: appwriteSchema.buckets.map((bucket) => bucket.id), - functions: appwriteSchema.functions.map((func) => func.id), - }; -}; - -export const createProject = async ( - connection: AppwriteConnection, - projectName: string -): Promise<{ projectId: string }> => { - const client = createAppwriteClient(connection); - const { Projects } = await import('node-appwrite'); - const projects = new Projects(client); - const projectId = ID.unique(); - - await projects.create(projectId, projectName); - - return { projectId }; -}; diff --git a/baas-control-plane/src/lib/env.ts b/baas-control-plane/src/lib/env.ts index f9a0bbe..ae166fe 100644 --- a/baas-control-plane/src/lib/env.ts +++ b/baas-control-plane/src/lib/env.ts @@ -1,30 +1,22 @@ import dotenv from 'dotenv'; -import path from 'path'; +import { z } from 'zod'; -const envFile = process.env.ENV_FILE || path.resolve(process.cwd(), '.env'); +dotenv.config(); -dotenv.config({ path: envFile }); +const envSchema = z.object({ + PORT: z.string().default('4000'), + APPWRITE_ENDPOINT: z.string().default('https://cloud.appwrite.io'), + APPWRITE_API_KEY: z.string().default('appwrite-api-key'), + SUPABASE_ENDPOINT: z.string().default('https://api.supabase.com'), + SUPABASE_SERVICE_KEY: z.string().default('supabase-service-key'), +}); -const numberFromEnv = (value: string | undefined, fallback: number) => { - if (!value) { - return fallback; - } - const parsed = Number.parseInt(value, 10); - return Number.isNaN(parsed) ? fallback : parsed; -}; +const parsed = envSchema.parse(process.env); export const env = { - nodeEnv: process.env.NODE_ENV ?? 'development', - port: numberFromEnv(process.env.BACKEND_PORT, 4000), - adminToken: process.env.ADMIN_API_TOKEN ?? '', - dataDir: process.env.DATA_DIR ?? 'data', - appwriteAdmin: { - endpoint: process.env.APPWRITE_ADMIN_ENDPOINT ?? '', - projectId: process.env.APPWRITE_ADMIN_PROJECT_ID ?? '', - apiKey: process.env.APPWRITE_ADMIN_API_KEY ?? '', - }, - defaults: { - appwriteEndpoint: process.env.DEFAULT_APPWRITE_ENDPOINT ?? process.env.APPWRITE_DEFAULT_ENDPOINT ?? '', - functionRuntime: process.env.APPWRITE_DEFAULT_RUNTIME ?? 'deno-1.35', - }, + port: Number(parsed.PORT), + appwriteEndpoint: parsed.APPWRITE_ENDPOINT, + appwriteApiKey: parsed.APPWRITE_API_KEY, + supabaseEndpoint: parsed.SUPABASE_ENDPOINT, + supabaseServiceKey: parsed.SUPABASE_SERVICE_KEY, }; diff --git a/baas-control-plane/src/lib/http.ts b/baas-control-plane/src/lib/http.ts new file mode 100644 index 0000000..c5764f4 --- /dev/null +++ b/baas-control-plane/src/lib/http.ts @@ -0,0 +1,24 @@ +export const http = { + async get(url: string, options?: RequestInit): Promise { + const response = await fetch(url, { ...options, method: 'GET' }); + if (!response.ok) { + throw new Error(`HTTP GET failed with status ${response.status}`); + } + return response.json() as Promise; + }, + async post(url: string, body?: unknown, options?: RequestInit): Promise { + const response = await fetch(url, { + ...options, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(options?.headers ?? {}), + }, + body: body ? JSON.stringify(body) : undefined, + }); + if (!response.ok) { + throw new Error(`HTTP POST failed with status ${response.status}`); + } + return response.json() as Promise; + }, +}; diff --git a/baas-control-plane/src/lib/logger.ts b/baas-control-plane/src/lib/logger.ts index c83763b..7aff104 100644 --- a/baas-control-plane/src/lib/logger.ts +++ b/baas-control-plane/src/lib/logger.ts @@ -1,15 +1,34 @@ -export type LogMeta = Record | undefined; +type LogPayload = Record; -const formatMeta = (meta?: LogMeta) => (meta ? ` ${JSON.stringify(meta)}` : ''); +const log = (level: 'info' | 'error' | 'warn', message: string, payload?: LogPayload) => { + const entry = { + level, + message, + timestamp: new Date().toISOString(), + ...payload, + }; + + if (level === 'error') { + console.error(entry); + return; + } + + if (level === 'warn') { + console.warn(entry); + return; + } + + console.log(entry); +}; export const logger = { - info(message: string, meta?: LogMeta) { - console.log(`[INFO] ${message}${formatMeta(meta)}`); + info(message: string, payload?: LogPayload) { + log('info', message, payload); }, - warn(message: string, meta?: LogMeta) { - console.warn(`[WARN] ${message}${formatMeta(meta)}`); + warn(message: string, payload?: LogPayload) { + log('warn', message, payload); }, - error(message: string, meta?: LogMeta) { - console.error(`[ERROR] ${message}${formatMeta(meta)}`); + error(message: string, payload?: LogPayload) { + log('error', message, payload); }, }; diff --git a/baas-control-plane/src/lib/storage.ts b/baas-control-plane/src/lib/storage.ts new file mode 100644 index 0000000..54e233e --- /dev/null +++ b/baas-control-plane/src/lib/storage.ts @@ -0,0 +1,29 @@ +import { promises as fs } from 'fs'; +import path from 'path'; + +const dataDir = path.resolve('data'); + +const ensureDir = async () => { + await fs.mkdir(dataDir, { recursive: true }); +}; + +const filePath = (file: string) => path.join(dataDir, file); + +export const storage = { + async readCollection(file: string): Promise { + await ensureDir(); + try { + const content = await fs.readFile(filePath(file), 'utf-8'); + return JSON.parse(content) as T[]; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + return []; + } + throw error; + } + }, + async writeCollection(file: string, data: T[]): Promise { + await ensureDir(); + await fs.writeFile(filePath(file), JSON.stringify(data, null, 2)); + }, +}; diff --git a/baas-control-plane/src/main.ts b/baas-control-plane/src/main.ts index 26ccd0e..728dc2d 100644 --- a/baas-control-plane/src/main.ts +++ b/baas-control-plane/src/main.ts @@ -1,34 +1,57 @@ -import express from 'express'; -import cors from 'cors'; +import Fastify from 'fastify'; +import cors from '@fastify/cors'; import { env } from './lib/env.js'; import { logger } from './lib/logger.js'; -import { requireAdminToken } from './modules/auth/auth.middleware.js'; -import { tenantsRouter } from './modules/tenants/tenants.routes.js'; -import { appwriteRouter } from './modules/projects/appwrite.routes.js'; +import { TenantsService } from './modules/tenants/tenants.service.js'; +import { ProjectsService } from './modules/projects/projects.service.js'; +import { ProvisioningService } from './modules/provisioning/provisioning.service.js'; +import { SchemaService } from './modules/schema/schema.service.js'; +import { SchemaVersioning } from './modules/schema/schema.versioning.js'; +import { SecretsService } from './modules/secrets/secrets.service.js'; +import { AuditService } from './modules/audit/audit.service.js'; +import { FinopsCollector } from './modules/finops/finops.collector.js'; +import { registerTenantsController } from './modules/tenants/tenants.controller.js'; +import { registerProjectsController } from './modules/projects/projects.controller.js'; +import { providerFactory } from './core/provider.factory.js'; -const app = express(); +const app = Fastify({ logger: false }); -app.use(cors()); -app.use(express.json({ limit: '1mb' })); +await app.register(cors, { origin: true }); -app.get('/health', (_request, response) => { - response.json({ status: 'ok' }); +const tenantsService = new TenantsService(); +const projectsService = new ProjectsService(); +const secretsService = new SecretsService(); +const auditService = new AuditService(); +const provisioningService = new ProvisioningService(projectsService, secretsService); +const schemaService = new SchemaService(projectsService, secretsService, new SchemaVersioning()); +const finopsCollector = new FinopsCollector(projectsService, secretsService); + +app.get('/health', async () => { + const appwrite = providerFactory.create('appwrite'); + const supabase = providerFactory.create('supabase'); + + const [appwriteHealthy, supabaseHealthy] = await Promise.all([ + appwrite.healthCheck(await secretsService.getProviderSecrets('appwrite', 'system')), + supabase.healthCheck(await secretsService.getProviderSecrets('supabase', 'system')), + ]); + + return { + status: 'ok', + providers: { + appwrite: appwriteHealthy, + supabase: supabaseHealthy, + }, + }; }); -app.use(requireAdminToken); +registerTenantsController(app, tenantsService, auditService); +registerProjectsController(app, projectsService, provisioningService, schemaService, auditService, finopsCollector); -app.use('/tenants', tenantsRouter); -app.use('/appwrite', appwriteRouter); - -app.use((error: Error, _request: express.Request, response: express.Response, _next: express.NextFunction) => { +app.setErrorHandler((error, _request, reply) => { logger.error('Request failed', { message: error.message }); - - const status = error.message.includes('not found') ? 404 : 400; - response.status(status).json({ - error: error.message, - }); + reply.status(400).send({ error: error.message }); }); -app.listen(env.port, () => { - logger.info(`Backend listening on http://localhost:${env.port}`); +app.listen({ port: env.port, host: '0.0.0.0' }).then(() => { + logger.info(`baas-control-plane listening on http://localhost:${env.port}`); }); diff --git a/baas-control-plane/src/modules/audit/audit.service.ts b/baas-control-plane/src/modules/audit/audit.service.ts new file mode 100644 index 0000000..5484893 --- /dev/null +++ b/baas-control-plane/src/modules/audit/audit.service.ts @@ -0,0 +1,18 @@ +import { storage } from '../../lib/storage.js'; +import { AuditEvent } from '../../core/types.js'; + +const AUDIT_FILE = 'audit-events.json'; + +export class AuditService { + async record(event: Omit): Promise { + const events = await storage.readCollection(AUDIT_FILE); + const entry: AuditEvent = { + id: crypto.randomUUID(), + createdAt: new Date().toISOString(), + ...event, + }; + events.push(entry); + await storage.writeCollection(AUDIT_FILE, events); + return entry; + } +} diff --git a/baas-control-plane/src/modules/auth/auth.middleware.ts b/baas-control-plane/src/modules/auth/auth.middleware.ts deleted file mode 100644 index 6dec529..0000000 --- a/baas-control-plane/src/modules/auth/auth.middleware.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { NextFunction, Request, Response } from 'express'; -import { env } from '../../lib/env.js'; - -export const requireAdminToken = (request: Request, response: Response, next: NextFunction) => { - if (!env.adminToken) { - return next(); - } - - const header = request.headers.authorization; - const fallbackToken = request.headers['x-admin-token']; - const tokenValue = Array.isArray(fallbackToken) ? fallbackToken[0] : fallbackToken; - const token = header?.startsWith('Bearer ') ? header.slice(7) : tokenValue; - - if (token !== env.adminToken) { - return response.status(401).json({ - error: 'Unauthorized', - message: 'Invalid admin token', - }); - } - - return next(); -}; diff --git a/baas-control-plane/src/modules/finops/finops.collector.ts b/baas-control-plane/src/modules/finops/finops.collector.ts new file mode 100644 index 0000000..7a93f1c --- /dev/null +++ b/baas-control-plane/src/modules/finops/finops.collector.ts @@ -0,0 +1,22 @@ +import { providerFactory } from '../../core/provider.factory.js'; +import { ProjectsService } from '../projects/projects.service.js'; +import { SecretsService } from '../secrets/secrets.service.js'; +import { ProviderMetrics } from '../../core/types.js'; + +export class FinopsCollector { + constructor( + private readonly projectsService: ProjectsService, + private readonly secretsService: SecretsService, + ) {} + + async collectForProject(projectId: string): Promise { + const project = await this.projectsService.getProject(projectId); + if (!project || !project.externalId) { + throw new Error('Project not provisioned'); + } + + const provider = providerFactory.create(project.provider); + const secrets = await this.secretsService.getProviderSecrets(project.provider, project.tenantId); + return provider.collectMetrics(project.externalId, secrets); + } +} diff --git a/baas-control-plane/src/modules/finops/finops.service.ts b/baas-control-plane/src/modules/finops/finops.service.ts deleted file mode 100644 index df3f7da..0000000 --- a/baas-control-plane/src/modules/finops/finops.service.ts +++ /dev/null @@ -1,24 +0,0 @@ -export type FinopsSnapshot = { - users: number; - documents: number; - storageBytes: number; - functionRuns: number; - capturedAt: string; -}; - -export type FinopsSummary = FinopsSnapshot & { - lastSyncedAt?: string; -}; - -export const createEmptyFinops = (): FinopsSummary => ({ - users: 0, - documents: 0, - storageBytes: 0, - functionRuns: 0, - capturedAt: new Date().toISOString(), -}); - -export const recordSnapshot = (summary: FinopsSummary, snapshot: FinopsSnapshot): FinopsSummary => ({ - ...snapshot, - lastSyncedAt: new Date().toISOString(), -}); diff --git a/baas-control-plane/src/modules/projects/appwrite.routes.ts b/baas-control-plane/src/modules/projects/appwrite.routes.ts deleted file mode 100644 index 29f1716..0000000 --- a/baas-control-plane/src/modules/projects/appwrite.routes.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Router } from 'express'; -import { z } from 'zod'; -import { syncSchemaForProject } from './appwrite.service.js'; - -const schemaRequest = z.object({ - tenantId: z.string().min(1), - projectRef: z.string().optional(), -}); - -export const appwriteRouter = Router(); - -appwriteRouter.post('/setup', async (request, response, next) => { - try { - const payload = schemaRequest.parse(request.body); - const result = await syncSchemaForProject(payload.tenantId, payload.projectRef); - response.status(200).json({ - message: 'Appwrite setup applied', - result, - }); - } catch (error) { - next(error); - } -}); - -appwriteRouter.post('/sync-schema', async (request, response, next) => { - try { - const payload = schemaRequest.parse(request.body); - const result = await syncSchemaForProject(payload.tenantId, payload.projectRef); - response.status(200).json({ - message: 'Schema synchronized', - result, - }); - } catch (error) { - next(error); - } -}); diff --git a/baas-control-plane/src/modules/projects/appwrite.service.ts b/baas-control-plane/src/modules/projects/appwrite.service.ts deleted file mode 100644 index 54d95d8..0000000 --- a/baas-control-plane/src/modules/projects/appwrite.service.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { applySchema } from '../../lib/appwrite.js'; -import { loadTenants, saveTenants } from '../tenants/tenants.store.js'; -import type { AppwriteProject } from '../tenants/tenants.types.js'; -import { logger } from '../../lib/logger.js'; - -export const resolveProject = async (tenantId: string, projectRef?: string): Promise => { - const tenants = await loadTenants(); - const tenant = tenants.find((item) => item.id === tenantId); - - if (!tenant) { - throw new Error('Tenant not found'); - } - - if (!projectRef) { - if (tenant.appwriteProjects.length === 1) { - return tenant.appwriteProjects[0]; - } - throw new Error('Multiple projects found. Provide projectRef.'); - } - - const project = tenant.appwriteProjects.find( - (item) => item.id === projectRef || item.projectId === projectRef - ); - - if (!project) { - throw new Error('Project not found'); - } - - return project; -}; - -export const syncSchemaForProject = async (tenantId: string, projectRef?: string) => { - const project = await resolveProject(tenantId, projectRef); - - const result = await applySchema({ - endpoint: project.endpoint, - projectId: project.projectId, - apiKey: project.apiKey, - }); - - project.updatedAt = new Date().toISOString(); - - const tenants = await loadTenants(); - const tenant = tenants.find((item) => item.id === tenantId); - if (tenant) { - tenant.updatedAt = project.updatedAt; - await saveTenants(tenants); - } - - logger.info('Schema synced for project', { - tenantId, - projectId: project.projectId, - }); - - return result; -}; diff --git a/baas-control-plane/src/modules/projects/appwriteSchema.ts b/baas-control-plane/src/modules/projects/appwriteSchema.ts deleted file mode 100644 index ee4b1f9..0000000 --- a/baas-control-plane/src/modules/projects/appwriteSchema.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { Permission, Role, Runtime } from 'node-appwrite'; -import { env } from '../../lib/env.js'; - -export type AttributeDefinition = - | { - type: 'string'; - key: string; - size: number; - required: boolean; - array?: boolean; - } - | { - type: 'enum'; - key: string; - elements: string[]; - required: boolean; - } - | { - type: 'integer'; - key: string; - required: boolean; - } - | { - type: 'datetime'; - key: string; - required: boolean; - } - | { - type: 'boolean'; - key: string; - required: boolean; - } - | { - type: 'url'; - key: string; - required: boolean; - }; - -export type CollectionSchema = { - id: string; - name: string; - permissions: string[]; - attributes: AttributeDefinition[]; -}; - -export type BucketSchema = { - id: string; - name: string; - permissions: string[]; - fileSecurity?: boolean; -}; - -export type FunctionSchema = { - id: string; - name: string; - runtime: Runtime; - execute: string[]; -}; - -export type AppwriteSchema = { - database: { - id: string; - name: string; - enabled: boolean; - }; - collections: CollectionSchema[]; - buckets: BucketSchema[]; - functions: FunctionSchema[]; -}; - -const runtime = (Object.values(Runtime) as string[]).includes(env.defaults.functionRuntime) - ? (env.defaults.functionRuntime as Runtime) - : Runtime.Deno135; - -export const appwriteSchema: AppwriteSchema = { - database: { - id: 'core-platform', - name: 'Core Platform', - enabled: true, - }, - collections: [ - { - id: 'tenants', - name: 'Tenants', - permissions: [ - Permission.read(Role.any()), - Permission.create(Role.team('admins')), - Permission.update(Role.team('admins')), - Permission.delete(Role.team('admins')), - ], - attributes: [ - { type: 'string', key: 'name', size: 255, required: true }, - { type: 'string', key: 'slug', size: 120, required: true }, - { type: 'enum', key: 'status', elements: ['active', 'inactive'], required: true }, - { type: 'datetime', key: 'createdAt', required: true }, - ], - }, - { - id: 'projects', - name: 'Projects', - permissions: [ - Permission.read(Role.team('admins')), - Permission.create(Role.team('admins')), - Permission.update(Role.team('admins')), - Permission.delete(Role.team('admins')), - ], - attributes: [ - { type: 'string', key: 'tenantId', size: 80, required: true }, - { type: 'string', key: 'name', size: 255, required: true }, - { type: 'string', key: 'endpoint', size: 255, required: true }, - { type: 'string', key: 'projectId', size: 255, required: true }, - { type: 'datetime', key: 'createdAt', required: true }, - ], - }, - { - id: 'finops_usage', - name: 'FinOps Usage', - permissions: [ - Permission.read(Role.team('admins')), - Permission.create(Role.team('admins')), - Permission.update(Role.team('admins')), - ], - attributes: [ - { type: 'string', key: 'tenantId', size: 80, required: true }, - { type: 'integer', key: 'users', required: true }, - { type: 'integer', key: 'documents', required: true }, - { type: 'integer', key: 'storageBytes', required: true }, - { type: 'integer', key: 'functionRuns', required: true }, - { type: 'datetime', key: 'capturedAt', required: true }, - ], - }, - { - id: 'audit_logs', - name: 'Audit Logs', - permissions: [ - Permission.read(Role.team('admins')), - Permission.create(Role.team('admins')), - ], - attributes: [ - { type: 'string', key: 'event', size: 500, required: true }, - { type: 'string', key: 'actor', size: 255, required: true }, - { type: 'datetime', key: 'timestamp', required: true }, - ], - }, - ], - buckets: [ - { - id: 'tenant-assets', - name: 'Tenant Assets', - permissions: [ - Permission.read(Role.team('admins')), - Permission.create(Role.team('admins')), - Permission.update(Role.team('admins')), - Permission.delete(Role.team('admins')), - ], - fileSecurity: true, - }, - ], - functions: [ - { - id: 'hello-world', - name: 'Hello World', - runtime, - execute: [Role.any()], - }, - { - id: 'sync-github', - name: 'Sync GitHub', - runtime, - execute: [Role.team('admins')], - }, - { - id: 'check-cloudflare-status', - name: 'Check Cloudflare Status', - runtime, - execute: [Role.team('admins')], - }, - ], -}; diff --git a/baas-control-plane/src/modules/projects/projects.controller.ts b/baas-control-plane/src/modules/projects/projects.controller.ts new file mode 100644 index 0000000..c027908 --- /dev/null +++ b/baas-control-plane/src/modules/projects/projects.controller.ts @@ -0,0 +1,75 @@ +import { FastifyInstance } from 'fastify'; +import { z } from 'zod'; +import { ProjectsService } from './projects.service.js'; +import { ProvisioningService } from '../provisioning/provisioning.service.js'; +import { SchemaService } from '../schema/schema.service.js'; +import { AuditService } from '../audit/audit.service.js'; +import { FinopsCollector } from '../finops/finops.collector.js'; + +const projectSchema = z.object({ + name: z.string().min(2), + provider: z.enum(['appwrite', 'supabase']), +}); + +const schemaSyncPayload = z.object({ + version: z.string().min(1), + payload: z.record(z.unknown()), +}); + +export const registerProjectsController = ( + app: FastifyInstance, + projectsService: ProjectsService, + provisioningService: ProvisioningService, + schemaService: SchemaService, + auditService: AuditService, + finopsCollector: FinopsCollector, +) => { + app.get('/tenants/:tenantId/projects', async (request) => { + const { tenantId } = request.params as { tenantId: string }; + return projectsService.listProjectsForTenant(tenantId); + }); + + app.post('/tenants/:tenantId/projects', async (request, reply) => { + const { tenantId } = request.params as { tenantId: string }; + const payload = projectSchema.parse(request.body); + const project = await projectsService.createProject(tenantId, payload); + await auditService.record({ + tenantId, + projectId: project.id, + action: 'project.created', + metadata: { provider: project.provider }, + }); + reply.code(201); + return project; + }); + + app.post('/projects/:projectId/provision', async (request) => { + const { projectId } = request.params as { projectId: string }; + const result = await provisioningService.provisionProject(projectId); + await auditService.record({ + projectId, + tenantId: result.project.tenantId, + action: 'project.provisioned', + metadata: { provider: result.project.provider, externalId: result.project.externalId }, + }); + return result; + }); + + app.post('/projects/:projectId/schema/sync', async (request) => { + const { projectId } = request.params as { projectId: string }; + const payload = schemaSyncPayload.parse(request.body); + const result = await schemaService.syncSchema(projectId, payload); + await auditService.record({ + projectId, + tenantId: result.project.tenantId, + action: 'schema.applied', + metadata: { version: payload.version }, + }); + return result; + }); + + app.get('/projects/:projectId/metrics', async (request) => { + const { projectId } = request.params as { projectId: string }; + return finopsCollector.collectForProject(projectId); + }); +}; diff --git a/baas-control-plane/src/modules/projects/projects.entity.ts b/baas-control-plane/src/modules/projects/projects.entity.ts new file mode 100644 index 0000000..3f6d399 --- /dev/null +++ b/baas-control-plane/src/modules/projects/projects.entity.ts @@ -0,0 +1,3 @@ +import { Project } from '../../core/types.js'; + +export type ProjectEntity = Project; diff --git a/baas-control-plane/src/modules/projects/projects.service.ts b/baas-control-plane/src/modules/projects/projects.service.ts new file mode 100644 index 0000000..0e8260d --- /dev/null +++ b/baas-control-plane/src/modules/projects/projects.service.ts @@ -0,0 +1,49 @@ +import { storage } from '../../lib/storage.js'; +import { Project, ProviderType } from '../../core/types.js'; + +const PROJECTS_FILE = 'projects.json'; + +export class ProjectsService { + async listProjectsForTenant(tenantId: string): Promise { + const projects = await storage.readCollection(PROJECTS_FILE); + return projects.filter((project) => project.tenantId === tenantId); + } + + async getProject(projectId: string): Promise { + const projects = await storage.readCollection(PROJECTS_FILE); + return projects.find((project) => project.id === projectId); + } + + async createProject(tenantId: string, input: { name: string; provider: ProviderType }): Promise { + const projects = await storage.readCollection(PROJECTS_FILE); + const now = new Date().toISOString(); + const project: Project = { + id: crypto.randomUUID(), + tenantId, + name: input.name, + provider: input.provider, + status: 'draft', + createdAt: now, + updatedAt: now, + }; + projects.push(project); + await storage.writeCollection(PROJECTS_FILE, projects); + return project; + } + + async updateProject(projectId: string, changes: Partial): Promise { + const projects = await storage.readCollection(PROJECTS_FILE); + const index = projects.findIndex((project) => project.id === projectId); + if (index === -1) { + throw new Error('Project not found'); + } + const updated = { + ...projects[index], + ...changes, + updatedAt: new Date().toISOString(), + }; + projects[index] = updated; + await storage.writeCollection(PROJECTS_FILE, projects); + return updated; + } +} diff --git a/baas-control-plane/src/modules/provisioning/provisioning.service.ts b/baas-control-plane/src/modules/provisioning/provisioning.service.ts new file mode 100644 index 0000000..691ac87 --- /dev/null +++ b/baas-control-plane/src/modules/provisioning/provisioning.service.ts @@ -0,0 +1,29 @@ +import { ProjectsService } from '../projects/projects.service.js'; +import { providerFactory } from '../../core/provider.factory.js'; +import { SecretsService } from '../secrets/secrets.service.js'; +import { Project } from '../../core/types.js'; + +export class ProvisioningService { + constructor( + private readonly projectsService: ProjectsService, + private readonly secretsService: SecretsService, + ) {} + + async provisionProject(projectId: string): Promise<{ project: Project }> { + const project = await this.projectsService.getProject(projectId); + if (!project) { + throw new Error('Project not found'); + } + + const provider = providerFactory.create(project.provider); + const secrets = await this.secretsService.getProviderSecrets(project.provider, project.tenantId); + + const created = await provider.createProject(project.name, secrets); + const updated = await this.projectsService.updateProject(projectId, { + status: 'provisioned', + externalId: created.externalId, + }); + + return { project: updated }; + } +} diff --git a/baas-control-plane/src/modules/schema/schema.service.ts b/baas-control-plane/src/modules/schema/schema.service.ts new file mode 100644 index 0000000..eb25af3 --- /dev/null +++ b/baas-control-plane/src/modules/schema/schema.service.ts @@ -0,0 +1,27 @@ +import { SchemaDefinition } from '../../core/types.js'; +import { ProjectsService } from '../projects/projects.service.js'; +import { providerFactory } from '../../core/provider.factory.js'; +import { SecretsService } from '../secrets/secrets.service.js'; +import { SchemaVersioning } from './schema.versioning.js'; + +export class SchemaService { + constructor( + private readonly projectsService: ProjectsService, + private readonly secretsService: SecretsService, + private readonly versioning: SchemaVersioning, + ) {} + + async syncSchema(projectId: string, schema: SchemaDefinition): Promise<{ project: { id: string; tenantId: string } }> { + const project = await this.projectsService.getProject(projectId); + if (!project || !project.externalId) { + throw new Error('Project not provisioned'); + } + + const provider = providerFactory.create(project.provider); + const secrets = await this.secretsService.getProviderSecrets(project.provider, project.tenantId); + await provider.applySchema(project.externalId, schema, secrets); + await this.versioning.addVersion(projectId, schema); + + return { project: { id: project.id, tenantId: project.tenantId } }; + } +} diff --git a/baas-control-plane/src/modules/schema/schema.versioning.ts b/baas-control-plane/src/modules/schema/schema.versioning.ts new file mode 100644 index 0000000..f8de273 --- /dev/null +++ b/baas-control-plane/src/modules/schema/schema.versioning.ts @@ -0,0 +1,28 @@ +import { storage } from '../../lib/storage.js'; +import { SchemaDefinition } from '../../core/types.js'; + +const SCHEMA_FILE = 'schema-versions.json'; + +interface SchemaVersionRecord { + projectId: string; + versions: SchemaDefinition[]; +} + +export class SchemaVersioning { + async listVersions(projectId: string): Promise { + const records = await storage.readCollection(SCHEMA_FILE); + const record = records.find((item) => item.projectId === projectId); + return record?.versions ?? []; + } + + async addVersion(projectId: string, schema: SchemaDefinition): Promise { + const records = await storage.readCollection(SCHEMA_FILE); + const existing = records.find((item) => item.projectId === projectId); + if (existing) { + existing.versions.push(schema); + } else { + records.push({ projectId, versions: [schema] }); + } + await storage.writeCollection(SCHEMA_FILE, records); + } +} diff --git a/baas-control-plane/src/modules/secrets/secrets.service.ts b/baas-control-plane/src/modules/secrets/secrets.service.ts new file mode 100644 index 0000000..b8a72b6 --- /dev/null +++ b/baas-control-plane/src/modules/secrets/secrets.service.ts @@ -0,0 +1,52 @@ +import { env } from '../../lib/env.js'; +import { ProviderSecrets, ProviderType } from '../../core/types.js'; +import { storage } from '../../lib/storage.js'; + +const SECRETS_FILE = 'provider-secrets.json'; + +interface SecretsRecord { + tenantId: string; + provider: ProviderType; + secrets: ProviderSecrets; +} + +export class SecretsService { + async getProviderSecrets(provider: ProviderType, tenantId: string): Promise { + const records = await storage.readCollection(SECRETS_FILE); + const record = records.find((item) => item.tenantId === tenantId && item.provider === provider); + + if (record) { + return record.secrets; + } + + const defaults: Record = { + appwrite: { + endpoint: env.appwriteEndpoint, + apiKey: env.appwriteApiKey, + }, + supabase: { + endpoint: env.supabaseEndpoint, + apiKey: env.supabaseServiceKey, + }, + }; + + return defaults[provider]; + } + + async rotateProviderSecrets( + provider: ProviderType, + tenantId: string, + secrets: ProviderSecrets, + ): Promise { + const records = await storage.readCollection(SECRETS_FILE); + const existing = records.find((item) => item.tenantId === tenantId && item.provider === provider); + + if (existing) { + existing.secrets = secrets; + } else { + records.push({ tenantId, provider, secrets }); + } + + await storage.writeCollection(SECRETS_FILE, records); + } +} diff --git a/baas-control-plane/src/modules/tenants/tenants.controller.ts b/baas-control-plane/src/modules/tenants/tenants.controller.ts new file mode 100644 index 0000000..b71ec39 --- /dev/null +++ b/baas-control-plane/src/modules/tenants/tenants.controller.ts @@ -0,0 +1,30 @@ +import { FastifyInstance } from 'fastify'; +import { z } from 'zod'; +import { TenantsService } from './tenants.service.js'; +import { AuditService } from '../audit/audit.service.js'; + +const tenantSchema = z.object({ + name: z.string().min(2), + plan: z.string().optional(), + status: z.enum(['active', 'suspended']).optional(), +}); + +export const registerTenantsController = ( + app: FastifyInstance, + tenantsService: TenantsService, + auditService: AuditService, +) => { + app.get('/tenants', async () => tenantsService.listTenants()); + + app.post('/tenants', async (request, reply) => { + const payload = tenantSchema.parse(request.body); + const tenant = await tenantsService.createTenant(payload); + await auditService.record({ + tenantId: tenant.id, + action: 'tenant.created', + metadata: { name: tenant.name, plan: tenant.plan }, + }); + reply.code(201); + return tenant; + }); +}; diff --git a/baas-control-plane/src/modules/tenants/tenants.entity.ts b/baas-control-plane/src/modules/tenants/tenants.entity.ts new file mode 100644 index 0000000..6d1365f --- /dev/null +++ b/baas-control-plane/src/modules/tenants/tenants.entity.ts @@ -0,0 +1,3 @@ +import { Tenant } from '../../core/types.js'; + +export type TenantEntity = Tenant; diff --git a/baas-control-plane/src/modules/tenants/tenants.routes.ts b/baas-control-plane/src/modules/tenants/tenants.routes.ts deleted file mode 100644 index 7af4b11..0000000 --- a/baas-control-plane/src/modules/tenants/tenants.routes.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Router } from 'express'; -import { z } from 'zod'; -import { - addAppwriteProject, - createTenant, - listTenantProjects, - listTenants, - redactProject, - removeTenantProject, - updateTenantProject, -} from './tenants.service.js'; - -const tenantSchema = z.object({ - name: z.string().min(2), - slug: z.string().optional(), -}); - -const projectSchema = z.object({ - name: z.string().min(2), - endpoint: z.string().url(), - projectId: z.string().optional(), - apiKey: z.string().min(10), - createProject: z.boolean().optional(), -}); - -const projectUpdateSchema = projectSchema.partial(); - -export const tenantsRouter = Router(); - -tenantsRouter.post('/', async (request, response, next) => { - try { - const payload = tenantSchema.parse(request.body); - const tenant = await createTenant(payload); - response.status(201).json(tenant); - } catch (error) { - next(error); - } -}); - -tenantsRouter.get('/', async (_request, response, next) => { - try { - const tenants = await listTenants(); - response.json(tenants); - } catch (error) { - next(error); - } -}); - -tenantsRouter.post('/:id/appwrite-project', async (request, response, next) => { - try { - const payload = projectSchema.parse(request.body); - const project = await addAppwriteProject(request.params.id, payload); - response.status(201).json(redactProject(project)); - } catch (error) { - next(error); - } -}); - -tenantsRouter.get('/:id/appwrite-projects', async (request, response, next) => { - try { - const projects = await listTenantProjects(request.params.id); - response.json(projects); - } catch (error) { - next(error); - } -}); - -tenantsRouter.patch('/:id/appwrite-projects/:projectRef', async (request, response, next) => { - try { - const payload = projectUpdateSchema.parse(request.body); - const project = await updateTenantProject(request.params.id, request.params.projectRef, payload); - response.json(redactProject(project)); - } catch (error) { - next(error); - } -}); - -tenantsRouter.delete('/:id/appwrite-projects/:projectRef', async (request, response, next) => { - try { - await removeTenantProject(request.params.id, request.params.projectRef); - response.status(204).send(); - } catch (error) { - next(error); - } -}); diff --git a/baas-control-plane/src/modules/tenants/tenants.service.ts b/baas-control-plane/src/modules/tenants/tenants.service.ts index 57eeef8..94efa7e 100644 --- a/baas-control-plane/src/modules/tenants/tenants.service.ts +++ b/baas-control-plane/src/modules/tenants/tenants.service.ts @@ -1,165 +1,33 @@ -import crypto from 'crypto'; -import { createEmptyFinops } from '../finops/finops.service.js'; -import { loadTenants, saveTenants } from './tenants.store.js'; -import type { AppwriteProject, AppwriteProjectInput, Tenant, TenantInput } from './tenants.types.js'; -import { env } from '../../lib/env.js'; -import { createProject } from '../../lib/appwrite.js'; +import { storage } from '../../lib/storage.js'; +import { logger } from '../../lib/logger.js'; +import { Tenant, TenantStatus } from '../../core/types.js'; -const slugify = (name: string) => - name - .toLowerCase() - .normalize('NFD') - .replace(/[^\w\s-]/g, '') - .trim() - .replace(/[\s_-]+/g, '-') - .replace(/^-+|-+$/g, ''); +const TENANTS_FILE = 'tenants.json'; -export const redactProject = (project: AppwriteProject) => ({ - ...project, - apiKey: project.apiKey ? `${project.apiKey.slice(0, 4)}****${project.apiKey.slice(-4)}` : '', -}); - -export const listTenants = async () => { - const tenants = await loadTenants(); - return tenants.map((tenant) => ({ - ...tenant, - appwriteProjects: tenant.appwriteProjects.map(redactProject), - })); -}; - -export const createTenant = async (input: TenantInput): Promise => { - const tenants = await loadTenants(); - const slug = input.slug ? slugify(input.slug) : slugify(input.name); - - if (tenants.some((tenant) => tenant.slug === slug)) { - throw new Error('Tenant slug already exists'); +export class TenantsService { + async listTenants(): Promise { + return storage.readCollection(TENANTS_FILE); } - const now = new Date().toISOString(); - const tenant: Tenant = { - id: crypto.randomUUID(), - name: input.name, - slug, - status: 'active', - createdAt: now, - updatedAt: now, - appwriteProjects: [], - finops: createEmptyFinops(), - }; - - tenants.push(tenant); - await saveTenants(tenants); - - return tenant; -}; - -export const addAppwriteProject = async (tenantId: string, input: AppwriteProjectInput): Promise => { - const tenants = await loadTenants(); - const tenant = tenants.find((item) => item.id === tenantId); - - if (!tenant) { - throw new Error('Tenant not found'); + async getTenant(id: string): Promise { + const tenants = await storage.readCollection(TENANTS_FILE); + return tenants.find((tenant) => tenant.id === id); } - let projectId = input.projectId; - - if (input.createProject) { - if (!env.appwriteAdmin.endpoint || !env.appwriteAdmin.apiKey || !env.appwriteAdmin.projectId) { - throw new Error('Missing Appwrite admin credentials to create projects'); - } - - const created = await createProject( - { - endpoint: env.appwriteAdmin.endpoint, - projectId: env.appwriteAdmin.projectId, - apiKey: env.appwriteAdmin.apiKey, - }, - input.name - ); - projectId = created.projectId; + async createTenant(input: { name: string; plan?: string; status?: TenantStatus }): Promise { + const tenants = await storage.readCollection(TENANTS_FILE); + const now = new Date().toISOString(); + const tenant: Tenant = { + id: crypto.randomUUID(), + name: input.name, + plan: input.plan ?? 'standard', + status: input.status ?? 'active', + createdAt: now, + updatedAt: now, + }; + tenants.push(tenant); + await storage.writeCollection(TENANTS_FILE, tenants); + logger.info('Tenant created', { tenantId: tenant.id }); + return tenant; } - - if (!projectId) { - throw new Error('projectId is required when createProject is false'); - } - - const now = new Date().toISOString(); - const project: AppwriteProject = { - id: crypto.randomUUID(), - name: input.name, - endpoint: input.endpoint, - projectId, - apiKey: input.apiKey, - createdAt: now, - updatedAt: now, - }; - - tenant.appwriteProjects.push(project); - tenant.updatedAt = now; - - await saveTenants(tenants); - - return project; -}; - -export const listTenantProjects = async (tenantId: string) => { - const tenants = await loadTenants(); - const tenant = tenants.find((item) => item.id === tenantId); - - if (!tenant) { - throw new Error('Tenant not found'); - } - - return tenant.appwriteProjects.map(redactProject); -}; - -export const updateTenantProject = async ( - tenantId: string, - projectRef: string, - updates: Partial -): Promise => { - const tenants = await loadTenants(); - const tenant = tenants.find((item) => item.id === tenantId); - - if (!tenant) { - throw new Error('Tenant not found'); - } - - const project = tenant.appwriteProjects.find((item) => item.id === projectRef || item.projectId === projectRef); - - if (!project) { - throw new Error('Project not found'); - } - - project.name = updates.name ?? project.name; - project.endpoint = updates.endpoint ?? project.endpoint; - project.projectId = updates.projectId ?? project.projectId; - project.apiKey = updates.apiKey ?? project.apiKey; - project.updatedAt = new Date().toISOString(); - - tenant.updatedAt = project.updatedAt; - await saveTenants(tenants); - - return project; -}; - -export const removeTenantProject = async (tenantId: string, projectRef: string): Promise => { - const tenants = await loadTenants(); - const tenant = tenants.find((item) => item.id === tenantId); - - if (!tenant) { - throw new Error('Tenant not found'); - } - - const nextProjects = tenant.appwriteProjects.filter( - (item) => item.id !== projectRef && item.projectId !== projectRef - ); - - if (nextProjects.length === tenant.appwriteProjects.length) { - throw new Error('Project not found'); - } - - tenant.appwriteProjects = nextProjects; - tenant.updatedAt = new Date().toISOString(); - await saveTenants(tenants); -}; +} diff --git a/baas-control-plane/src/modules/tenants/tenants.store.ts b/baas-control-plane/src/modules/tenants/tenants.store.ts deleted file mode 100644 index 4d0333b..0000000 --- a/baas-control-plane/src/modules/tenants/tenants.store.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { promises as fs } from 'fs'; -import path from 'path'; -import { env } from '../../lib/env.js'; -import type { Tenant } from './tenants.types.js'; - -const dataFile = path.join(env.dataDir, 'tenants.json'); - -const ensureDataDir = async () => { - await fs.mkdir(env.dataDir, { recursive: true }); -}; - -export const loadTenants = async (): Promise => { - try { - const content = await fs.readFile(dataFile, 'utf-8'); - return JSON.parse(content) as Tenant[]; - } catch (error) { - if (typeof error === 'object' && error !== null && 'code' in error && (error as { code?: string }).code === 'ENOENT') { - return []; - } - throw error; - } -}; - -export const saveTenants = async (tenants: Tenant[]): Promise => { - await ensureDataDir(); - await fs.writeFile(dataFile, JSON.stringify(tenants, null, 2)); -}; diff --git a/baas-control-plane/src/modules/tenants/tenants.types.ts b/baas-control-plane/src/modules/tenants/tenants.types.ts deleted file mode 100644 index 55541c5..0000000 --- a/baas-control-plane/src/modules/tenants/tenants.types.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { FinopsSummary } from '../finops/finops.service.js'; - -export type AppwriteProject = { - id: string; - name: string; - endpoint: string; - projectId: string; - apiKey: string; - createdAt: string; - updatedAt: string; -}; - -export type Tenant = { - id: string; - name: string; - slug: string; - status: 'active' | 'inactive'; - createdAt: string; - updatedAt: string; - appwriteProjects: AppwriteProject[]; - finops: FinopsSummary; -}; - -export type TenantInput = { - name: string; - slug?: string; -}; - -export type AppwriteProjectInput = { - name: string; - endpoint: string; - projectId?: string; - apiKey: string; - createProject?: boolean; -}; diff --git a/baas-control-plane/src/providers/appwrite/appwrite.client.ts b/baas-control-plane/src/providers/appwrite/appwrite.client.ts new file mode 100644 index 0000000..d8204c8 --- /dev/null +++ b/baas-control-plane/src/providers/appwrite/appwrite.client.ts @@ -0,0 +1,48 @@ +import { ProviderMetrics, ProviderProject, ProviderSecrets, SchemaDefinition } from '../../core/types.js'; +import { logger } from '../../lib/logger.js'; + +export class AppwriteClient { + async createProject(name: string, secrets: ProviderSecrets): Promise { + logger.info('Appwrite create project requested', { name, endpoint: secrets.endpoint }); + return { + externalId: `appwrite_${crypto.randomUUID()}`, + dashboardUrl: `${secrets.endpoint}/console/project`, + }; + } + + async deleteProject(externalId: string, secrets: ProviderSecrets): Promise { + logger.info('Appwrite delete project requested', { externalId, endpoint: secrets.endpoint }); + } + + async applySchema(externalId: string, schema: SchemaDefinition, secrets: ProviderSecrets): Promise { + logger.info('Appwrite apply schema requested', { + externalId, + version: schema.version, + endpoint: secrets.endpoint, + }); + } + + async collectMetrics(externalId: string, secrets: ProviderSecrets): Promise { + logger.info('Appwrite metrics requested', { externalId, endpoint: secrets.endpoint }); + return { + users: 0, + storageMb: 0, + requests: 0, + functions: 0, + capturedAt: new Date().toISOString(), + }; + } + + async rotateSecrets(externalId: string, secrets: ProviderSecrets): Promise { + logger.info('Appwrite secrets rotation requested', { externalId, endpoint: secrets.endpoint }); + return { + ...secrets, + apiKey: `${secrets.apiKey}-rotated`, + }; + } + + async healthCheck(secrets: ProviderSecrets): Promise { + logger.info('Appwrite health check requested', { endpoint: secrets.endpoint }); + return true; + } +} diff --git a/baas-control-plane/src/providers/appwrite/appwrite.metrics.ts b/baas-control-plane/src/providers/appwrite/appwrite.metrics.ts new file mode 100644 index 0000000..a293239 --- /dev/null +++ b/baas-control-plane/src/providers/appwrite/appwrite.metrics.ts @@ -0,0 +1,10 @@ +import { ProviderMetrics, ProviderSecrets } from '../../core/types.js'; +import { AppwriteClient } from './appwrite.client.js'; + +const client = new AppwriteClient(); + +export const appwriteMetrics = { + async collect(externalId: string, secrets: ProviderSecrets): Promise { + return client.collectMetrics(externalId, secrets); + }, +}; diff --git a/baas-control-plane/src/providers/appwrite/appwrite.provisioning.ts b/baas-control-plane/src/providers/appwrite/appwrite.provisioning.ts new file mode 100644 index 0000000..fdb99f1 --- /dev/null +++ b/baas-control-plane/src/providers/appwrite/appwrite.provisioning.ts @@ -0,0 +1,31 @@ +import { ProviderInterface } from '../../core/provider.interface.js'; +import { ProviderMetrics, ProviderProject, ProviderSecrets, SchemaDefinition } from '../../core/types.js'; +import { AppwriteClient } from './appwrite.client.js'; + +export class AppwriteProvider implements ProviderInterface { + private readonly client = new AppwriteClient(); + + async createProject(name: string, secrets: ProviderSecrets): Promise { + return this.client.createProject(name, secrets); + } + + async deleteProject(externalId: string, secrets: ProviderSecrets): Promise { + await this.client.deleteProject(externalId, secrets); + } + + async applySchema(externalId: string, schema: SchemaDefinition, secrets: ProviderSecrets): Promise { + await this.client.applySchema(externalId, schema, secrets); + } + + async collectMetrics(externalId: string, secrets: ProviderSecrets): Promise { + return this.client.collectMetrics(externalId, secrets); + } + + async rotateSecrets(externalId: string, secrets: ProviderSecrets): Promise { + return this.client.rotateSecrets(externalId, secrets); + } + + async healthCheck(secrets: ProviderSecrets): Promise { + return this.client.healthCheck(secrets); + } +} diff --git a/baas-control-plane/src/providers/appwrite/appwrite.schema.ts b/baas-control-plane/src/providers/appwrite/appwrite.schema.ts new file mode 100644 index 0000000..3feee19 --- /dev/null +++ b/baas-control-plane/src/providers/appwrite/appwrite.schema.ts @@ -0,0 +1,10 @@ +import { SchemaDefinition, ProviderSecrets } from '../../core/types.js'; +import { AppwriteClient } from './appwrite.client.js'; + +const client = new AppwriteClient(); + +export const appwriteSchema = { + async apply(externalId: string, schema: SchemaDefinition, secrets: ProviderSecrets): Promise { + await client.applySchema(externalId, schema, secrets); + }, +}; diff --git a/baas-control-plane/src/providers/supabase/supabase.client.ts b/baas-control-plane/src/providers/supabase/supabase.client.ts new file mode 100644 index 0000000..cdba5a8 --- /dev/null +++ b/baas-control-plane/src/providers/supabase/supabase.client.ts @@ -0,0 +1,48 @@ +import { ProviderMetrics, ProviderProject, ProviderSecrets, SchemaDefinition } from '../../core/types.js'; +import { logger } from '../../lib/logger.js'; + +export class SupabaseClient { + async createProject(name: string, secrets: ProviderSecrets): Promise { + logger.info('Supabase create project requested', { name, endpoint: secrets.endpoint }); + return { + externalId: `supabase_${crypto.randomUUID()}`, + dashboardUrl: `${secrets.endpoint}/project`, + }; + } + + async deleteProject(externalId: string, secrets: ProviderSecrets): Promise { + logger.info('Supabase delete project requested', { externalId, endpoint: secrets.endpoint }); + } + + async applySchema(externalId: string, schema: SchemaDefinition, secrets: ProviderSecrets): Promise { + logger.info('Supabase apply schema requested', { + externalId, + version: schema.version, + endpoint: secrets.endpoint, + }); + } + + async collectMetrics(externalId: string, secrets: ProviderSecrets): Promise { + logger.info('Supabase metrics requested', { externalId, endpoint: secrets.endpoint }); + return { + users: 0, + storageMb: 0, + requests: 0, + functions: 0, + capturedAt: new Date().toISOString(), + }; + } + + async rotateSecrets(externalId: string, secrets: ProviderSecrets): Promise { + logger.info('Supabase secrets rotation requested', { externalId, endpoint: secrets.endpoint }); + return { + ...secrets, + apiKey: `${secrets.apiKey}-rotated`, + }; + } + + async healthCheck(secrets: ProviderSecrets): Promise { + logger.info('Supabase health check requested', { endpoint: secrets.endpoint }); + return true; + } +} diff --git a/baas-control-plane/src/providers/supabase/supabase.metrics.ts b/baas-control-plane/src/providers/supabase/supabase.metrics.ts new file mode 100644 index 0000000..e331300 --- /dev/null +++ b/baas-control-plane/src/providers/supabase/supabase.metrics.ts @@ -0,0 +1,10 @@ +import { ProviderMetrics, ProviderSecrets } from '../../core/types.js'; +import { SupabaseClient } from './supabase.client.js'; + +const client = new SupabaseClient(); + +export const supabaseMetrics = { + async collect(externalId: string, secrets: ProviderSecrets): Promise { + return client.collectMetrics(externalId, secrets); + }, +}; diff --git a/baas-control-plane/src/providers/supabase/supabase.provisioning.ts b/baas-control-plane/src/providers/supabase/supabase.provisioning.ts new file mode 100644 index 0000000..c008a6c --- /dev/null +++ b/baas-control-plane/src/providers/supabase/supabase.provisioning.ts @@ -0,0 +1,31 @@ +import { ProviderInterface } from '../../core/provider.interface.js'; +import { ProviderMetrics, ProviderProject, ProviderSecrets, SchemaDefinition } from '../../core/types.js'; +import { SupabaseClient } from './supabase.client.js'; + +export class SupabaseProvider implements ProviderInterface { + private readonly client = new SupabaseClient(); + + async createProject(name: string, secrets: ProviderSecrets): Promise { + return this.client.createProject(name, secrets); + } + + async deleteProject(externalId: string, secrets: ProviderSecrets): Promise { + await this.client.deleteProject(externalId, secrets); + } + + async applySchema(externalId: string, schema: SchemaDefinition, secrets: ProviderSecrets): Promise { + await this.client.applySchema(externalId, schema, secrets); + } + + async collectMetrics(externalId: string, secrets: ProviderSecrets): Promise { + return this.client.collectMetrics(externalId, secrets); + } + + async rotateSecrets(externalId: string, secrets: ProviderSecrets): Promise { + return this.client.rotateSecrets(externalId, secrets); + } + + async healthCheck(secrets: ProviderSecrets): Promise { + return this.client.healthCheck(secrets); + } +} diff --git a/baas-control-plane/src/providers/supabase/supabase.schema.ts b/baas-control-plane/src/providers/supabase/supabase.schema.ts new file mode 100644 index 0000000..5f0c671 --- /dev/null +++ b/baas-control-plane/src/providers/supabase/supabase.schema.ts @@ -0,0 +1,10 @@ +import { ProviderSecrets, SchemaDefinition } from '../../core/types.js'; +import { SupabaseClient } from './supabase.client.js'; + +const client = new SupabaseClient(); + +export const supabaseSchema = { + async apply(externalId: string, schema: SchemaDefinition, secrets: ProviderSecrets): Promise { + await client.applySchema(externalId, schema, secrets); + }, +}; diff --git a/baas-control-plane/src/scripts/setup-appwrite.ts b/baas-control-plane/src/scripts/setup-appwrite.ts deleted file mode 100644 index 7b2ca09..0000000 --- a/baas-control-plane/src/scripts/setup-appwrite.ts +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env node - -import { applySchema } from '../lib/appwrite.js'; -import { env } from '../lib/env.js'; -import { logger } from '../lib/logger.js'; - -const endpoint = process.env.APPWRITE_SETUP_ENDPOINT || env.appwriteAdmin.endpoint || env.defaults.appwriteEndpoint; -const projectId = process.env.APPWRITE_SETUP_PROJECT_ID || ''; -const apiKey = process.env.APPWRITE_SETUP_API_KEY || env.appwriteAdmin.apiKey; - -if (!endpoint || !projectId || !apiKey) { - logger.error('Missing Appwrite setup credentials', { - endpoint: Boolean(endpoint), - projectId: Boolean(projectId), - apiKey: Boolean(apiKey), - }); - process.exit(1); -} - -const run = async () => { - const result = await applySchema({ endpoint, projectId, apiKey }, { logPrefix: 'setup' }); - logger.info('Setup completed', result); -}; - -run().catch((error: Error) => { - logger.error('Setup failed', { message: error.message }); - process.exit(1); -});