From 93df3c8c20134a664fb99483339492df0d8ade15 Mon Sep 17 00:00:00 2001 From: defiQUG Date: Mon, 9 Feb 2026 21:51:50 -0800 Subject: [PATCH] Initial commit: add .gitignore and README --- .github/workflows/ci.yml | 38 + .gitignore | 16 + .nvmrc | 1 + README.md | 42 + apps/api/package.json | 36 + apps/api/src/audit.ts | 31 + apps/api/src/auth.ts | 70 + apps/api/src/health.test.ts | 7 + apps/api/src/index.ts | 69 + apps/api/src/integrations/proxmox.ts | 2 + apps/api/src/integrations/redfish.ts | 2 + apps/api/src/integrations/unifi.ts | 26 + apps/api/src/openapi-spec.ts | 20 + apps/api/src/routes/v1/asset-components.ts | 30 + apps/api/src/routes/v1/assets.ts | 90 + apps/api/src/routes/v1/auth.test.ts | 25 + apps/api/src/routes/v1/auth.ts | 42 + apps/api/src/routes/v1/capacity.ts | 89 + apps/api/src/routes/v1/compliance-profiles.ts | 77 + apps/api/src/routes/v1/index.ts | 44 + apps/api/src/routes/v1/ingestion.test.ts | 35 + apps/api/src/routes/v1/ingestion.ts | 60 + apps/api/src/routes/v1/inspection.ts | 47 + apps/api/src/routes/v1/integrations.ts | 50 + apps/api/src/routes/v1/maintenances.ts | 29 + apps/api/src/routes/v1/offers.ts | 57 + apps/api/src/routes/v1/purchase-orders.ts | 74 + apps/api/src/routes/v1/reports.ts | 46 + apps/api/src/routes/v1/roles.ts | 43 + apps/api/src/routes/v1/shipments.ts | 39 + apps/api/src/routes/v1/sites.ts | 68 + apps/api/src/routes/v1/unifi-controllers.ts | 59 + apps/api/src/routes/v1/upload.ts | 18 + apps/api/src/routes/v1/users.ts | 77 + apps/api/src/routes/v1/vendors.test.ts | 26 + apps/api/src/routes/v1/vendors.ts | 58 + apps/api/src/routes/v1/workflow.ts | 56 + apps/api/src/schemas/errors.ts | 14 + apps/api/src/storage.ts | 67 + apps/api/tsconfig.json | 13 + apps/web/index.html | 12 + apps/web/package.json | 25 + apps/web/src/App.tsx | 33 + apps/web/src/api/client.ts | 58 + apps/web/src/components/Layout.tsx | 40 + apps/web/src/contexts/AuthContext.tsx | 70 + apps/web/src/index.css | 3 + apps/web/src/main.tsx | 10 + apps/web/src/pages/Assets.tsx | 30 + apps/web/src/pages/Capacity.tsx | 140 + apps/web/src/pages/Dashboard.tsx | 8 + apps/web/src/pages/Login.tsx | 42 + apps/web/src/pages/Offers.tsx | 27 + apps/web/src/pages/PurchaseOrders.tsx | 30 + apps/web/src/pages/Sites.tsx | 30 + apps/web/src/pages/Vendors.tsx | 27 + apps/web/tsconfig.json | 17 + apps/web/tsconfig.node.json | 10 + apps/web/vite.config.ts | 14 + apps/workflow/package.json | 24 + apps/workflow/src/index.test.ts | 12 + apps/workflow/src/index.ts | 39 + apps/workflow/tsconfig.json | 15 + data/inspection-checklists/gpus.json | 1 + data/inspection-checklists/memory.json | 1 + data/inspection-checklists/nics.json | 9 + data/inspection-checklists/servers.json | 1 + data/operational-baseline-hardware.json | 67 + docs/api-error-format.md | 17 + docs/architecture.md | 20 + docs/capacity-dashboard-spec.md | 7 + docs/cicd.md | 11 + docs/compliance-profiles.md | 23 + docs/erd.md | 70 + docs/integration-spec-proxmox.md | 2 + docs/integration-spec-redfish.md | 2 + docs/integration-spec-unifi.md | 21 + docs/next-steps-before-swagger-and-ui.md | 111 + docs/observability.md | 2 + docs/offer-ingestion.md | 99 + docs/openapi.yaml | 175 + docs/operational-baseline.md | 95 + docs/purchasing-feedback-loop.md | 29 + docs/rbac-sovereign-operations.md | 36 + docs/runbooks/incident-response.md | 4 + docs/runbooks/provisioning-and-integration.md | 6 + docs/runbooks/receiving-and-inspection.md | 9 + docs/runbooks/receiving-and-racking.md | 13 + docs/security.md | 2 + docs/sovereign-controller-topology.md | 42 + docs/vendor-portal.md | 49 + env.example | 30 + eslint.config.js | 17 + infra/docker-compose.yml | 40 + package.json | 23 + packages/auth/package.json | 22 + packages/auth/src/index.test.ts | 6 + packages/auth/src/index.ts | 64 + packages/auth/tsconfig.json | 15 + packages/schema/drizzle.config.ts | 10 + packages/schema/drizzle/0000_initial.sql | 287 + .../drizzle/0001_inspection_workflow.sql | 41 + .../0002_unifi_product_intelligence.sql | 24 + .../drizzle/0003_compliance_profiles.sql | 14 + .../schema/drizzle/0004_unifi_controllers.sql | 12 + .../0005_vendor_user_and_ingestion.sql | 10 + packages/schema/drizzle/meta/_journal.json | 1 + packages/schema/package.json | 28 + packages/schema/src/db/client.ts | 12 + packages/schema/src/db/schema.test.ts | 10 + packages/schema/src/db/schema.ts | 417 ++ packages/schema/src/index.ts | 2 + packages/schema/tsconfig.json | 15 + packages/schema/vitest.config.ts | 8 + pnpm-lock.yaml | 5636 +++++++++++++++++ pnpm-workspace.yaml | 3 + 116 files changed, 10080 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .nvmrc create mode 100644 README.md create mode 100644 apps/api/package.json create mode 100644 apps/api/src/audit.ts create mode 100644 apps/api/src/auth.ts create mode 100644 apps/api/src/health.test.ts create mode 100644 apps/api/src/index.ts create mode 100644 apps/api/src/integrations/proxmox.ts create mode 100644 apps/api/src/integrations/redfish.ts create mode 100644 apps/api/src/integrations/unifi.ts create mode 100644 apps/api/src/openapi-spec.ts create mode 100644 apps/api/src/routes/v1/asset-components.ts create mode 100644 apps/api/src/routes/v1/assets.ts create mode 100644 apps/api/src/routes/v1/auth.test.ts create mode 100644 apps/api/src/routes/v1/auth.ts create mode 100644 apps/api/src/routes/v1/capacity.ts create mode 100644 apps/api/src/routes/v1/compliance-profiles.ts create mode 100644 apps/api/src/routes/v1/index.ts create mode 100644 apps/api/src/routes/v1/ingestion.test.ts create mode 100644 apps/api/src/routes/v1/ingestion.ts create mode 100644 apps/api/src/routes/v1/inspection.ts create mode 100644 apps/api/src/routes/v1/integrations.ts create mode 100644 apps/api/src/routes/v1/maintenances.ts create mode 100644 apps/api/src/routes/v1/offers.ts create mode 100644 apps/api/src/routes/v1/purchase-orders.ts create mode 100644 apps/api/src/routes/v1/reports.ts create mode 100644 apps/api/src/routes/v1/roles.ts create mode 100644 apps/api/src/routes/v1/shipments.ts create mode 100644 apps/api/src/routes/v1/sites.ts create mode 100644 apps/api/src/routes/v1/unifi-controllers.ts create mode 100644 apps/api/src/routes/v1/upload.ts create mode 100644 apps/api/src/routes/v1/users.ts create mode 100644 apps/api/src/routes/v1/vendors.test.ts create mode 100644 apps/api/src/routes/v1/vendors.ts create mode 100644 apps/api/src/routes/v1/workflow.ts create mode 100644 apps/api/src/schemas/errors.ts create mode 100644 apps/api/src/storage.ts create mode 100644 apps/api/tsconfig.json create mode 100644 apps/web/index.html create mode 100644 apps/web/package.json create mode 100644 apps/web/src/App.tsx create mode 100644 apps/web/src/api/client.ts create mode 100644 apps/web/src/components/Layout.tsx create mode 100644 apps/web/src/contexts/AuthContext.tsx create mode 100644 apps/web/src/index.css create mode 100644 apps/web/src/main.tsx create mode 100644 apps/web/src/pages/Assets.tsx create mode 100644 apps/web/src/pages/Capacity.tsx create mode 100644 apps/web/src/pages/Dashboard.tsx create mode 100644 apps/web/src/pages/Login.tsx create mode 100644 apps/web/src/pages/Offers.tsx create mode 100644 apps/web/src/pages/PurchaseOrders.tsx create mode 100644 apps/web/src/pages/Sites.tsx create mode 100644 apps/web/src/pages/Vendors.tsx create mode 100644 apps/web/tsconfig.json create mode 100644 apps/web/tsconfig.node.json create mode 100644 apps/web/vite.config.ts create mode 100644 apps/workflow/package.json create mode 100644 apps/workflow/src/index.test.ts create mode 100644 apps/workflow/src/index.ts create mode 100644 apps/workflow/tsconfig.json create mode 100644 data/inspection-checklists/gpus.json create mode 100644 data/inspection-checklists/memory.json create mode 100644 data/inspection-checklists/nics.json create mode 100644 data/inspection-checklists/servers.json create mode 100644 data/operational-baseline-hardware.json create mode 100644 docs/api-error-format.md create mode 100644 docs/architecture.md create mode 100644 docs/capacity-dashboard-spec.md create mode 100644 docs/cicd.md create mode 100644 docs/compliance-profiles.md create mode 100644 docs/erd.md create mode 100644 docs/integration-spec-proxmox.md create mode 100644 docs/integration-spec-redfish.md create mode 100644 docs/integration-spec-unifi.md create mode 100644 docs/next-steps-before-swagger-and-ui.md create mode 100644 docs/observability.md create mode 100644 docs/offer-ingestion.md create mode 100644 docs/openapi.yaml create mode 100644 docs/operational-baseline.md create mode 100644 docs/purchasing-feedback-loop.md create mode 100644 docs/rbac-sovereign-operations.md create mode 100644 docs/runbooks/incident-response.md create mode 100644 docs/runbooks/provisioning-and-integration.md create mode 100644 docs/runbooks/receiving-and-inspection.md create mode 100644 docs/runbooks/receiving-and-racking.md create mode 100644 docs/security.md create mode 100644 docs/sovereign-controller-topology.md create mode 100644 docs/vendor-portal.md create mode 100644 env.example create mode 100644 eslint.config.js create mode 100644 infra/docker-compose.yml create mode 100644 package.json create mode 100644 packages/auth/package.json create mode 100644 packages/auth/src/index.test.ts create mode 100644 packages/auth/src/index.ts create mode 100644 packages/auth/tsconfig.json create mode 100644 packages/schema/drizzle.config.ts create mode 100644 packages/schema/drizzle/0000_initial.sql create mode 100644 packages/schema/drizzle/0001_inspection_workflow.sql create mode 100644 packages/schema/drizzle/0002_unifi_product_intelligence.sql create mode 100644 packages/schema/drizzle/0003_compliance_profiles.sql create mode 100644 packages/schema/drizzle/0004_unifi_controllers.sql create mode 100644 packages/schema/drizzle/0005_vendor_user_and_ingestion.sql create mode 100644 packages/schema/drizzle/meta/_journal.json create mode 100644 packages/schema/package.json create mode 100644 packages/schema/src/db/client.ts create mode 100644 packages/schema/src/db/schema.test.ts create mode 100644 packages/schema/src/db/schema.ts create mode 100644 packages/schema/src/index.ts create mode 100644 packages/schema/tsconfig.json create mode 100644 packages/schema/vitest.config.ts create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b2634dd --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,38 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint-and-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 9 + + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Lint + run: pnpm run lint + + - name: Test + run: pnpm run test + + - name: Build + run: pnpm run build diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a856cde --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +node_modules +dist +build +.env +.env.* +!.env.example +*.log +.DS_Store +coverage +.nyc_output +.turbo +*.tsbuildinfo +.idea +.vscode/* +!.vscode/extensions.json +!.vscode/settings.json diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..209e3ef --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20 diff --git a/README.md b/README.md new file mode 100644 index 0000000..06b68d9 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# Sankofa HW Infra + +Hardware procurement, inventory, and operations platform for **sovereign cloud operations**: offer intake, inspection workflows, purchasing controls, asset lifecycle, multi-site management, and integrations. UniFi is used as a **hardware-aware, compliance-relevant infrastructure layer** (product intelligence, support horizon, per-sovereign controller topology). See [docs/architecture.md](docs/architecture.md), [docs/integration-spec-unifi.md](docs/integration-spec-unifi.md), and [docs/sovereign-controller-topology.md](docs/sovereign-controller-topology.md). + +## Stack + +- **Monorepo**: pnpm workspaces +- **API**: Fastify (Node), REST `/api/v1`, JWT + RBAC/ABAC +- **Web**: React + Vite +- **DB**: PostgreSQL (Drizzle), S3-compatible object storage +- **Workflow**: Embedded state machines (PO approval, inspection) + +## Quick start + +1. Copy `env.example` to `.env` and set `DATABASE_URL`, optional `S3_*`, `JWT_SECRET`. +2. Start Postgres: `cd infra && docker compose up -d` +3. Migrate: `pnpm db:migrate` +4. Install: `pnpm install` +5. API: `pnpm --filter @sankofa/api run dev` (port 4000) +6. Web: `pnpm --filter @sankofa/web run dev` (port 3000) + +## Scripts + +- `pnpm run build` — build all packages +- `pnpm run test` — run tests +- `pnpm run lint` — lint +- `pnpm db:migrate` — run DB migrations + +## Docs + +- [Architecture](docs/architecture.md) +- [ERD](docs/erd.md) +- [OpenAPI](docs/openapi.yaml) +- [RBAC sovereign operations](docs/rbac-sovereign-operations.md), [Compliance profiles](docs/compliance-profiles.md) +- [CI/CD](docs/cicd.md) +- [Integration specs](docs/integration-spec-unifi.md), [Proxmox](docs/integration-spec-proxmox.md), [Redfish](docs/integration-spec-redfish.md) +- [Purchasing feedback loop](docs/purchasing-feedback-loop.md), [Sovereign controller topology](docs/sovereign-controller-topology.md) +- [Capacity dashboard spec](docs/capacity-dashboard-spec.md) (RU utilization, power headroom, GPU inventory; UI at `/capacity`) +- [Operational baseline](docs/operational-baseline.md) (current hardware in-hand; see [data/operational-baseline-hardware.json](data/operational-baseline-hardware.json) for structured import) +- [Vendor portal](docs/vendor-portal.md) (vendor user login, scoped offers/POs), [Offer ingestion](docs/offer-ingestion.md) (scrape + email intake) +- [Next steps before Swagger and UI](docs/next-steps-before-swagger-and-ui.md) +- [Runbooks](docs/runbooks/) diff --git a/apps/api/package.json b/apps/api/package.json new file mode 100644 index 0000000..40eade0 --- /dev/null +++ b/apps/api/package.json @@ -0,0 +1,36 @@ +{ + "name": "@sankofa/api", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "tsc", + "dev": "tsx watch src/index.ts", + "start": "node dist/index.js", + "test": "vitest run", + "lint": "eslint src --ext .ts" + }, + "dependencies": { + "@aws-sdk/client-s3": "^3.700.0", + "@aws-sdk/s3-request-presigner": "^3.700.0", + "@fastify/cors": "^10.0.0", + "@fastify/jwt": "^9.0.0", + "@fastify/multipart": "^9.0.0", + "@fastify/sensible": "^6.0.0", + "@fastify/swagger": "^9.7.0", + "@fastify/swagger-ui": "^5.2.5", + "@sankofa/auth": "workspace:*", + "@sankofa/schema": "workspace:*", + "@sankofa/workflow": "workspace:*", + "drizzle-orm": "^0.36.0", + "fastify": "^5.1.0", + "yaml": "^2.8.2" + }, + "devDependencies": { + "@types/node": "^22.10.0", + "eslint": "^9.15.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0", + "vitest": "^2.1.0" + } +} diff --git a/apps/api/src/audit.ts b/apps/api/src/audit.ts new file mode 100644 index 0000000..8ed2372 --- /dev/null +++ b/apps/api/src/audit.ts @@ -0,0 +1,31 @@ +import type { FastifyRequest } from "fastify"; +import { auditEvents } from "@sankofa/schema"; + +export interface AuditPayload { + orgId: string; + actorId?: string; + actorEmail?: string; + action: string; + resourceType: string; + resourceId: string; + beforeState?: Record; + afterState?: Record; +} + +export function getActorFromRequest(req: FastifyRequest): { actorId?: string; actorEmail?: string } { + const user = (req as unknown as { user?: { sub?: string; email?: string } }).user; + return { actorId: user?.sub, actorEmail: user?.email ?? (req.headers["x-user-email"] as string) }; +} + +export async function writeAudit(db: ReturnType, payload: AuditPayload) { + await db.insert(auditEvents).values({ + orgId: payload.orgId, + actorId: payload.actorId ?? null, + actorEmail: payload.actorEmail ?? null, + action: payload.action, + resourceType: payload.resourceType, + resourceId: payload.resourceId, + beforeState: payload.beforeState ?? null, + afterState: payload.afterState ?? null, + }); +} diff --git a/apps/api/src/auth.ts b/apps/api/src/auth.ts new file mode 100644 index 0000000..dd13ecd --- /dev/null +++ b/apps/api/src/auth.ts @@ -0,0 +1,70 @@ +import type { FastifyInstance, FastifyRequest } from "fastify"; +import { hasPermission, hasAnyPermission, type RoleName, type Permission } from "@sankofa/auth"; + +const ORG_HEADER = "x-org-id"; +const ROLES_HEADER = "x-roles"; + +export async function authPlugin(app: FastifyInstance) { + app.decorate("orgId", (req: FastifyRequest): string => { + const h = (req.headers[ORG_HEADER] as string) || ""; + return h || "default"; + }); + + app.decorate("getRoles", (req: FastifyRequest): RoleName[] => { + const header = (req.headers[ROLES_HEADER] as string) || ""; + if (header) return header.split(",").map((r) => r.trim() as RoleName).filter(Boolean); + const payload = (req as unknown as { user?: { roles?: string[] } }).user; + return (payload?.roles as RoleName[]) ?? []; + }); + + app.decorate("vendorId", (req: FastifyRequest): string | null => { + const payload = (req as unknown as { user?: { vendorId?: string } }).user; + return payload?.vendorId ?? null; + }); + + app.decorate("requirePermission", (permission: Permission) => async (req: FastifyRequest) => { + const payload = (req as unknown as { user?: { sub?: string } }).user; + if (!payload?.sub) throw app.httpErrors.unauthorized("Authentication required"); + const roles = app.getRoles(req); + if (!hasPermission(roles, permission)) throw app.httpErrors.forbidden("Insufficient permission"); + }); + + app.decorate("requireAnyPermission", (permissions: Permission[]) => async (req: FastifyRequest) => { + const payload = (req as unknown as { user?: { sub?: string } }).user; + if (!payload?.sub) throw app.httpErrors.unauthorized("Authentication required"); + const roles = app.getRoles(req); + if (!hasAnyPermission(roles, permissions)) throw app.httpErrors.forbidden("Insufficient permission"); + }); + + app.addHook("preHandler", async (req) => { + try { + await req.jwtVerify(); + (req as unknown as { user?: unknown }).user = (req as unknown as { user: unknown }).user ?? {}; + } catch { + // Optional JWT + } + }); + + app.addHook("preHandler", async (req) => { + const permission = (req.routeOptions.config as { permission?: Permission } | undefined)?.permission; + if (!permission) return; + const payload = (req as unknown as { user?: { sub?: string } }).user; + if (!payload?.sub) throw app.httpErrors.unauthorized("Authentication required"); + const roles = app.getRoles(req); + if (!hasPermission(roles, permission)) throw app.httpErrors.forbidden("Insufficient permission"); + }); +} + +declare module "fastify" { + interface FastifyInstance { + orgId: (req: FastifyRequest) => string; + getRoles: (req: FastifyRequest) => RoleName[]; + vendorId: (req: FastifyRequest) => string | null; + requirePermission: (permission: Permission) => (req: FastifyRequest) => Promise; + requireAnyPermission: (permissions: Permission[]) => (req: FastifyRequest) => Promise; + db: ReturnType; + } + interface FastifyContextConfig { + permission?: Permission; + } +} diff --git a/apps/api/src/health.test.ts b/apps/api/src/health.test.ts new file mode 100644 index 0000000..d2185c8 --- /dev/null +++ b/apps/api/src/health.test.ts @@ -0,0 +1,7 @@ +import { describe, it, expect } from "vitest"; + +describe("api", () => { + it("placeholder", () => { + expect(1).toBe(1); + }); +}); diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts new file mode 100644 index 0000000..6623d3a --- /dev/null +++ b/apps/api/src/index.ts @@ -0,0 +1,69 @@ +import { fileURLToPath } from "node:url"; +import Fastify from "fastify"; +import cors from "@fastify/cors"; +import jwt from "@fastify/jwt"; +import multipart from "@fastify/multipart"; +import sensible from "@fastify/sensible"; +import { getDb } from "@sankofa/schema"; +import { authPlugin } from "./auth.js"; +import { registerV1Routes } from "./routes/v1/index.js"; +import { errorCodes, type ApiErrorPayload } from "./schemas/errors.js"; +import { openApiSpec } from "./openapi-spec.js"; + +const PORT = Number(process.env.API_PORT) || 4000; +const HOST = process.env.API_HOST || "0.0.0"; + +const statusToCode: Record = { + 400: errorCodes.BAD_REQUEST, + 401: errorCodes.UNAUTHORIZED, + 403: errorCodes.FORBIDDEN, + 404: errorCodes.NOT_FOUND, + 409: errorCodes.CONFLICT, +}; + +export async function buildApp() { + const app = Fastify({ logger: false }); + await app.register(cors, { origin: true }); + await app.register(sensible); + await app.register(jwt, { + secret: process.env.JWT_SECRET || "dev-secret-change-in-production", + }); + await app.register(multipart, { limits: { fileSize: 50 * 1024 * 1024 } }); + app.decorate("db", getDb()); + await app.register(authPlugin); + await app.register(registerV1Routes, { prefix: "/api/v1" }); + app.get("/health", async () => ({ status: "ok" })); + + app.get("/api/openapi.json", async (_req, reply) => reply.type("application/json").send(openApiSpec)); + app.get("/api/docs", async (_req, reply) => { + reply.type("text/html").send(` + + +Sankofa API +
+ + + +`); + }); + + app.setErrorHandler((err: { statusCode?: number; message?: string; validation?: unknown }, _req, reply) => { + const status = err.statusCode ?? 500; + const payload: ApiErrorPayload = { + error: err.message ?? "Internal Server Error", + code: statusToCode[status] ?? "INTERNAL_ERROR", + }; + if (err.validation) payload.details = err.validation; + return reply.status(status).send(payload); + }); + + return app; +} + +async function main() { + const app = await buildApp(); + await app.listen({ port: PORT, host: HOST }); +} + +const isMain = process.argv[1] === fileURLToPath(import.meta.url); +if (isMain) main().catch((err) => { console.error(err); process.exit(1); }); diff --git a/apps/api/src/integrations/proxmox.ts b/apps/api/src/integrations/proxmox.ts new file mode 100644 index 0000000..9d4a88f --- /dev/null +++ b/apps/api/src/integrations/proxmox.ts @@ -0,0 +1,2 @@ +export interface ProxmoxNode { node: string; status?: string; } +export async function listProxmoxNodes(_baseUrl: string, _token: string): Promise { return []; } diff --git a/apps/api/src/integrations/redfish.ts b/apps/api/src/integrations/redfish.ts new file mode 100644 index 0000000..25771f7 --- /dev/null +++ b/apps/api/src/integrations/redfish.ts @@ -0,0 +1,2 @@ +export interface RedfishSystem { id: string; serialNumber?: string; } +export async function getRedfishSystem(_baseUrl: string, _token: string, _systemId: string): Promise { return null; } diff --git a/apps/api/src/integrations/unifi.ts b/apps/api/src/integrations/unifi.ts new file mode 100644 index 0000000..60d181b --- /dev/null +++ b/apps/api/src/integrations/unifi.ts @@ -0,0 +1,26 @@ +export interface UnifiDevice { + id: string; + name: string; + model?: string; + generation?: string; + supportHorizon?: string; +} + +export async function listUnifiDevices(_baseUrl: string, _token: string): Promise { + return []; +} + +export type CatalogRow = { sku: string; modelName: string; generation: string; supportHorizon: string | null }; + +export function enrichDevicesWithCatalog( + devices: UnifiDevice[], + catalog: CatalogRow[] +): UnifiDevice[] { + const bySku = new Map(catalog.map((c) => [c.sku, c])); + const byModel = new Map(catalog.map((c) => [c.modelName, c])); + return devices.map((d) => { + const match = (d.model && bySku.get(d.model)) || (d.model && byModel.get(d.model)); + if (!match) return d; + return { ...d, generation: match.generation, supportHorizon: match.supportHorizon ?? undefined }; + }); +} diff --git a/apps/api/src/openapi-spec.ts b/apps/api/src/openapi-spec.ts new file mode 100644 index 0000000..00f2950 --- /dev/null +++ b/apps/api/src/openapi-spec.ts @@ -0,0 +1,20 @@ +import { readFileSync } from "fs"; +import { join } from "path"; +import { parse } from "yaml"; + +function loadSpec(): Record { + try { + const path = join(process.cwd(), "docs", "openapi.yaml"); + const raw = readFileSync(path, "utf8"); + return parse(raw) as Record; + } catch { + return { + openapi: "3.0.3", + info: { title: "Sankofa HW Infra API", version: "0.1.0" }, + servers: [{ url: "/api/v1" }], + paths: {}, + }; + } +} + +export const openApiSpec = loadSpec(); diff --git a/apps/api/src/routes/v1/asset-components.ts b/apps/api/src/routes/v1/asset-components.ts new file mode 100644 index 0000000..7853bb8 --- /dev/null +++ b/apps/api/src/routes/v1/asset-components.ts @@ -0,0 +1,30 @@ +import type { FastifyInstance } from "fastify"; +import { eq, and } from "drizzle-orm"; +import { assetComponents as acTable, assets as assetsTable } from "@sankofa/schema"; + +export async function assetComponentsRoutes(app: FastifyInstance) { + const db = app.db; + app.get("/", async (req, reply) => { + const list = await db.select().from(acTable); + return reply.send(list); + }); + app.get<{ Params: { assetId: string } }>("/by-parent/:assetId", async (req, reply) => { + const orgId = app.orgId(req); + const [parent] = await db.select().from(assetsTable).where(and(eq(assetsTable.id, req.params.assetId), eq(assetsTable.orgId, orgId))); + if (!parent) return reply.notFound(); + const list = await db.select().from(acTable).where(eq(acTable.parentAssetId, req.params.assetId)); + return reply.send(list); + }); + app.post<{ Body: { parentAssetId: string; childAssetId: string; role: string; slotIndex?: number } }>("/", async (req, reply) => { + const orgId = app.orgId(req); + const [parent] = await db.select().from(assetsTable).where(and(eq(assetsTable.id, req.body.parentAssetId), eq(assetsTable.orgId, orgId))); + if (!parent) return reply.notFound(); + const [inserted] = await db.insert(acTable).values({ parentAssetId: req.body.parentAssetId, childAssetId: req.body.childAssetId, role: req.body.role, slotIndex: req.body.slotIndex ?? null }).returning(); + return reply.code(201).send(inserted); + }); + app.delete<{ Params: { id: string } }>("/:id", async (req, reply) => { + const [deleted] = await db.delete(acTable).where(eq(acTable.id, req.params.id)).returning({ id: acTable.id }); + if (!deleted) return reply.notFound(); + return reply.code(204).send(); + }); +} diff --git a/apps/api/src/routes/v1/assets.ts b/apps/api/src/routes/v1/assets.ts new file mode 100644 index 0000000..9d7bd2d --- /dev/null +++ b/apps/api/src/routes/v1/assets.ts @@ -0,0 +1,90 @@ +import type { FastifyInstance } from "fastify"; +import { eq, and } from "drizzle-orm"; +import { assets as assetsTable } from "@sankofa/schema"; + +export async function assetsRoutes(app: FastifyInstance) { + const db = app.db; + + app.get("/", async (req, reply) => { + const orgId = app.orgId(req); + const list = await db.select().from(assetsTable).where(eq(assetsTable.orgId, orgId)); + return reply.send(list); + }); + + app.get<{ Params: { id: string } }>("/:id", async (req, reply) => { + const orgId = app.orgId(req); + const [row] = await db + .select() + .from(assetsTable) + .where(and(eq(assetsTable.id, req.params.id), eq(assetsTable.orgId, orgId))); + if (!row) return reply.notFound(); + return reply.send(row); + }); + + app.post<{ + Body: { + assetId: string; + category: string; + manufacturerSerial?: string; + serviceTag?: string; + partNumber?: string; + condition?: string; + warranty?: string; + siteId?: string; + projectId?: string; + sensitivityTier?: string; + }; + }>("/", async (req, reply) => { + const orgId = app.orgId(req); + const [inserted] = await db + .insert(assetsTable) + .values({ + orgId, + assetId: req.body.assetId, + category: req.body.category, + manufacturerSerial: req.body.manufacturerSerial ?? null, + serviceTag: req.body.serviceTag ?? null, + partNumber: req.body.partNumber ?? null, + condition: req.body.condition ?? null, + warranty: req.body.warranty ?? null, + siteId: req.body.siteId ?? null, + projectId: req.body.projectId ?? null, + sensitivityTier: req.body.sensitivityTier ?? null, + }) + .returning(); + return reply.code(201).send(inserted); + }); + + app.patch<{ + Params: { id: string }; + Body: Partial<{ + assetId: string; + category: string; + status: string; + siteId: string; + positionId: string; + ownerId: string; + projectId: string; + sensitivityTier: string; + }>; + }>("/:id", async (req, reply) => { + const orgId = app.orgId(req); + const [updated] = await db + .update(assetsTable) + .set({ ...req.body, updatedAt: new Date() }) + .where(and(eq(assetsTable.id, req.params.id), eq(assetsTable.orgId, orgId))) + .returning(); + if (!updated) return reply.notFound(); + return reply.send(updated); + }); + + app.delete<{ Params: { id: string } }>("/:id", async (req, reply) => { + const orgId = app.orgId(req); + const [deleted] = await db + .delete(assetsTable) + .where(and(eq(assetsTable.id, req.params.id), eq(assetsTable.orgId, orgId))) + .returning({ id: assetsTable.id }); + if (!deleted) return reply.notFound(); + return reply.code(204).send(); + }); +} diff --git a/apps/api/src/routes/v1/auth.test.ts b/apps/api/src/routes/v1/auth.test.ts new file mode 100644 index 0000000..85f9395 --- /dev/null +++ b/apps/api/src/routes/v1/auth.test.ts @@ -0,0 +1,25 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { buildApp } from "../../index.js"; + +describe("auth", () => { + let app: Awaited>; + + beforeAll(async () => { + app = await buildApp(); + }); + + afterAll(async () => { + await app.close(); + }); + + it("POST /api/v1/auth/token with unknown email returns 401 or 500 when DB unavailable", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/v1/auth/token", + headers: { "content-type": "application/json" }, + payload: { email: "nobody@example.com" }, + }); + expect([401, 500]).toContain(res.statusCode); + expect(JSON.parse(res.payload).error).toBeDefined(); + }); +}); diff --git a/apps/api/src/routes/v1/auth.ts b/apps/api/src/routes/v1/auth.ts new file mode 100644 index 0000000..f1f439f --- /dev/null +++ b/apps/api/src/routes/v1/auth.ts @@ -0,0 +1,42 @@ +import type { FastifyInstance } from "fastify"; +import { eq, and } from "drizzle-orm"; +import { users as usersTable, userRoles, roles as rolesTable } from "@sankofa/schema"; +import type { RoleName } from "@sankofa/auth"; + +export async function authRoutes(app: FastifyInstance) { + const db = app.db; + + app.post<{ + Body: { email: string; password?: string }; + }>( + "/token", + { + schema: { + body: { + type: "object", + required: ["email"], + properties: { email: { type: "string", format: "email" }, password: { type: "string" } }, + }, + response: { 200: { type: "object", properties: { token: { type: "string" }, user: { type: "object" } } } }, + }, + }, + async (req, reply) => { + const orgId = (req.headers["x-org-id"] as string) || "default"; + const { email } = req.body; + const [user] = await db.select().from(usersTable).where(and(eq(usersTable.email, email), eq(usersTable.orgId, orgId))); + if (!user) return reply.code(401).send({ error: "Invalid email or password", code: "UNAUTHORIZED" }); + + const ur = await db.select({ roleName: rolesTable.name }).from(userRoles).innerJoin(rolesTable, eq(userRoles.roleId, rolesTable.id)).where(eq(userRoles.userId, user.id)); + const roleNames = ur.map((r) => r.roleName as RoleName).filter(Boolean); + + const token = app.jwt.sign({ + sub: user.id, + email: user.email, + roles: roleNames, + vendorId: user.vendorId ?? undefined, + orgId: user.orgId, + }); + return reply.send({ token, user: { id: user.id, email: user.email, name: user.name, roles: roleNames, vendorId: user.vendorId ?? null } }); + } + ); +} diff --git a/apps/api/src/routes/v1/capacity.ts b/apps/api/src/routes/v1/capacity.ts new file mode 100644 index 0000000..63116fe --- /dev/null +++ b/apps/api/src/routes/v1/capacity.ts @@ -0,0 +1,89 @@ +import type { FastifyInstance } from "fastify"; +import { eq, and, inArray } from "drizzle-orm"; +import { + assets as assetsTable, + sites as sitesTable, + rooms as roomsTable, + rows as rowsTable, + racks as racksTable, + positions as positionsTable, +} from "@sankofa/schema"; + +export async function capacityRoutes(app: FastifyInstance) { + const db = app.db; + + app.get<{ Params: { siteId: string } }>("/sites/:siteId", async (req, reply) => { + const orgId = app.orgId(req); + const siteId = req.params.siteId; + const [site] = await db.select().from(sitesTable).where(and(eq(sitesTable.id, siteId), eq(sitesTable.orgId, orgId))); + if (!site) return reply.notFound(); + + const rooms = await db.select({ id: roomsTable.id }).from(roomsTable).where(eq(roomsTable.siteId, siteId)); + const roomIds = rooms.map((r) => r.id); + if (roomIds.length === 0) return reply.send({ siteId, usedRu: 0, totalRu: 0, utilizationPercent: 0 }); + + const rows = await db.select({ id: rowsTable.id }).from(rowsTable).where(inArray(rowsTable.roomId, roomIds)); + const rowIds = rows.map((r) => r.id); + if (rowIds.length === 0) return reply.send({ siteId, usedRu: 0, totalRu: 0, utilizationPercent: 0 }); + + const racks = await db.select({ id: racksTable.id, ruTotal: racksTable.ruTotal }).from(racksTable).where(inArray(racksTable.rowId, rowIds)); + const totalRu = racks.reduce((sum, r) => sum + r.ruTotal, 0); + const rackIds = racks.map((r) => r.id); + if (rackIds.length === 0) return reply.send({ siteId, usedRu: 0, totalRu: 0, utilizationPercent: 0 }); + + const positions = await db.select({ id: positionsTable.id, ruStart: positionsTable.ruStart, ruEnd: positionsTable.ruEnd }).from(positionsTable).where(inArray(positionsTable.rackId, rackIds)); + const occupiedPositionIds = await db.select({ positionId: assetsTable.positionId }).from(assetsTable).where(and(eq(assetsTable.orgId, orgId), eq(assetsTable.siteId, siteId))); + const occupiedSet = new Set(occupiedPositionIds.map((a) => a.positionId).filter(Boolean)); + const usedRu = positions.filter((p) => occupiedSet.has(p.id)).reduce((sum, p) => sum + (p.ruEnd - p.ruStart + 1), 0); + const utilizationPercent = totalRu > 0 ? Math.round((usedRu / totalRu) * 100) : 0; + return reply.send({ siteId, usedRu, totalRu, utilizationPercent }); + }); + + app.get<{ Params: { siteId: string } }>("/sites/:siteId/power", async (req, reply) => { + const orgId = app.orgId(req); + const siteId = req.params.siteId; + const [site] = await db.select().from(sitesTable).where(and(eq(sitesTable.id, siteId), eq(sitesTable.orgId, orgId))); + if (!site) return reply.notFound(); + + const rooms = await db.select({ id: roomsTable.id }).from(roomsTable).where(eq(roomsTable.siteId, siteId)); + const roomIds = rooms.map((r) => r.id); + if (roomIds.length === 0) return reply.send({ siteId, circuitLimitWatts: 0, measuredDrawWatts: null, headroomWatts: null }); + + const rows = await db.select({ id: rowsTable.id }).from(rowsTable).where(inArray(rowsTable.roomId, roomIds)); + const rowIds = rows.map((r) => r.id); + if (rowIds.length === 0) return reply.send({ siteId, circuitLimitWatts: 0, measuredDrawWatts: null, headroomWatts: null }); + + const racks = await db.select({ powerFeeds: racksTable.powerFeeds }).from(racksTable).where(inArray(racksTable.rowId, rowIds)); + let circuitLimitWatts = 0; + for (const r of racks) { + const feeds = (r.powerFeeds as { circuitLimitWatts?: number }[] | null) ?? []; + for (const f of feeds) circuitLimitWatts += f.circuitLimitWatts ?? 0; + } + return reply.send({ + siteId, + circuitLimitWatts, + measuredDrawWatts: null, + headroomWatts: null, + }); + }); + + app.get("/gpu-inventory", async (req, reply) => { + const orgId = app.orgId(req); + const list = await db.select({ + id: assetsTable.id, + assetId: assetsTable.assetId, + siteId: assetsTable.siteId, + status: assetsTable.status, + partNumber: assetsTable.partNumber, + }).from(assetsTable).where(and(eq(assetsTable.orgId, orgId), eq(assetsTable.category, "gpu"))); + const bySite: Record = {}; + const byType: Record = {}; + for (const a of list) { + const sid = a.siteId ?? "unassigned"; + bySite[sid] = (bySite[sid] ?? 0) + 1; + const typeKey = a.partNumber ?? "unknown"; + byType[typeKey] = (byType[typeKey] ?? 0) + 1; + } + return reply.send({ total: list.length, bySite, byType }); + }); +} diff --git a/apps/api/src/routes/v1/compliance-profiles.ts b/apps/api/src/routes/v1/compliance-profiles.ts new file mode 100644 index 0000000..eb63c81 --- /dev/null +++ b/apps/api/src/routes/v1/compliance-profiles.ts @@ -0,0 +1,77 @@ +import type { FastifyInstance } from "fastify"; +import { eq, and } from "drizzle-orm"; +import { complianceProfiles as profilesTable } from "@sankofa/schema"; + +export async function complianceProfilesRoutes(app: FastifyInstance) { + const db = app.db; + + app.get("/", async (req, reply) => { + const orgId = app.orgId(req); + const list = await db.select().from(profilesTable).where(eq(profilesTable.orgId, orgId)); + return reply.send(list); + }); + + app.get<{ Params: { id: string } }>("/:id", async (req, reply) => { + const orgId = app.orgId(req); + const [row] = await db + .select() + .from(profilesTable) + .where(and(eq(profilesTable.id, req.params.id), eq(profilesTable.orgId, orgId))); + if (!row) return reply.notFound(); + return reply.send(row); + }); + + app.post<{ + Body: { + name: string; + firmwareFreezePolicy?: { lockedVersion?: string; minVersion?: string; maxVersion?: string }; + allowedGenerations?: string[]; + approvedSkus?: string[]; + siteId?: string; + }; + }>("/", async (req, reply) => { + const orgId = app.orgId(req); + const [inserted] = await db + .insert(profilesTable) + .values({ + orgId, + name: req.body.name, + firmwareFreezePolicy: req.body.firmwareFreezePolicy ?? null, + allowedGenerations: req.body.allowedGenerations ?? null, + approvedSkus: req.body.approvedSkus ?? null, + siteId: req.body.siteId ?? null, + }) + .returning(); + return reply.code(201).send(inserted); + }); + + app.patch<{ + Params: { id: string }; + Body: Partial<{ + name: string; + firmwareFreezePolicy: { lockedVersion?: string; minVersion?: string; maxVersion?: string }; + allowedGenerations: string[]; + approvedSkus: string[]; + siteId: string; + }>; + }>("/:id", async (req, reply) => { + const orgId = app.orgId(req); + const [updated] = await db + .update(profilesTable) + .set({ ...req.body, updatedAt: new Date() }) + .where(and(eq(profilesTable.id, req.params.id), eq(profilesTable.orgId, orgId))) + .returning(); + if (!updated) return reply.notFound(); + return reply.send(updated); + }); + + app.delete<{ Params: { id: string } }>("/:id", async (req, reply) => { + const orgId = app.orgId(req); + const [deleted] = await db + .delete(profilesTable) + .where(and(eq(profilesTable.id, req.params.id), eq(profilesTable.orgId, orgId))) + .returning({ id: profilesTable.id }); + if (!deleted) return reply.notFound(); + return reply.code(204).send(); + }); +} diff --git a/apps/api/src/routes/v1/index.ts b/apps/api/src/routes/v1/index.ts new file mode 100644 index 0000000..1ec8c1e --- /dev/null +++ b/apps/api/src/routes/v1/index.ts @@ -0,0 +1,44 @@ +import type { FastifyInstance } from "fastify"; +import { authRoutes } from "./auth"; +import { vendorsRoutes } from "./vendors"; +import { offersRoutes } from "./offers"; +import { usersRoutes } from "./users"; +import { rolesRoutes } from "./roles"; +import { purchaseOrdersRoutes } from "./purchase-orders"; +import { assetsRoutes } from "./assets"; +import { sitesRoutes } from "./sites"; +import { uploadRoutes } from "./upload"; +import { workflowRoutes } from "./workflow"; +import { inspectionRoutes } from "./inspection"; +import { shipmentsRoutes } from "./shipments"; +import { assetComponentsRoutes } from "./asset-components"; +import { capacityRoutes } from "./capacity"; +import { integrationsRoutes } from "./integrations"; +import { maintenancesRoutes } from "./maintenances"; +import { complianceProfilesRoutes } from "./compliance-profiles"; +import { unifiControllersRoutes } from "./unifi-controllers"; +import { reportsRoutes } from "./reports"; +import { ingestionRoutes } from "./ingestion"; + +export async function registerV1Routes(app: FastifyInstance) { + await app.register(authRoutes, { prefix: "/auth" }); + await app.register(vendorsRoutes, { prefix: "/vendors" }); + await app.register(offersRoutes, { prefix: "/offers" }); + await app.register(usersRoutes, { prefix: "/users" }); + await app.register(rolesRoutes, { prefix: "/roles" }); + await app.register(purchaseOrdersRoutes, { prefix: "/purchase-orders" }); + await app.register(assetsRoutes, { prefix: "/assets" }); + await app.register(sitesRoutes, { prefix: "/sites" }); + await app.register(uploadRoutes, { prefix: "/upload" }); + await app.register(workflowRoutes, { prefix: "/workflow" }); + await app.register(inspectionRoutes, { prefix: "/inspection" }); + await app.register(shipmentsRoutes, { prefix: "/shipments" }); + await app.register(assetComponentsRoutes, { prefix: "/asset-components" }); + await app.register(capacityRoutes, { prefix: "/capacity" }); + await app.register(integrationsRoutes, { prefix: "/integrations" }); + await app.register(maintenancesRoutes, { prefix: "/maintenances" }); + await app.register(complianceProfilesRoutes, { prefix: "/compliance-profiles" }); + await app.register(unifiControllersRoutes, { prefix: "/unifi-controllers" }); + await app.register(reportsRoutes, { prefix: "/reports" }); + await app.register(ingestionRoutes, { prefix: "/ingestion" }); +} diff --git a/apps/api/src/routes/v1/ingestion.test.ts b/apps/api/src/routes/v1/ingestion.test.ts new file mode 100644 index 0000000..3aa364f --- /dev/null +++ b/apps/api/src/routes/v1/ingestion.test.ts @@ -0,0 +1,35 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { buildApp } from "../../index.js"; + +describe("ingestion", () => { + let app: Awaited>; + + beforeAll(async () => { + app = await buildApp(); + }); + + afterAll(async () => { + await app.close(); + }); + + it("POST /api/v1/ingestion/offers returns 401 without x-ingestion-api-key", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/v1/ingestion/offers", + headers: { "content-type": "application/json" }, + payload: { source: "email", quantity: 1, unit_price: "1" }, + }); + expect(res.statusCode).toBe(401); + expect(JSON.parse(res.payload).error).toContain("ingestion API key"); + }); + + it("POST /api/v1/ingestion/offers returns 401 with wrong x-ingestion-api-key", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/v1/ingestion/offers", + headers: { "content-type": "application/json", "x-ingestion-api-key": "wrong" }, + payload: { source: "email", quantity: 1, unit_price: "1" }, + }); + expect(res.statusCode).toBe(401); + }); +}); diff --git a/apps/api/src/routes/v1/ingestion.ts b/apps/api/src/routes/v1/ingestion.ts new file mode 100644 index 0000000..855eb4d --- /dev/null +++ b/apps/api/src/routes/v1/ingestion.ts @@ -0,0 +1,60 @@ +import type { FastifyInstance } from "fastify"; +import { offers as offersTable } from "@sankofa/schema"; + +const INGESTION_KEY = process.env.INGESTION_API_KEY; + +export async function ingestionRoutes(app: FastifyInstance) { + const db = app.db; + + app.addHook("preHandler", async (req, reply) => { + const key = (req.headers["x-ingestion-api-key"] as string) || ""; + if (!INGESTION_KEY || key !== INGESTION_KEY) { + return reply.code(401).send({ error: "Invalid or missing ingestion API key" }); + } + }); + + app.post<{ + Body: { + source: "scraped" | "email"; + source_ref?: string; + source_metadata?: Record; + vendor_id?: string | null; + sku?: string; + mpn?: string; + quantity: number; + unit_price: string; + incoterms?: string; + lead_time_days?: number; + country_of_origin?: string; + condition?: string; + warranty?: string; + evidence_refs?: { key: string; hash?: string }[]; + }; + }>("/offers", async (req, reply) => { + const orgId = (req.headers["x-org-id"] as string) || "default"; + const body = req.body; + const now = new Date(); + const [inserted] = await db + .insert(offersTable) + .values({ + orgId, + vendorId: body.vendor_id ?? null, + sku: body.sku ?? null, + mpn: body.mpn ?? null, + quantity: body.quantity, + unitPrice: body.unit_price, + incoterms: body.incoterms ?? null, + leadTimeDays: body.lead_time_days ?? null, + countryOfOrigin: body.country_of_origin ?? null, + condition: body.condition ?? null, + warranty: body.warranty ?? null, + evidenceRefs: body.evidence_refs ?? null, + source: body.source, + sourceRef: body.source_ref ?? null, + sourceMetadata: body.source_metadata ?? null, + ingestedAt: now, + }) + .returning(); + return reply.code(201).send(inserted); + }); +} diff --git a/apps/api/src/routes/v1/inspection.ts b/apps/api/src/routes/v1/inspection.ts new file mode 100644 index 0000000..252209d --- /dev/null +++ b/apps/api/src/routes/v1/inspection.ts @@ -0,0 +1,47 @@ +import type { FastifyInstance } from "fastify"; +import { eq, and } from "drizzle-orm"; +import { inspectionTemplates as tplTable, inspectionRuns as runsTable } from "@sankofa/schema"; + +export async function inspectionRoutes(app: FastifyInstance) { + const db = app.db; + + app.get("/templates", async (req, reply) => { + const orgId = app.orgId(req); + const list = await db.select().from(tplTable).where(eq(tplTable.orgId, orgId)); + return reply.send(list); + }); + + app.post<{ Body: { category: string; name: string; steps: { id: string; label: string; required?: boolean }[] } }>("/templates", async (req, reply) => { + const orgId = app.orgId(req); + const [inserted] = await db.insert(tplTable).values({ orgId, category: req.body.category, name: req.body.name, steps: req.body.steps }).returning(); + return reply.code(201).send(inserted); + }); + + app.get("/runs", async (req, reply) => { + const orgId = app.orgId(req); + const list = await db.select().from(runsTable).where(eq(runsTable.orgId, orgId)); + return reply.send(list); + }); + + app.post<{ Body: { templateId: string; offerId?: string; assetId?: string } }>("/runs", async (req, reply) => { + const orgId = app.orgId(req); + const [inserted] = await db.insert(runsTable).values({ + orgId, + templateId: req.body.templateId, + offerId: req.body.offerId ?? null, + assetId: req.body.assetId ?? null, + }).returning(); + return reply.code(201).send(inserted); + }); + + app.patch<{ Params: { id: string }; Body: { status?: string; evidenceRefs?: { key: string; hash?: string }[]; resultNotes?: string } }>("/runs/:id", async (req, reply) => { + const orgId = app.orgId(req); + const [updated] = await db.update(runsTable).set({ + ...req.body, + completedAt: req.body.status === "pass" || req.body.status === "fail" ? new Date() : undefined, + updatedAt: new Date(), + }).where(and(eq(runsTable.id, req.params.id), eq(runsTable.orgId, orgId))).returning(); + if (!updated) return reply.notFound(); + return reply.send(updated); + }); +} diff --git a/apps/api/src/routes/v1/integrations.ts b/apps/api/src/routes/v1/integrations.ts new file mode 100644 index 0000000..6e336a0 --- /dev/null +++ b/apps/api/src/routes/v1/integrations.ts @@ -0,0 +1,50 @@ +import type { FastifyInstance } from "fastify"; +import { eq, and } from "drizzle-orm"; +import { integrationMappings as mappingsTable, unifiProductCatalog as catalogTable } from "@sankofa/schema"; +import { listUnifiDevices, enrichDevicesWithCatalog } from "../../integrations/unifi.js"; +import { listProxmoxNodes } from "../../integrations/proxmox.js"; + +export async function integrationsRoutes(app: FastifyInstance) { + const db = app.db; + app.get<{ Params: { siteId: string } }>("/unifi/sites/:siteId/devices", async (req, reply) => { + const token = (req.headers["x-unifi-token"] as string) || ""; + const baseUrl = (req.headers["x-unifi-url"] as string) || ""; + const devices = await listUnifiDevices(baseUrl, token); + const catalog = await db.select().from(catalogTable); + const enriched = enrichDevicesWithCatalog(devices, catalog); + return reply.send(enriched); + }); + app.get<{ Querystring: { generation?: string; approved_sovereign?: string } }>("/unifi/product-catalog", async (req, reply) => { + const gen = (req.query as { generation?: string }).generation; + const approved = (req.query as { approved_sovereign?: string }).approved_sovereign; + const conditions = [ + ...(gen ? [eq(catalogTable.generation, gen)] : []), + ...(approved === "true" ? [eq(catalogTable.approvedSovereignDefault, true)] : []), + ]; + const list = conditions.length + ? await db.select().from(catalogTable).where(and(...conditions)) + : await db.select().from(catalogTable); + return reply.send(list); + }); + app.get<{ Params: { sku: string } }>("/unifi/product-catalog/:sku", async (req, reply) => { + const [row] = await db.select().from(catalogTable).where(eq(catalogTable.sku, req.params.sku)); + if (!row) return reply.notFound(); + return reply.send(row); + }); + app.get<{ Params: { siteId: string } }>("/proxmox/sites/:siteId/nodes", async (req, reply) => { + const token = (req.headers["x-proxmox-token"] as string) || ""; + const baseUrl = (req.headers["x-proxmox-url"] as string) || ""; + const nodes = await listProxmoxNodes(baseUrl, token); + return reply.send(nodes); + }); + app.get("/mappings", async (req, reply) => { + const orgId = app.orgId(req); + const list = await db.select().from(mappingsTable).where(eq(mappingsTable.orgId, orgId)); + return reply.send(list); + }); + app.post<{ Body: { assetId?: string; siteId?: string; provider: string; externalId: string } }>("/mappings", async (req, reply) => { + const orgId = app.orgId(req); + const [inserted] = await db.insert(mappingsTable).values({ orgId, assetId: req.body.assetId ?? null, siteId: req.body.siteId ?? null, provider: req.body.provider, externalId: req.body.externalId }).returning(); + return reply.code(201).send(inserted); + }); +} diff --git a/apps/api/src/routes/v1/maintenances.ts b/apps/api/src/routes/v1/maintenances.ts new file mode 100644 index 0000000..64ff15e --- /dev/null +++ b/apps/api/src/routes/v1/maintenances.ts @@ -0,0 +1,29 @@ +import type { FastifyInstance } from "fastify"; +import { eq, and } from "drizzle-orm"; +import { maintenances as maintenancesTable } from "@sankofa/schema"; + +export async function maintenancesRoutes(app: FastifyInstance) { + const db = app.db; + app.get("/", async (req, reply) => { + const orgId = app.orgId(req); + const list = await db.select().from(maintenancesTable).where(eq(maintenancesTable.orgId, orgId)); + return reply.send(list); + }); + app.get<{ Params: { id: string } }>("/:id", async (req, reply) => { + const orgId = app.orgId(req); + const [row] = await db.select().from(maintenancesTable).where(and(eq(maintenancesTable.id, req.params.id), eq(maintenancesTable.orgId, orgId))); + if (!row) return reply.notFound(); + return reply.send(row); + }); + app.post<{ Body: { assetId: string; type: string; vendorTicketRef?: string; description?: string } }>("/", async (req, reply) => { + const orgId = app.orgId(req); + const [inserted] = await db.insert(maintenancesTable).values({ orgId, assetId: req.body.assetId, type: req.body.type, vendorTicketRef: req.body.vendorTicketRef ?? null, description: req.body.description ?? null }).returning(); + return reply.code(201).send(inserted); + }); + app.patch<{ Params: { id: string }; Body: { status?: string } }>("/:id", async (req, reply) => { + const orgId = app.orgId(req); + const [updated] = await db.update(maintenancesTable).set({ ...req.body, updatedAt: new Date() }).where(and(eq(maintenancesTable.id, req.params.id), eq(maintenancesTable.orgId, orgId))).returning(); + if (!updated) return reply.notFound(); + return reply.send(updated); + }); +} diff --git a/apps/api/src/routes/v1/offers.ts b/apps/api/src/routes/v1/offers.ts new file mode 100644 index 0000000..e4b24d5 --- /dev/null +++ b/apps/api/src/routes/v1/offers.ts @@ -0,0 +1,57 @@ +import type { FastifyInstance } from "fastify"; +import { eq, and, sql } from "drizzle-orm"; +import { offers as offersTable } from "@sankofa/schema"; + +export async function offersRoutes(app: FastifyInstance) { + const db = app.db; + const listSchema = { querystring: { type: "object", properties: { limit: { type: "integer" }, offset: { type: "integer" } } } }; + app.get("/", { schema: listSchema }, async (req, reply) => { + const orgId = app.orgId(req); + const vid = app.vendorId(req); + const limit = Math.min(Number((req.query as { limit?: number }).limit) || 50, 100); + const offset = Number((req.query as { offset?: number }).offset) || 0; + const conditions = [eq(offersTable.orgId, orgId)] as ReturnType[]; + if (vid) conditions.push(eq(offersTable.vendorId, vid)); + const list = await db.select().from(offersTable).where(and(...conditions)).limit(limit).offset(offset); + const [{ total }] = await db.select({ total: sql`count(*)::int` }).from(offersTable).where(and(...conditions)); + return reply.send({ data: list, total }); + }); + app.get<{ Params: { id: string } }>("/:id", async (req, reply) => { + const orgId = app.orgId(req); + const vid = app.vendorId(req); + const conditions = [eq(offersTable.id, req.params.id), eq(offersTable.orgId, orgId)] as ReturnType[]; + if (vid) conditions.push(eq(offersTable.vendorId, vid)); + const [row] = await db.select().from(offersTable).where(and(...conditions)); + if (!row) return reply.notFound(); + return reply.send(row); + }); + app.post<{ Body: { vendorId?: string; sku?: string; mpn?: string; quantity: number; unitPrice: string; incoterms?: string; leadTimeDays?: number; countryOfOrigin?: string; condition?: string; warranty?: string; evidenceRefs?: { key: string; hash?: string }[] } }>("/", async (req, reply) => { + const orgId = app.orgId(req); + const vid = app.vendorId(req); + const vendorId = vid ?? req.body.vendorId ?? null; + if (!vendorId) throw app.httpErrors.badRequest("vendorId required (or login as vendor user)"); + const [inserted] = await db.insert(offersTable).values({ + orgId, vendorId, sku: req.body.sku ?? null, mpn: req.body.mpn ?? null, quantity: req.body.quantity, unitPrice: req.body.unitPrice, + incoterms: req.body.incoterms ?? null, leadTimeDays: req.body.leadTimeDays ?? null, countryOfOrigin: req.body.countryOfOrigin ?? null, condition: req.body.condition ?? null, warranty: req.body.warranty ?? null, evidenceRefs: req.body.evidenceRefs ?? null, + }).returning(); + return reply.code(201).send(inserted); + }); + app.patch<{ Params: { id: string }; Body: Partial<{ sku: string; mpn: string; quantity: number; unitPrice: string; status: string; evidenceRefs: { key: string; hash?: string }[] }> }>("/:id", async (req, reply) => { + const orgId = app.orgId(req); + const vid = app.vendorId(req); + const conditions = [eq(offersTable.id, req.params.id), eq(offersTable.orgId, orgId)] as ReturnType[]; + if (vid) conditions.push(eq(offersTable.vendorId, vid)); + const [updated] = await db.update(offersTable).set({ ...req.body, updatedAt: new Date() }).where(and(...conditions)).returning(); + if (!updated) return reply.notFound(); + return reply.send(updated); + }); + app.delete<{ Params: { id: string } }>("/:id", async (req, reply) => { + const orgId = app.orgId(req); + const vid = app.vendorId(req); + const conditions = [eq(offersTable.id, req.params.id), eq(offersTable.orgId, orgId)] as ReturnType[]; + if (vid) conditions.push(eq(offersTable.vendorId, vid)); + const [deleted] = await db.delete(offersTable).where(and(...conditions)).returning({ id: offersTable.id }); + if (!deleted) return reply.notFound(); + return reply.code(204).send(); + }); +} diff --git a/apps/api/src/routes/v1/purchase-orders.ts b/apps/api/src/routes/v1/purchase-orders.ts new file mode 100644 index 0000000..daf6ac2 --- /dev/null +++ b/apps/api/src/routes/v1/purchase-orders.ts @@ -0,0 +1,74 @@ +import type { FastifyInstance } from "fastify"; +import { eq, and } from "drizzle-orm"; +import { purchaseOrders as poTable } from "@sankofa/schema"; + +export async function purchaseOrdersRoutes(app: FastifyInstance) { + const db = app.db; + + app.get("/", async (req, reply) => { + const orgId = app.orgId(req); + const vid = app.vendorId(req); + const conditions = [eq(poTable.orgId, orgId)] as ReturnType[]; + if (vid) conditions.push(eq(poTable.vendorId, vid)); + const list = await db.select().from(poTable).where(and(...conditions)); + return reply.send(list); + }); + + app.get<{ Params: { id: string } }>("/:id", async (req, reply) => { + const orgId = app.orgId(req); + const vid = app.vendorId(req); + const conditions = [eq(poTable.id, req.params.id), eq(poTable.orgId, orgId)] as ReturnType[]; + if (vid) conditions.push(eq(poTable.vendorId, vid)); + const [row] = await db.select().from(poTable).where(and(...conditions)); + if (!row) return reply.notFound(); + return reply.send(row); + }); + + app.post<{ + Body: { + vendorId: string; + lineItems: { offerId?: string; sku?: string; quantity: number; unitPrice: string }[]; + escrowTerms?: string; + inspectionSiteId?: string; + deliverySiteId?: string; + }; + }>("/", async (req, reply) => { + const orgId = app.orgId(req); + const [inserted] = await db + .insert(poTable) + .values({ + orgId, + vendorId: req.body.vendorId, + lineItems: req.body.lineItems, + escrowTerms: req.body.escrowTerms ?? null, + inspectionSiteId: req.body.inspectionSiteId ?? null, + deliverySiteId: req.body.deliverySiteId ?? null, + }) + .returning(); + return reply.code(201).send(inserted); + }); + + app.patch<{ + Params: { id: string }; + Body: Partial<{ status: string; approvalStage: string; escrowTerms: string }>; + }>("/:id", async (req, reply) => { + const orgId = app.orgId(req); + const [updated] = await db + .update(poTable) + .set({ ...req.body, updatedAt: new Date() }) + .where(and(eq(poTable.id, req.params.id), eq(poTable.orgId, orgId))) + .returning(); + if (!updated) return reply.notFound(); + return reply.send(updated); + }); + + app.delete<{ Params: { id: string } }>("/:id", async (req, reply) => { + const orgId = app.orgId(req); + const [deleted] = await db + .delete(poTable) + .where(and(eq(poTable.id, req.params.id), eq(poTable.orgId, orgId))) + .returning({ id: poTable.id }); + if (!deleted) return reply.notFound(); + return reply.code(204).send(); + }); +} diff --git a/apps/api/src/routes/v1/reports.ts b/apps/api/src/routes/v1/reports.ts new file mode 100644 index 0000000..f039bef --- /dev/null +++ b/apps/api/src/routes/v1/reports.ts @@ -0,0 +1,46 @@ +import type { FastifyInstance } from "fastify"; +import { eq, and } from "drizzle-orm"; +import { assets as assetsTable, integrationMappings as mappingsTable, unifiProductCatalog as catalogTable } from "@sankofa/schema"; + +export async function reportsRoutes(app: FastifyInstance) { + const db = app.db; + + app.get<{ Querystring: { org_id?: string; site_id?: string } }>("/bom", async (req, reply) => { + const orgId = (req.query as { org_id?: string }).org_id ?? app.orgId(req); + const siteId = (req.query as { site_id?: string }).site_id; + const assetList = siteId + ? await db.select().from(assetsTable).where(and(eq(assetsTable.orgId, orgId), eq(assetsTable.siteId, siteId))) + : await db.select().from(assetsTable).where(eq(assetsTable.orgId, orgId)); + const mappings = await db.select().from(mappingsTable).where(eq(mappingsTable.orgId, orgId)); + const catalog = await db.select().from(catalogTable); + const items = assetList.map((a) => { + const mapping = mappings.find((m) => m.assetId === a.id && m.provider === "unifi"); + const catalogEntry = mapping ? catalog.find((c) => c.sku === mapping.externalId || c.modelName === mapping.externalId) : null; + return { + assetId: a.assetId, + category: a.category, + siteId: a.siteId, + catalogSku: catalogEntry?.sku, + generation: catalogEntry?.generation, + supportHorizon: catalogEntry?.supportHorizon, + }; + }); + return reply.send({ orgId, siteId: siteId ?? null, items }); + }); + + app.get<{ Querystring: { org_id?: string; horizon_months?: string } }>("/support-risk", async (req, reply) => { + const orgId = (req.query as { org_id?: string }).org_id ?? app.orgId(req); + const horizonMonths = Math.min(24, Math.max(1, parseInt((req.query as { horizon_months?: string }).horizon_months ?? "12", 10) || 12)); + const catalog = await db.select().from(catalogTable); + const mappings = await db.select().from(mappingsTable).where(and(eq(mappingsTable.orgId, orgId), eq(mappingsTable.provider, "unifi"))); + const cutoff = new Date(); + cutoff.setMonth(cutoff.getMonth() + horizonMonths); + const atRisk = catalog.filter((c) => { + if (!c.eolDate) return false; + const eol = new Date(c.eolDate); + return eol <= cutoff; + }); + const bySku = atRisk.map((c) => ({ sku: c.sku, modelName: c.modelName, generation: c.generation, eolDate: c.eolDate, supportHorizon: c.supportHorizon })); + return reply.send({ orgId, horizonMonths, atRisk: bySku, deviceCount: mappings.length }); + }); +} diff --git a/apps/api/src/routes/v1/roles.ts b/apps/api/src/routes/v1/roles.ts new file mode 100644 index 0000000..331190f --- /dev/null +++ b/apps/api/src/routes/v1/roles.ts @@ -0,0 +1,43 @@ +import type { FastifyInstance } from "fastify"; +import { eq } from "drizzle-orm"; +import { roles as rolesTable } from "@sankofa/schema"; + +export async function rolesRoutes(app: FastifyInstance) { + const db = app.db; + + app.get("/", async (_req, reply) => { + const list = await db.select().from(rolesTable); + return reply.send(list); + }); + + app.get<{ Params: { id: string } }>("/:id", async (req, reply) => { + const [row] = await db.select().from(rolesTable).where(eq(rolesTable.id, req.params.id)); + if (!row) return reply.notFound(); + return reply.send(row); + }); + + app.post<{ Body: { name: string; description?: string; permissions?: string[] } }>( + "/", + { schema: { body: { type: "object", required: ["name"], properties: { name: { type: "string" }, description: { type: "string" }, permissions: { type: "array", items: { type: "string" } } } } } }, + async (req, reply) => { + const [inserted] = await db.insert(rolesTable).values({ + name: req.body.name, + description: req.body.description ?? null, + permissions: req.body.permissions ?? [], + }).returning(); + return reply.code(201).send(inserted); + } + ); + + app.patch<{ Params: { id: string }; Body: Partial<{ name: string; description: string; permissions: string[] }> }>("/:id", async (req, reply) => { + const [updated] = await db.update(rolesTable).set({ ...req.body, updatedAt: new Date() }).where(eq(rolesTable.id, req.params.id)).returning(); + if (!updated) return reply.notFound(); + return reply.send(updated); + }); + + app.delete<{ Params: { id: string } }>("/:id", async (req, reply) => { + const [deleted] = await db.delete(rolesTable).where(eq(rolesTable.id, req.params.id)).returning({ id: rolesTable.id }); + if (!deleted) return reply.notFound(); + return reply.code(204).send(); + }); +} diff --git a/apps/api/src/routes/v1/shipments.ts b/apps/api/src/routes/v1/shipments.ts new file mode 100644 index 0000000..13e23ac --- /dev/null +++ b/apps/api/src/routes/v1/shipments.ts @@ -0,0 +1,39 @@ +import type { FastifyInstance } from "fastify"; +import { eq, and } from "drizzle-orm"; +import { shipments as shipmentsTable, assets as assetsTable } from "@sankofa/schema"; + +export async function shipmentsRoutes(app: FastifyInstance) { + const db = app.db; + app.get("/", async (req, reply) => { + const orgId = app.orgId(req); + const list = await db.select().from(shipmentsTable).where(eq(shipmentsTable.orgId, orgId)); + return reply.send(list); + }); + app.get<{ Params: { id: string } }>("/:id", async (req, reply) => { + const orgId = app.orgId(req); + const [row] = await db.select().from(shipmentsTable).where(and(eq(shipmentsTable.id, req.params.id), eq(shipmentsTable.orgId, orgId))); + if (!row) return reply.notFound(); + return reply.send(row); + }); + app.post<{ Body: { purchaseOrderId: string; tracking?: string } }>("/", async (req, reply) => { + const orgId = app.orgId(req); + const [inserted] = await db.insert(shipmentsTable).values({ orgId, purchaseOrderId: req.body.purchaseOrderId, tracking: req.body.tracking ?? null }).returning(); + return reply.code(201).send(inserted); + }); + app.patch<{ Params: { id: string }; Body: { tracking?: string; status?: string } }>("/:id", async (req, reply) => { + const orgId = app.orgId(req); + const [updated] = await db.update(shipmentsTable).set({ ...req.body, updatedAt: new Date() }).where(and(eq(shipmentsTable.id, req.params.id), eq(shipmentsTable.orgId, orgId))).returning(); + if (!updated) return reply.notFound(); + return reply.send(updated); + }); + app.post<{ Params: { id: string }; Body: { assetIds: string[] } }>("/:id/receive", async (req, reply) => { + const orgId = app.orgId(req); + const [shipment] = await db.select().from(shipmentsTable).where(and(eq(shipmentsTable.id, req.params.id), eq(shipmentsTable.orgId, orgId))); + if (!shipment) return reply.notFound(); + for (const assetId of req.body.assetIds || []) { + await db.update(assetsTable).set({ status: "received", updatedAt: new Date() }).where(and(eq(assetsTable.id, assetId), eq(assetsTable.orgId, orgId))); + } + await db.update(shipmentsTable).set({ status: "received", updatedAt: new Date() }).where(and(eq(shipmentsTable.id, req.params.id), eq(shipmentsTable.orgId, orgId))); + return reply.send({ status: "received" }); + }); +} diff --git a/apps/api/src/routes/v1/sites.ts b/apps/api/src/routes/v1/sites.ts new file mode 100644 index 0000000..843ce1b --- /dev/null +++ b/apps/api/src/routes/v1/sites.ts @@ -0,0 +1,68 @@ +import type { FastifyInstance } from "fastify"; +import { eq, and } from "drizzle-orm"; +import { sites as sitesTable, racks as racksTable, positions as positionsTable, rows as rowsTable, rooms as roomsTable } from "@sankofa/schema"; + +export async function sitesRoutes(app: FastifyInstance) { + const db = app.db; + + app.get("/", async (req, reply) => { + const orgId = app.orgId(req); + const list = await db.select().from(sitesTable).where(eq(sitesTable.orgId, orgId)); + return reply.send(list); + }); + + app.get<{ Params: { id: string } }>("/:id", async (req, reply) => { + const orgId = app.orgId(req); + const [row] = await db.select().from(sitesTable).where(and(eq(sitesTable.id, req.params.id), eq(sitesTable.orgId, orgId))); + if (!row) return reply.notFound(); + return reply.send(row); + }); + + app.post<{ Body: { name: string; regionId?: string; address?: string; networkMetadata?: unknown } }>("/", async (req, reply) => { + const orgId = app.orgId(req); + const [inserted] = await db.insert(sitesTable).values({ + orgId, name: req.body.name, regionId: req.body.regionId ?? null, address: req.body.address ?? null, networkMetadata: req.body.networkMetadata ?? null, + }).returning(); + return reply.code(201).send(inserted); + }); + + app.patch<{ Params: { id: string }; Body: Partial<{ name: string; address: string; networkMetadata: unknown }> }>("/:id", async (req, reply) => { + const orgId = app.orgId(req); + const [updated] = await db.update(sitesTable).set({ + name: req.body.name, + address: req.body.address, + networkMetadata: req.body.networkMetadata as { uplinks?: string[]; vlans?: string[]; portProfiles?: string[]; ipRanges?: string[] } | undefined, + updatedAt: new Date(), + }).where(and(eq(sitesTable.id, req.params.id), eq(sitesTable.orgId, orgId))).returning(); + if (!updated) return reply.notFound(); + return reply.send(updated); + }); + + app.delete<{ Params: { id: string } }>("/:id", async (req, reply) => { + const orgId = app.orgId(req); + const [deleted] = await db.delete(sitesTable).where(and(eq(sitesTable.id, req.params.id), eq(sitesTable.orgId, orgId))).returning({ id: sitesTable.id }); + if (!deleted) return reply.notFound(); + return reply.code(204).send(); + }); + + app.get<{ Params: { siteId: string } }>("/:siteId/racks", async (req, reply) => { + const orgId = app.orgId(req); + const [site] = await db.select().from(sitesTable).where(and(eq(sitesTable.id, req.params.siteId), eq(sitesTable.orgId, orgId))); + if (!site) return reply.notFound(); + const roomsList = await db.select().from(roomsTable).where(eq(roomsTable.siteId, req.params.siteId)); + const rowsList: { id: string }[] = []; + for (const r of roomsList) { + const rRows = await db.select({ id: rowsTable.id }).from(rowsTable).where(eq(rowsTable.roomId, r.id)); + rowsList.push(...rRows); + } + const rackList: unknown[] = []; + for (const row of rowsList) { + const rRacks = await db.select().from(racksTable).where(eq(racksTable.rowId, row.id)); + for (const rack of rRacks) { + const posList = await db.select().from(positionsTable).where(eq(positionsTable.rackId, rack.id)); + rackList.push({ ...rack, positions: posList }); + } + } + return reply.send(rackList); + }); +} diff --git a/apps/api/src/routes/v1/unifi-controllers.ts b/apps/api/src/routes/v1/unifi-controllers.ts new file mode 100644 index 0000000..3199849 --- /dev/null +++ b/apps/api/src/routes/v1/unifi-controllers.ts @@ -0,0 +1,59 @@ +import type { FastifyInstance } from "fastify"; +import { eq, and } from "drizzle-orm"; +import { unifiControllers as controllersTable } from "@sankofa/schema"; + +export async function unifiControllersRoutes(app: FastifyInstance) { + const db = app.db; + + app.get("/", async (req, reply) => { + const orgId = app.orgId(req); + const list = await db.select().from(controllersTable).where(eq(controllersTable.orgId, orgId)); + return reply.send(list); + }); + + app.get<{ Params: { id: string } }>("/:id", async (req, reply) => { + const orgId = app.orgId(req); + const [row] = await db + .select() + .from(controllersTable) + .where(and(eq(controllersTable.id, req.params.id), eq(controllersTable.orgId, orgId))); + if (!row) return reply.notFound(); + return reply.send(row); + }); + + app.post<{ Body: { siteId?: string; baseUrl: string; role: string; region?: string } }>("/", async (req, reply) => { + const orgId = app.orgId(req); + const [inserted] = await db + .insert(controllersTable) + .values({ + orgId, + siteId: req.body.siteId ?? null, + baseUrl: req.body.baseUrl, + role: req.body.role, + region: req.body.region ?? null, + }) + .returning(); + return reply.code(201).send(inserted); + }); + + app.patch<{ Params: { id: string }; Body: Partial<{ baseUrl: string; role: string; region: string }> }>("/:id", async (req, reply) => { + const orgId = app.orgId(req); + const [updated] = await db + .update(controllersTable) + .set({ ...req.body, updatedAt: new Date() }) + .where(and(eq(controllersTable.id, req.params.id), eq(controllersTable.orgId, orgId))) + .returning(); + if (!updated) return reply.notFound(); + return reply.send(updated); + }); + + app.delete<{ Params: { id: string } }>("/:id", async (req, reply) => { + const orgId = app.orgId(req); + const [deleted] = await db + .delete(controllersTable) + .where(and(eq(controllersTable.id, req.params.id), eq(controllersTable.orgId, orgId))) + .returning({ id: controllersTable.id }); + if (!deleted) return reply.notFound(); + return reply.code(204).send(); + }); +} diff --git a/apps/api/src/routes/v1/upload.ts b/apps/api/src/routes/v1/upload.ts new file mode 100644 index 0000000..5676fa5 --- /dev/null +++ b/apps/api/src/routes/v1/upload.ts @@ -0,0 +1,18 @@ +import type { FastifyInstance } from "fastify"; +import { uploadDocument } from "../../storage"; +import { randomUUID } from "crypto"; + +export async function uploadRoutes(app: FastifyInstance) { + app.post<{ Querystring: { prefix?: string } }>("/", async (req, reply) => { + const data = await req.file(); + if (!data) return reply.badRequest("No file"); + const buf = await data.toBuffer(); + const prefix = (req.query as { prefix?: string }).prefix ?? "documents"; + const key = `${prefix}/${randomUUID()}/${data.filename}`; + const result = await uploadDocument(key, buf, data.mimetype || "application/octet-stream", { + originalName: data.filename, + orgId: app.orgId(req), + }); + return reply.send({ key: result.key, bucket: result.bucket, etag: result.etag }); + }); +} diff --git a/apps/api/src/routes/v1/users.ts b/apps/api/src/routes/v1/users.ts new file mode 100644 index 0000000..6a22b1a --- /dev/null +++ b/apps/api/src/routes/v1/users.ts @@ -0,0 +1,77 @@ +import type { FastifyInstance } from "fastify"; +import { eq, and, sql } from "drizzle-orm"; +import { users as usersTable, userRoles, roles as rolesTable } from "@sankofa/schema"; + +export async function usersRoutes(app: FastifyInstance) { + const db = app.db; + + const listSchema = { querystring: { type: "object", properties: { limit: { type: "integer" }, offset: { type: "integer" } } } }; + app.get("/", { schema: listSchema }, async (req, reply) => { + const orgId = app.orgId(req); + const limit = Math.min(Number((req.query as { limit?: number }).limit) || 50, 100); + const offset = Number((req.query as { offset?: number }).offset) || 0; + const list = await db.select().from(usersTable).where(eq(usersTable.orgId, orgId)).limit(limit).offset(offset); + const [{ total }] = await db.select({ total: sql`count(*)::int` }).from(usersTable).where(eq(usersTable.orgId, orgId)); + return reply.send({ data: list, total }); + }); + + app.get<{ Params: { id: string } }>("/:id", async (req, reply) => { + const orgId = app.orgId(req); + const [row] = await db.select().from(usersTable).where(and(eq(usersTable.id, req.params.id), eq(usersTable.orgId, orgId))); + if (!row) return reply.notFound(); + const ur = await db.select({ roleId: userRoles.roleId, roleName: rolesTable.name }).from(userRoles).innerJoin(rolesTable, eq(userRoles.roleId, rolesTable.id)).where(eq(userRoles.userId, row.id)); + return reply.send({ ...row, roleIds: ur.map((r) => r.roleId), roleNames: ur.map((r) => r.roleName) }); + }); + + app.post<{ + Body: { email: string; name?: string; orgUnitId?: string; vendorId?: string }; + }>( + "/", + { + schema: { + body: { type: "object", required: ["email"], properties: { email: { type: "string" }, name: { type: "string" }, orgUnitId: { type: "string" }, vendorId: { type: "string" } } }, + }, + }, + async (req, reply) => { + const orgId = app.orgId(req); + const [inserted] = await db.insert(usersTable).values({ + orgId, + email: req.body.email, + name: req.body.name ?? null, + orgUnitId: req.body.orgUnitId ?? null, + vendorId: req.body.vendorId ?? null, + }).returning(); + return reply.code(201).send(inserted); + } + ); + + app.patch<{ Params: { id: string }; Body: Partial<{ name: string; orgUnitId: string; vendorId: string }> }>("/:id", async (req, reply) => { + const orgId = app.orgId(req); + const [updated] = await db.update(usersTable).set({ ...req.body, updatedAt: new Date() }).where(and(eq(usersTable.id, req.params.id), eq(usersTable.orgId, orgId))).returning(); + if (!updated) return reply.notFound(); + return reply.send(updated); + }); + + app.delete<{ Params: { id: string } }>("/:id", async (req, reply) => { + const orgId = app.orgId(req); + const [deleted] = await db.delete(usersTable).where(and(eq(usersTable.id, req.params.id), eq(usersTable.orgId, orgId))).returning({ id: usersTable.id }); + if (!deleted) return reply.notFound(); + return reply.code(204).send(); + }); + + app.post<{ Params: { id: string }; Body: { roleId: string } }>("/:id/roles", async (req, reply) => { + const orgId = app.orgId(req); + const [user] = await db.select().from(usersTable).where(and(eq(usersTable.id, req.params.id), eq(usersTable.orgId, orgId))); + if (!user) return reply.notFound(); + await db.insert(userRoles).values({ userId: user.id, roleId: req.body.roleId }).onConflictDoNothing(); + return reply.code(204).send(); + }); + + app.delete<{ Params: { id: string }; Querystring: { roleId: string } }>("/:id/roles", async (req, reply) => { + const orgId = app.orgId(req); + const [user] = await db.select().from(usersTable).where(and(eq(usersTable.id, req.params.id), eq(usersTable.orgId, orgId))); + if (!user) return reply.notFound(); + await db.delete(userRoles).where(and(eq(userRoles.userId, user.id), eq(userRoles.roleId, (req.query as { roleId: string }).roleId))); + return reply.code(204).send(); + }); +} diff --git a/apps/api/src/routes/v1/vendors.test.ts b/apps/api/src/routes/v1/vendors.test.ts new file mode 100644 index 0000000..b2a9061 --- /dev/null +++ b/apps/api/src/routes/v1/vendors.test.ts @@ -0,0 +1,26 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { buildApp } from "../../index.js"; + +describe("vendors", () => { + let app: Awaited>; + + beforeAll(async () => { + app = await buildApp(); + }); + + afterAll(async () => { + await app.close(); + }); + + it("GET /api/v1/vendors without auth returns 401 or 500 when DB unavailable", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/v1/vendors", + headers: { "x-org-id": "default" }, + }); + expect([401, 500]).toContain(res.statusCode); + const body = JSON.parse(res.payload); + expect(body.error).toBeDefined(); + if (res.statusCode === 401) expect(body.code).toBe("UNAUTHORIZED"); + }); +}); diff --git a/apps/api/src/routes/v1/vendors.ts b/apps/api/src/routes/v1/vendors.ts new file mode 100644 index 0000000..fc24cbe --- /dev/null +++ b/apps/api/src/routes/v1/vendors.ts @@ -0,0 +1,58 @@ +import type { FastifyInstance } from "fastify"; +import { eq, and, sql } from "drizzle-orm"; +import { vendors as vendorsTable } from "@sankofa/schema"; +import { getActorFromRequest, writeAudit } from "../../audit.js"; + +export async function vendorsRoutes(app: FastifyInstance) { + const db = app.db; + app.get("/", { + config: { permission: "vendors:read" }, + schema: { querystring: { type: "object", properties: { limit: { type: "integer" }, offset: { type: "integer" } } } }, + }, async (req, reply) => { + const orgId = app.orgId(req); + const vid = app.vendorId(req); + const limit = Math.min(Number((req.query as { limit?: number }).limit) || 50, 100); + const offset = Number((req.query as { offset?: number }).offset) || 0; + if (vid) { + const [row] = await db.select().from(vendorsTable).where(and(eq(vendorsTable.id, vid), eq(vendorsTable.orgId, orgId))); + return reply.send({ data: row ? [row] : [], total: row ? 1 : 0 }); + } + const list = await db.select().from(vendorsTable).where(eq(vendorsTable.orgId, orgId)).limit(limit).offset(offset); + const [{ total }] = await db.select({ total: sql`count(*)::int` }).from(vendorsTable).where(eq(vendorsTable.orgId, orgId)); + return reply.send({ data: list, total }); + }); + app.get<{ Params: { id: string } }>("/:id", { config: { permission: "vendors:read" } }, async (req, reply) => { + const orgId = app.orgId(req); + const vid = app.vendorId(req); + if (vid && req.params.id !== vid) throw app.httpErrors.forbidden("Vendor users may only access their own vendor"); + const [row] = await db.select().from(vendorsTable).where(and(eq(vendorsTable.id, req.params.id), eq(vendorsTable.orgId, orgId))); + if (!row) return reply.notFound(); + return reply.send(row); + }); + app.post<{ Body: { legalName: string; contacts?: unknown; trustTier?: string } }>("/", { + config: { permission: "vendors:write" }, + schema: { body: { type: "object", required: ["legalName"], properties: { legalName: { type: "string" }, contacts: {}, trustTier: { type: "string" } } } }, + }, async (req, reply) => { + if (app.vendorId(req)) throw app.httpErrors.forbidden("Vendor users cannot create vendors"); + const orgId = app.orgId(req); + const [inserted] = await db.insert(vendorsTable).values({ orgId, legalName: req.body.legalName, contacts: (req.body.contacts as { email?: string; phone?: string; name?: string }[] | null) ?? null, trustTier: req.body.trustTier ?? "unknown" }).returning(); + return reply.code(201).send(inserted); + }); + app.patch<{ Params: { id: string }; Body: Record }>("/:id", { config: { permission: "vendors:write" } }, async (req, reply) => { + if (app.vendorId(req)) throw app.httpErrors.forbidden("Vendor users cannot update vendors"); + const orgId = app.orgId(req); + const [before] = await db.select().from(vendorsTable).where(and(eq(vendorsTable.id, req.params.id), eq(vendorsTable.orgId, orgId))); + const [updated] = await db.update(vendorsTable).set({ ...req.body, updatedAt: new Date() }).where(and(eq(vendorsTable.id, req.params.id), eq(vendorsTable.orgId, orgId))).returning(); + if (!updated) return reply.notFound(); + const actor = getActorFromRequest(req); + await writeAudit(db, { orgId, ...actor, action: "vendor.update", resourceType: "vendor", resourceId: req.params.id, beforeState: before ? { ...before } : undefined, afterState: { ...updated } }); + return reply.send(updated); + }); + app.delete<{ Params: { id: string } }>("/:id", { config: { permission: "vendors:write" } }, async (req, reply) => { + if (app.vendorId(req)) throw app.httpErrors.forbidden("Vendor users cannot delete vendors"); + const orgId = app.orgId(req); + const [deleted] = await db.delete(vendorsTable).where(and(eq(vendorsTable.id, req.params.id), eq(vendorsTable.orgId, orgId))).returning({ id: vendorsTable.id }); + if (!deleted) return reply.notFound(); + return reply.code(204).send(); + }); +} diff --git a/apps/api/src/routes/v1/workflow.ts b/apps/api/src/routes/v1/workflow.ts new file mode 100644 index 0000000..bcb880b --- /dev/null +++ b/apps/api/src/routes/v1/workflow.ts @@ -0,0 +1,56 @@ +import type { FastifyInstance } from "fastify"; +import { eq, and } from "drizzle-orm"; +import { purchaseOrders as poTable, offers as offersTable, vendors as vendorsTable } from "@sankofa/schema"; +import { nextPOStage, canTransitionPO, computeOfferRiskScore } from "@sankofa/workflow"; + +export async function workflowRoutes(app: FastifyInstance) { + const db = app.db; + app.post<{ Params: { id: string }; Body: { trustTier?: string; priceDeviation?: number; conditionAmbiguity?: boolean } }>("/offers/:id/risk-score", async (req, reply) => { + const orgId = app.orgId(req); + const [offer] = await db.select().from(offersTable).where(and(eq(offersTable.id, req.params.id), eq(offersTable.orgId, orgId))); + if (!offer) return reply.notFound(); + const [vendor] = offer.vendorId ? await db.select().from(vendorsTable).where(eq(vendorsTable.id, offer.vendorId)) : [null]; + const factors = { trustTier: req.body.trustTier ?? vendor?.trustTier ?? "unknown", priceDeviation: req.body.priceDeviation, conditionAmbiguity: req.body.conditionAmbiguity ?? !offer.condition }; + const { score, factors: outFactors } = computeOfferRiskScore(factors); + await db.update(offersTable).set({ riskScore: String(score), riskFactors: outFactors as unknown as Record, updatedAt: new Date() }).where(and(eq(offersTable.id, req.params.id), eq(offersTable.orgId, orgId))); + return reply.send({ score, factors: outFactors }); + }); + app.post<{ Params: { id: string } }>("/purchase-orders/:id/submit", async (req, reply) => { + const orgId = app.orgId(req); + const [po] = await db.select().from(poTable).where(and(eq(poTable.id, req.params.id), eq(poTable.orgId, orgId))); + if (!po) return reply.notFound(); + if (po.status !== "draft") return reply.badRequest("PO not in draft"); + await db.update(poTable).set({ status: "pending_approval", approvalStage: "requester", updatedAt: new Date() }).where(and(eq(poTable.id, req.params.id), eq(poTable.orgId, orgId))); + return reply.send({ status: "pending_approval", approvalStage: "requester" }); + }); + app.post<{ Params: { id: string } }>("/purchase-orders/:id/approve", async (req, reply) => { + const orgId = app.orgId(req); + const [po] = await db.select().from(poTable).where(and(eq(poTable.id, req.params.id), eq(poTable.orgId, orgId))); + if (!po) return reply.notFound(); + if (po.status !== "pending_approval") return reply.badRequest("PO not pending approval"); + const next = nextPOStage(po.approvalStage as "requester" | "procurement" | "finance" | "executive" | null); + if (next) { + await db.update(poTable).set({ approvalStage: next, updatedAt: new Date() }).where(and(eq(poTable.id, req.params.id), eq(poTable.orgId, orgId))); + return reply.send({ status: "pending_approval", approvalStage: next }); + } + await db.update(poTable).set({ status: "approved", approvalStage: "executive", updatedAt: new Date() }).where(and(eq(poTable.id, req.params.id), eq(poTable.orgId, orgId))); + return reply.send({ status: "approved" }); + }); + app.post<{ Params: { id: string } }>("/purchase-orders/:id/reject", async (req, reply) => { + const orgId = app.orgId(req); + const [po] = await db.select().from(poTable).where(and(eq(poTable.id, req.params.id), eq(poTable.orgId, orgId))); + if (!po) return reply.notFound(); + if (po.status !== "pending_approval") return reply.badRequest("PO not pending approval"); + await db.update(poTable).set({ status: "rejected", updatedAt: new Date() }).where(and(eq(poTable.id, req.params.id), eq(poTable.orgId, orgId))); + return reply.send({ status: "rejected" }); + }); + app.patch<{ Params: { id: string }; Body: { status: string } }>("/purchase-orders/:id/status", async (req, reply) => { + const orgId = app.orgId(req); + const [po] = await db.select().from(poTable).where(and(eq(poTable.id, req.params.id), eq(poTable.orgId, orgId))); + if (!po) return reply.notFound(); + const to = req.body.status as "draft" | "pending_approval" | "approved" | "rejected" | "ordered" | "received"; + if (!canTransitionPO(po.status as "draft" | "pending_approval" | "approved" | "rejected" | "ordered" | "received", to)) return reply.badRequest("Invalid status transition"); + await db.update(poTable).set({ status: to, updatedAt: new Date() }).where(and(eq(poTable.id, req.params.id), eq(poTable.orgId, orgId))); + return reply.send({ status: to }); + }); +} diff --git a/apps/api/src/schemas/errors.ts b/apps/api/src/schemas/errors.ts new file mode 100644 index 0000000..b319926 --- /dev/null +++ b/apps/api/src/schemas/errors.ts @@ -0,0 +1,14 @@ +/** Standard error payload for API responses */ +export interface ApiErrorPayload { + error: string; + code?: string; + details?: unknown; +} + +export const errorCodes = { + BAD_REQUEST: "BAD_REQUEST", + UNAUTHORIZED: "UNAUTHORIZED", + FORBIDDEN: "FORBIDDEN", + NOT_FOUND: "NOT_FOUND", + CONFLICT: "CONFLICT", +} as const; diff --git a/apps/api/src/storage.ts b/apps/api/src/storage.ts new file mode 100644 index 0000000..ad6811b --- /dev/null +++ b/apps/api/src/storage.ts @@ -0,0 +1,67 @@ +import { + S3Client, + PutObjectCommand, + GetObjectCommand, + HeadObjectCommand, +} from "@aws-sdk/client-s3"; +import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; + +const endpoint = process.env.S3_ENDPOINT; +const region = process.env.S3_REGION || "us-east-1"; +const bucket = process.env.S3_BUCKET || "sankofa-documents"; +const forcePathStyle = Boolean(endpoint); + +export const s3Client = new S3Client({ + region, + ...(endpoint + ? { + endpoint, + forcePathStyle, + credentials: { + accessKeyId: process.env.S3_ACCESS_KEY || "minioadmin", + secretAccessKey: process.env.S3_SECRET_KEY || "minioadmin", + }, + } + : {}), +}); + +export interface UploadResult { + key: string; + bucket: string; + etag?: string; +} + +export async function uploadDocument( + key: string, + body: Buffer | Uint8Array, + contentType: string, + metadata?: Record +): Promise { + const command = new PutObjectCommand({ + Bucket: bucket, + Key: key, + Body: body, + ContentType: contentType, + Metadata: metadata, + }); + const out = await s3Client.send(command); + return { key, bucket, etag: out.ETag }; +} + +export async function getDocumentKey(key: string): Promise { + try { + await s3Client.send( + new HeadObjectCommand({ Bucket: bucket, Key: key }) + ); + return true; + } catch { + return false; + } +} + +export async function getSignedDownloadUrl(key: string, expiresIn = 3600): Promise { + const command = new GetObjectCommand({ Bucket: bucket, Key: key }); + return getSignedUrl(s3Client, command, { expiresIn }); +} + +export { bucket as defaultBucket }; diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json new file mode 100644 index 0000000..0738404 --- /dev/null +++ b/apps/api/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "outDir": "dist", + "rootDir": "src", + "skipLibCheck": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/apps/web/index.html b/apps/web/index.html new file mode 100644 index 0000000..f0ecea3 --- /dev/null +++ b/apps/web/index.html @@ -0,0 +1,12 @@ + + + + + + Sankofa HW Infra + + +
+ + + diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..fa02e44 --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,25 @@ +{ + "name": "@sankofa/web", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "tsc && vite build", + "dev": "vite", + "preview": "vite preview", + "lint": "eslint src --ext .ts,.tsx" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^7.0.0" + }, + "devDependencies": { + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.4", + "eslint": "^9.15.0", + "typescript": "^5.7.0", + "vite": "^6.0.0" + } +} diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx new file mode 100644 index 0000000..d04f674 --- /dev/null +++ b/apps/web/src/App.tsx @@ -0,0 +1,33 @@ +import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; +import { AuthProvider } from "./contexts/AuthContext"; +import { Layout } from "./components/Layout"; +import { Dashboard } from "./pages/Dashboard"; +import { Login } from "./pages/Login"; +import { Vendors } from "./pages/Vendors"; +import { Offers } from "./pages/Offers"; +import { PurchaseOrders } from "./pages/PurchaseOrders"; +import { Assets } from "./pages/Assets"; +import { Sites } from "./pages/Sites"; +import { Capacity } from "./pages/Capacity"; + +export default function App() { + return ( + + + + } /> + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + + ); +} diff --git a/apps/web/src/api/client.ts b/apps/web/src/api/client.ts new file mode 100644 index 0000000..450ab9a --- /dev/null +++ b/apps/web/src/api/client.ts @@ -0,0 +1,58 @@ +const API_BASE = (import.meta as unknown as { env: { VITE_API_URL?: string } }).env?.VITE_API_URL ?? ""; + +export interface ApiErrorPayload { + error: string; + code?: string; + details?: unknown; +} + +function getToken(): string | null { + return localStorage.getItem("sankofa_token"); +} + +function getOrgId(): string { + return localStorage.getItem("sankofa_org_id") ?? "default"; +} + +export async function api( + path: string, + options: RequestInit & { params?: Record } = {} +): Promise { + const { params, ...init } = options; + const url = params ? `${API_BASE}${path}?${new URLSearchParams(params)}` : `${API_BASE}${path}`; + const token = getToken(); + const orgId = getOrgId(); + const headers: HeadersInit = { + "Content-Type": "application/json", + "x-org-id": orgId, + ...(init.headers as Record), + }; + if (token) (headers as Record)["Authorization"] = `Bearer ${token}`; + const res = await fetch(url, { ...init, headers }); + if (res.status === 401) { + localStorage.removeItem("sankofa_token"); + window.dispatchEvent(new CustomEvent("auth:401")); + const body = (await res.json().catch(() => ({}))) as ApiErrorPayload; + throw new Error(body.error ?? "Unauthorized"); + } + if (!res.ok) { + const body = (await res.json().catch(() => ({}))) as ApiErrorPayload; + throw new Error(body.error ?? "Request failed: " + res.status); + } + if (res.status === 204) return undefined as T; + return res.json() as Promise; +} + +export function setAuth(token: string, orgId?: string) { + localStorage.setItem("sankofa_token", token); + if (orgId != null) localStorage.setItem("sankofa_org_id", orgId); +} + +export function clearAuth() { + localStorage.removeItem("sankofa_token"); + localStorage.removeItem("sankofa_org_id"); +} + +export function isAuthenticated(): boolean { + return !!getToken(); +} diff --git a/apps/web/src/components/Layout.tsx b/apps/web/src/components/Layout.tsx new file mode 100644 index 0000000..dd3d514 --- /dev/null +++ b/apps/web/src/components/Layout.tsx @@ -0,0 +1,40 @@ +import { Link, Outlet, useNavigate } from "react-router-dom"; +import { useAuth } from "../contexts/AuthContext"; + +const nav = [ + { to: "/", label: "Dashboard" }, + { to: "/vendors", label: "Vendors" }, + { to: "/offers", label: "Offers" }, + { to: "/purchase-orders", label: "Purchase orders" }, + { to: "/assets", label: "Assets" }, + { to: "/sites", label: "Sites" }, + { to: "/capacity", label: "Capacity" }, +]; + +export function Layout() { + const { user, logout } = useAuth(); + const navigate = useNavigate(); + return ( +
+
+

Sankofa HW Infra

+ + {user ? ( + + {user.email} + + + ) : ( + Sign in + )} +
+
+ +
+
+ ); +} diff --git a/apps/web/src/contexts/AuthContext.tsx b/apps/web/src/contexts/AuthContext.tsx new file mode 100644 index 0000000..16b205e --- /dev/null +++ b/apps/web/src/contexts/AuthContext.tsx @@ -0,0 +1,70 @@ +import { createContext, useContext, useCallback, useState, useEffect, type ReactNode } from "react"; +import { api, setAuth as apiSetAuth, clearAuth, isAuthenticated } from "../api/client"; + +interface User { + id: string; + email: string; + name: string | null; + roles: string[]; + vendorId: string | null; +} + +interface AuthState { + user: User | null; + loading: boolean; + login: (email: string, password?: string) => Promise; + logout: () => void; + setOrgId: (orgId: string) => void; +} + +const AuthContext = createContext(null); + +export function AuthProvider({ children }: { children: ReactNode }) { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + const login = useCallback(async (email: string, _password?: string) => { + const res = await api<{ token: string; user: User }>("/api/v1/auth/token", { + method: "POST", + body: JSON.stringify({ email }), + }); + apiSetAuth(res.token, "default"); + setUser(res.user); + }, []); + + const logout = useCallback(() => { + clearAuth(); + setUser(null); + }, []); + + const setOrgId = useCallback((orgId: string) => { + localStorage.setItem("sankofa_org_id", orgId); + }, []); + + useEffect(() => { + if (!isAuthenticated()) { + setLoading(false); + return; + } + setUser(null); + setLoading(false); + }, []); + + useEffect(() => { + const on401 = () => setUser(null); + window.addEventListener("auth:401", on401); + return () => window.removeEventListener("auth:401", on401); + }, []); + + return ( + + {children} + + ); +} + +export function useAuth(): AuthState { + const ctx = useContext(AuthContext); + if (!ctx) throw new Error("useAuth must be used within AuthProvider"); + return ctx; +} diff --git a/apps/web/src/index.css b/apps/web/src/index.css new file mode 100644 index 0000000..0e43f96 --- /dev/null +++ b/apps/web/src/index.css @@ -0,0 +1,3 @@ +:root { font-family: system-ui,sans-serif; line-height: 1.5; color: #1a1a1a; background: #f8f9fa; } +* { box-sizing: border-box; } +body { margin: 0; } diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx new file mode 100644 index 0000000..9b67590 --- /dev/null +++ b/apps/web/src/main.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; +import "./index.css"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + +); diff --git a/apps/web/src/pages/Assets.tsx b/apps/web/src/pages/Assets.tsx new file mode 100644 index 0000000..6b73719 --- /dev/null +++ b/apps/web/src/pages/Assets.tsx @@ -0,0 +1,30 @@ +import { useEffect, useState } from "react"; +import { api } from "../api/client"; + +type Asset = { id: string; hostname?: string; status: string }; + +export function Assets() { + const [list, setList] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + + useEffect(() => { + api("/api/v1/assets") + .then(setList) + .catch((e) => setError(e.message)) + .finally(() => setLoading(false)); + }, []); + + if (loading) return

Loading...

; + if (error) return

{error}

; + return ( +
+

Assets

+
    + {list.map((a) => ( +
  • {a.hostname ?? a.id} - {a.status}
  • + ))} +
+
+ ); +} diff --git a/apps/web/src/pages/Capacity.tsx b/apps/web/src/pages/Capacity.tsx new file mode 100644 index 0000000..780b698 --- /dev/null +++ b/apps/web/src/pages/Capacity.tsx @@ -0,0 +1,140 @@ +import { useEffect, useState } from "react"; +import { api } from "../api/client"; + +type Site = { id: string; name: string }; +type SiteCapacity = { siteId: string; usedRu: number; totalRu: number; utilizationPercent: number }; +type SitePower = { siteId: string; circuitLimitWatts: number; measuredDrawWatts: number | null; headroomWatts: number | null }; +type GpuInventory = { total: number; bySite: Record; byType: Record }; + +export function Capacity() { + const [sites, setSites] = useState([]); + const [capacityBySite, setCapacityBySite] = useState([]); + const [powerBySite, setPowerBySite] = useState([]); + const [gpu, setGpu] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + + useEffect(() => { + async function load() { + try { + const sitesRes = await api("/api/v1/sites"); + const list = Array.isArray(sitesRes) ? sitesRes : (sitesRes.data ?? []); + setSites(list); + const caps: SiteCapacity[] = []; + const pows: SitePower[] = []; + for (const s of list) { + try { + const [c, p] = await Promise.all([ + api(`/api/v1/capacity/sites/${s.id}`), + api(`/api/v1/capacity/sites/${s.id}/power`), + ]); + caps.push(c); + pows.push(p); + } catch { + caps.push({ siteId: s.id, usedRu: 0, totalRu: 0, utilizationPercent: 0 }); + pows.push({ siteId: s.id, circuitLimitWatts: 0, measuredDrawWatts: null, headroomWatts: null }); + } + } + setCapacityBySite(caps); + setPowerBySite(pows); + const gpuRes = await api("/api/v1/capacity/gpu-inventory"); + setGpu(gpuRes); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load capacity"); + } finally { + setLoading(false); + } + } + load(); + }, []); + + if (loading) return

Loading...

; + if (error) return

{error}

; + + return ( +
+

Capacity planning

+ +
+

RU utilization by site

+ + + + + + + + + + + {sites.map((s) => { + const cap = capacityBySite.find((c) => c.siteId === s.id); + return ( + + + + + + + ); + })} + +
SiteUsed RUTotal RUUtilization
{s.name}{cap?.usedRu ?? 0}{cap?.totalRu ?? 0}{cap?.utilizationPercent ?? 0}%
+
+ +
+

Power headroom by site

+ + + + + + + + + + {sites.map((s) => { + const pow = powerBySite.find((p) => p.siteId === s.id); + return ( + + + + + + ); + })} + +
SiteCircuit limit (W)Measured draw
{s.name}{pow?.circuitLimitWatts ?? 0}{pow?.measuredDrawWatts != null ? pow.measuredDrawWatts + " W" : "—"}
+
+ +
+

GPU inventory

+ {gpu && ( + <> +

Total: {gpu.total}

+
+
+

By site

+
    + {Object.entries(gpu.bySite).map(([siteId, count]) => ( +
  • {siteId}: {count}
  • + ))} + {Object.keys(gpu.bySite).length === 0 &&
  • } +
+
+
+

By type

+
    + {Object.entries(gpu.byType).map(([type, count]) => ( +
  • {type}: {count}
  • + ))} + {Object.keys(gpu.byType).length === 0 &&
  • } +
+
+
+ + )} +
+
+ ); +} diff --git a/apps/web/src/pages/Dashboard.tsx b/apps/web/src/pages/Dashboard.tsx new file mode 100644 index 0000000..2ad7fdd --- /dev/null +++ b/apps/web/src/pages/Dashboard.tsx @@ -0,0 +1,8 @@ +export function Dashboard() { + return ( +
+

Control Plane

+

Inventory, procurement, sites, and operations.

+
+ ); +} diff --git a/apps/web/src/pages/Login.tsx b/apps/web/src/pages/Login.tsx new file mode 100644 index 0000000..c197cac --- /dev/null +++ b/apps/web/src/pages/Login.tsx @@ -0,0 +1,42 @@ +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { useAuth } from "../contexts/AuthContext"; + +export function Login() { + const [email, setEmail] = useState(""); + const [error, setError] = useState(""); + const { login } = useAuth(); + const navigate = useNavigate(); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(""); + try { + await login(email); + navigate("/", { replace: true }); + } catch (err) { + setError(err instanceof Error ? err.message : "Login failed"); + } + } + + return ( +
+

Sign in

+
+
+ + setEmail(e.target.value)} + required + style={{ width: "100%", padding: 8 }} + /> +
+ {error &&

{error}

} + +
+
+ ); +} diff --git a/apps/web/src/pages/Offers.tsx b/apps/web/src/pages/Offers.tsx new file mode 100644 index 0000000..a62817c --- /dev/null +++ b/apps/web/src/pages/Offers.tsx @@ -0,0 +1,27 @@ +import { useEffect, useState } from "react"; +import { api } from "../api/client"; + +type Offer = { id: string; vendorId: string; quantity: number; unitPrice: string; status: string }; + +export function Offers() { + const [list, setList] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + + useEffect(() => { + api<{ data: Offer[]; total: number }>("/api/v1/offers").then((r) => setList(r.data)).catch((e) => setError(e.message)).finally(() => setLoading(false)); + }, []); + + if (loading) return

Loading...

; + if (error) return

{error}

; + return ( +
+

Offers

+
    + {list.map((o) => ( +
  • Qty {o.quantity} at {o.unitPrice} - {o.status}
  • + ))} +
+
+ ); +} diff --git a/apps/web/src/pages/PurchaseOrders.tsx b/apps/web/src/pages/PurchaseOrders.tsx new file mode 100644 index 0000000..451812f --- /dev/null +++ b/apps/web/src/pages/PurchaseOrders.tsx @@ -0,0 +1,30 @@ +import { useEffect, useState } from "react"; +import { api } from "../api/client"; + +type PO = { id: string; vendorId: string; status: string }; + +export function PurchaseOrders() { + const [list, setList] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + + useEffect(() => { + api("/api/v1/purchase-orders") + .then(setList) + .catch((e) => setError(e.message)) + .finally(() => setLoading(false)); + }, []); + + if (loading) return

Loading...

; + if (error) return

{error}

; + return ( +
+

Purchase orders

+
    + {list.map((p) => ( +
  • {p.status}
  • + ))} +
+
+ ); +} diff --git a/apps/web/src/pages/Sites.tsx b/apps/web/src/pages/Sites.tsx new file mode 100644 index 0000000..0d76764 --- /dev/null +++ b/apps/web/src/pages/Sites.tsx @@ -0,0 +1,30 @@ +import { useEffect, useState } from "react"; +import { api } from "../api/client"; + +type Site = { id: string; name: string; regionId?: string }; + +export function Sites() { + const [list, setList] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + + useEffect(() => { + api("/api/v1/sites") + .then(setList) + .catch((e) => setError(e.message)) + .finally(() => setLoading(false)); + }, []); + + if (loading) return

Loading...

; + if (error) return

{error}

; + return ( +
+

Sites

+
    + {list.map((s) => ( +
  • {s.name}
  • + ))} +
+
+ ); +} diff --git a/apps/web/src/pages/Vendors.tsx b/apps/web/src/pages/Vendors.tsx new file mode 100644 index 0000000..150944a --- /dev/null +++ b/apps/web/src/pages/Vendors.tsx @@ -0,0 +1,27 @@ +import { useEffect, useState } from "react"; +import { api } from "../api/client"; + +type Vendor = { id: string; legalName: string; trustTier: string }; + +export function Vendors() { + const [list, setList] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + + useEffect(() => { + api<{ data: Vendor[]; total: number }>("/api/v1/vendors").then((r) => setList(r.data)).catch((e) => setError(e.message)).finally(() => setLoading(false)); + }, []); + + if (loading) return

Loading...

; + if (error) return

{error}

; + return ( +
+

Vendors

+
    + {list.map((v) => ( +
  • {v.legalName} ({v.trustTier})
  • + ))} +
+
+ ); +} diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json new file mode 100644 index 0000000..4d38446 --- /dev/null +++ b/apps/web/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "jsx": "react-jsx", + "noEmit": true, + "skipLibCheck": true, + "baseUrl": ".", + "paths": { "@/*": ["src/*"] } + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/apps/web/tsconfig.node.json b/apps/web/tsconfig.node.json new file mode 100644 index 0000000..d02c37d --- /dev/null +++ b/apps/web/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true + }, + "include": ["vite.config.ts"] +} diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts new file mode 100644 index 0000000..2543a10 --- /dev/null +++ b/apps/web/vite.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import path from "path"; + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { "@": path.resolve(__dirname, "src") }, + }, + server: { + port: 3000, + proxy: { "/api": { target: "http://localhost:4000", changeOrigin: true } }, + }, +}); diff --git a/apps/workflow/package.json b/apps/workflow/package.json new file mode 100644 index 0000000..cb1af43 --- /dev/null +++ b/apps/workflow/package.json @@ -0,0 +1,24 @@ +{ + "name": "@sankofa/workflow", + "version": "0.1.0", + "private": true, + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "scripts": { + "build": "tsc", + "dev": "tsx watch src/index.ts", + "test": "vitest run", + "lint": "eslint src --ext .ts" + }, + "dependencies": { + "@sankofa/schema": "workspace:*" + }, + "devDependencies": { + "@types/node": "^22.10.0", + "eslint": "^9.15.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0", + "vitest": "^2.1.0" + } +} diff --git a/apps/workflow/src/index.test.ts b/apps/workflow/src/index.test.ts new file mode 100644 index 0000000..188a4bb --- /dev/null +++ b/apps/workflow/src/index.test.ts @@ -0,0 +1,12 @@ +import { describe, it, expect } from "vitest"; +import { canTransitionPO, computeOfferRiskScore } from "./index"; + +describe("workflow", () => { + it("allows draft to pending_approval", () => { + expect(canTransitionPO("draft", "pending_approval")).toBe(true); + }); + it("risk score computed", () => { + const { score } = computeOfferRiskScore({ trustTier: "unknown" }); + expect(score).toBeGreaterThanOrEqual(0); + }); +}); diff --git a/apps/workflow/src/index.ts b/apps/workflow/src/index.ts new file mode 100644 index 0000000..300bd9f --- /dev/null +++ b/apps/workflow/src/index.ts @@ -0,0 +1,39 @@ +export const PO_STATUS = ["draft", "pending_approval", "approved", "rejected", "ordered", "received"] as const; +export type POStatus = (typeof PO_STATUS)[number]; + +export const APPROVAL_STAGES = ["requester", "procurement", "finance", "executive"] as const; +export type ApprovalStage = (typeof APPROVAL_STAGES)[number]; + +export function nextPOStage(current: ApprovalStage | null): ApprovalStage | null { + if (!current) return "requester"; + const i = APPROVAL_STAGES.indexOf(current); + return i < APPROVAL_STAGES.length - 1 ? APPROVAL_STAGES[i + 1] : null; +} + +export function canTransitionPO(from: POStatus, to: POStatus): boolean { + const allowed: Record = { + draft: ["pending_approval", "rejected"], + pending_approval: ["approved", "rejected", "draft"], + approved: ["ordered"], + rejected: ["draft"], + ordered: ["received"], + received: [], + }; + return allowed[from]?.includes(to) ?? false; +} + +export interface RiskFactors { + trustTier: string; + priceDeviation?: number; + conditionAmbiguity?: boolean; +} + +export function computeOfferRiskScore(factors: RiskFactors): { score: number; factors: RiskFactors } { + let score = 0; + if (factors.trustTier === "unknown") score += 30; + else if (factors.trustTier === "low") score += 20; + else if (factors.trustTier === "medium") score += 10; + if (factors.priceDeviation != null && factors.priceDeviation > 0.2) score += 25; + if (factors.conditionAmbiguity) score += 20; + return { score: Math.min(100, score), factors }; +} diff --git a/apps/workflow/tsconfig.json b/apps/workflow/tsconfig.json new file mode 100644 index 0000000..800ab5c --- /dev/null +++ b/apps/workflow/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "outDir": "dist", + "rootDir": "src", + "skipLibCheck": true, + "declaration": true, + "declarationMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/data/inspection-checklists/gpus.json b/data/inspection-checklists/gpus.json new file mode 100644 index 0000000..75af599 --- /dev/null +++ b/data/inspection-checklists/gpus.json @@ -0,0 +1 @@ +{"category":"gpu","name":"GPU inspection","steps":[{"id":"board_id","label":"Board ID","required":true}]} diff --git a/data/inspection-checklists/memory.json b/data/inspection-checklists/memory.json new file mode 100644 index 0000000..63b627a --- /dev/null +++ b/data/inspection-checklists/memory.json @@ -0,0 +1 @@ +{"category":"memory","name":"Memory inspection","steps":[{"id":"memtest","label":"Memtest pass","required":true}]} diff --git a/data/inspection-checklists/nics.json b/data/inspection-checklists/nics.json new file mode 100644 index 0000000..e0569f8 --- /dev/null +++ b/data/inspection-checklists/nics.json @@ -0,0 +1,9 @@ +{ + "category": "nic", + "name": "NIC inspection", + "steps": [ + { "id": "pci_id", "label": "PCI IDs", "required": true }, + { "id": "firmware", "label": "Firmware version", "required": false }, + { "id": "link_test", "label": "Link test", "required": true } + ] +} diff --git a/data/inspection-checklists/servers.json b/data/inspection-checklists/servers.json new file mode 100644 index 0000000..09a3a11 --- /dev/null +++ b/data/inspection-checklists/servers.json @@ -0,0 +1 @@ +{"category":"server","name":"Server inspection","steps":[{"id":"chassis","label":"Chassis condition","required":true},{"id":"service_tag","label":"Service tag verified","required":true}]} diff --git a/data/operational-baseline-hardware.json b/data/operational-baseline-hardware.json new file mode 100644 index 0000000..a2d0438 --- /dev/null +++ b/data/operational-baseline-hardware.json @@ -0,0 +1,67 @@ +[ + { + "id": "ml110", + "category": "server", + "model": "HPE ProLiant ML110", + "role": "Core services / management / utility workloads", + "formFactor": "Tower / rack-convertible", + "status": "running", + "quantity": "TBD", + "notes": "Suitable for control-plane services, monitoring, identity, light virtualization" + }, + { + "id": "r630", + "category": "server", + "model": "Dell PowerEdge R630", + "role": "General compute / virtualization / legacy workloads", + "formFactor": "1U rackmount", + "status": "running", + "quantity": "TBD", + "notes": "Ideal for Proxmox clusters, utility VMs, staging environments" + }, + { + "id": "udm-pro", + "category": "network", + "model": "UniFi Dream Machine Pro", + "role": "Edge gateway, UniFi OS controller, firewall", + "status": "running", + "quantity": "TBD", + "notes": "Per-site edge control; candidate for per-sovereign controller domains" + }, + { + "id": "unifi-xg", + "category": "network", + "model": "UniFi XG Switches", + "role": "High-throughput aggregation / core switching", + "status": "running", + "quantity": "TBD", + "notes": "10G/25G backbone for compute and storage traffic" + }, + { + "id": "spectrum-modem", + "category": "network", + "model": "Spectrum Business Cable Modem", + "role": "Primary or secondary WAN connectivity", + "status": "installed", + "quantity": "TBD", + "notes": "Business-class internet; typically paired with UDM Pro" + }, + { + "id": "apc-cabinet", + "category": "rack", + "model": "APC Equipment Cabinet", + "role": "Secure rack enclosure", + "status": "installed", + "quantity": "TBD", + "notes": "Houses compute, network, and power equipment" + }, + { + "id": "apc-ups", + "category": "power", + "model": "APC UPS", + "role": "Power conditioning and battery backup", + "status": "installed", + "quantity": "TBD", + "notes": "Runtime and load to be captured per site for capacity planning" + } +] diff --git a/docs/api-error-format.md b/docs/api-error-format.md new file mode 100644 index 0000000..5917727 --- /dev/null +++ b/docs/api-error-format.md @@ -0,0 +1,17 @@ +# API error response format + +All API errors use a consistent JSON body: + +```json +{ + "error": "Human-readable message", + "code": "UNAUTHORIZED", + "details": {} +} +``` + +- **error** (string): Message for clients and logs. +- **code** (string, optional): Machine-readable code. One of `BAD_REQUEST`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `CONFLICT`, `INTERNAL_ERROR`. +- **details** (object, optional): Extra data (e.g. validation errors under `details` when `code` is `BAD_REQUEST`). + +HTTP status matches the error (400, 401, 403, 404, 409, 500). The OpenAPI spec references the `ApiError` schema in `components.schemas`. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..9ee7331 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,20 @@ +# Sankofa HW Infra — Architecture + +## Component diagram + +See the plan file for the Mermaid flowchart (Control Plane UI, API, Workflow Engine, PostgreSQL, S3, Integration Layer, IAM, Audit, Logging). + +## Components + +- **Control Plane UI**: React SPA; inventory, procurement, sites, approvals, audit. +- **API Layer**: REST `/api/v1`; CRUD for core entities; JWT + RBAC/ABAC; file upload to S3. +- **Workflow Engine**: Purchase approvals, inspection checklists (Phase 1+). +- **PostgreSQL**: Transactions, core entities, audit_events (append-only). +- **Object Storage (S3)**: Invoices, packing lists, inspection photos, serial dumps. +- **Integration Layer**: UniFi, Proxmox, Redfish connectors; credentials in Vault. +- **IAM**: Roles, permissions; ABAC attributes (site_id, project_id). +- **Audit Log**: Who/when/what, before/after; WORM retention. + +## Sovereign cloud positioning + +Sankofa Phoenix operates as a **sovereign cloud services provider**. Multi-tenant isolation is per sovereign (org); UniFi, Proxmox, and hardware inventory form **one source of truth** for determinism and compliance. UniFi telemetry (with product intelligence), rack/power metadata, and Proxmox workloads are synthesized for root-cause analysis, capacity planning, and enforced hardware standards per sovereign profile. See [sovereign-controller-topology.md](sovereign-controller-topology.md), [rbac-sovereign-operations.md](rbac-sovereign-operations.md), and [purchasing-feedback-loop.md](purchasing-feedback-loop.md). diff --git a/docs/capacity-dashboard-spec.md b/docs/capacity-dashboard-spec.md new file mode 100644 index 0000000..cefb770 --- /dev/null +++ b/docs/capacity-dashboard-spec.md @@ -0,0 +1,7 @@ +# Capacity planning dashboard spec + +- **RU utilization**: Per site, sum of assigned positions vs total RU (from racks); show percentage. **Implemented.** API: `GET /api/v1/capacity/sites/:siteId` returns `usedRu`, `totalRu`, and `utilizationPercent`. +- **Power headroom**: From rack `power_feeds` (circuit limits). **Implemented.** API: `GET /api/v1/capacity/sites/:siteId/power` returns `circuitLimitWatts`, `measuredDrawWatts` (null until Phase 4), `headroomWatts` (null). Measured draw can be added when telemetry is available. +- **GPU inventory**: By type (part number) and location. **Implemented.** API: `GET /api/v1/capacity/gpu-inventory` returns `total`, `bySite`, and `byType`. +- **Read-only**: All capacity endpoints are read-only; no edits. +- **Web**: Capacity dashboard at `/capacity` shows RU utilization, power headroom, and GPU inventory by site and type. diff --git a/docs/cicd.md b/docs/cicd.md new file mode 100644 index 0000000..0a70a3f --- /dev/null +++ b/docs/cicd.md @@ -0,0 +1,11 @@ +# CI/CD pipeline + +- Lint: `pnpm run lint` (ESLint over apps and packages) +- Test: `pnpm run test` (Vitest per package) +- Build: `pnpm run build` (all workspace packages) + +GitHub Actions: `.github/workflows/ci.yml` runs on push/PR to main: install, lint, test, build. + +Environments: Dev (local + docker-compose), Staging/Production (set DATABASE_URL, S3_*, JWT_SECRET). + +Runbook: Start Postgres via `infra/docker-compose up -d`. Migrate: `pnpm db:migrate`. API: `pnpm --filter @sankofa/api run dev`. Web: `pnpm --filter @sankofa/web run dev`. diff --git a/docs/compliance-profiles.md b/docs/compliance-profiles.md new file mode 100644 index 0000000..bef91c2 --- /dev/null +++ b/docs/compliance-profiles.md @@ -0,0 +1,23 @@ +# Compliance profiles + +Compliance profiles define **firmware freeze**, **allowed hardware generations**, and **approved SKUs** per sovereign (org) or per site. They feed purchasing (approved buy lists) and UniFi device approval. + +## Purpose + +- **Firmware freeze:** Lock to a version or range (e.g. 2024.Q2, or min/max version) so only compliant firmware is allowed. +- **Allowed generations:** Restrict hardware to e.g. Gen2 and Enterprise only (from UniFi product catalog). +- **Approved SKUs:** Explicit list of SKUs that may be purchased or deployed; optional per-site override. + +Profiles are attached to `org_id` (sovereign/tenant); optionally `site_id` for site-specific rules. + +## API + +- `GET /api/v1/compliance-profiles` — list profiles for the current org. +- `GET /api/v1/compliance-profiles/:id` — get one profile. +- `POST /api/v1/compliance-profiles` — create (body: name, firmwareFreezePolicy, allowedGenerations, approvedSkus, siteId). +- `PATCH /api/v1/compliance-profiles/:id` — update. +- `DELETE /api/v1/compliance-profiles/:id` — delete. + +## Use in validation + +When generating the **approved purchasing catalog** or when syncing UniFi devices, filter or flag by compliance profile: only SKUs in `approved_skus` or in `allowed_generations` (from the UniFi product catalog) are considered approved for that sovereign/site. diff --git a/docs/erd.md b/docs/erd.md new file mode 100644 index 0000000..1027e53 --- /dev/null +++ b/docs/erd.md @@ -0,0 +1,70 @@ +# Database ERD + +## Entity relationship overview + +```mermaid +erDiagram + org_units ||--o{ org_units : parent + org_units ||--o{ users : org_unit + users ||--o{ user_roles : user + roles ||--o{ user_roles : role + sites ||--o{ user_roles : scope_site + + vendors ||--o{ vendor_bank_details : vendor + vendors ||--o{ offers : vendor + vendors ||--o{ purchase_orders : vendor + + regions ||--o{ sites : region + sites ||--o{ rooms : site + rooms ||--o{ rows : room + rows ||--o{ racks : row + racks ||--o{ positions : rack + sites ||--o{ assets : site + positions ||--o{ assets : position + users ||--o{ assets : owner + + assets ||--o{ asset_components : parent + assets ||--o{ asset_components : child + assets ||--o{ provisioning_records : asset + assets ||--o{ maintenances : asset + + purchase_orders }o--|| sites : inspection_site + purchase_orders }o--|| sites : delivery_site + purchase_orders ||--o{ shipments : po + users ||--o{ audit_events : actor + + org_units { uuid id text name uuid parent_id text org_id } + users { uuid id text email text org_id uuid org_unit_id } + vendors { uuid id text org_id text legal_name text trust_tier } + offers { uuid id text org_id uuid vendor_id int quantity decimal unit_price text status } + purchase_orders { uuid id text org_id uuid vendor_id jsonb line_items text status } + shipments { uuid id uuid purchase_order_id text tracking text status } + regions { uuid id text org_id text name } + sites { uuid id text org_id uuid region_id text name jsonb network_metadata } + rooms { uuid id uuid site_id text name } + rows { uuid id uuid room_id text name } + racks { uuid id uuid row_id text name int ru_total jsonb power_feeds } + positions { uuid id uuid rack_id int ru_start int ru_end uuid asset_id } + assets { uuid id text org_id text asset_id text category text status uuid site_id uuid position_id } + asset_components { uuid id uuid parent_asset_id uuid child_asset_id text role } + provisioning_records { uuid id uuid asset_id text hypervisor_node text cluster_id } + maintenances { uuid id text org_id uuid asset_id text type text status } + audit_events { uuid id text org_id uuid actor_id text action text resource_type text resource_id jsonb before_state jsonb after_state timestamp occurred_at } + roles { uuid id text name jsonb permissions } + user_roles { uuid user_id uuid role_id uuid scope_site_id text scope_project_id } +``` + +## Core tables + +- **org_units**, **users**: Tenancy and org hierarchy. +- **vendors**, **vendor_bank_details**: Vendor master; versioned bank details with dual approval. +- **offers**: SKU/MPN, quantity, price, evidence_refs, risk_score, status. +- **purchase_orders**: Line items, approval_stage, escrow_terms, inspection_site_id, delivery_site_id. +- **shipments**: PO link, tracking, customs_docs_refs. +- **regions**, **sites**, **rooms**, **rows**, **racks**, **positions**: Site hierarchy and RU mapping. +- **assets**: asset_id, category, serials, proof_artifact_refs, site_id, position_id, status, chain_of_custody. +- **asset_components**: parent_asset_id, child_asset_id, role (gpu/cpu/dimm/nic). +- **provisioning_records**: OS image, hypervisor node, cluster_id. +- **maintenances**: RMA/incident/part_swap; vendor_ticket_ref. +- **audit_events**: Append-only; actor_id, action, resource_type, resource_id, before_state, after_state. +- **roles**, **user_roles**: RBAC; scope_site_id, scope_project_id for ABAC. diff --git a/docs/integration-spec-proxmox.md b/docs/integration-spec-proxmox.md new file mode 100644 index 0000000..80ed8a1 --- /dev/null +++ b/docs/integration-spec-proxmox.md @@ -0,0 +1,2 @@ +# Proxmox integration spec +Use cases: nodes, inventory. Auth: token per site (Vault). Map Asset to node via integration_mappings. diff --git a/docs/integration-spec-redfish.md b/docs/integration-spec-redfish.md new file mode 100644 index 0000000..44e8479 --- /dev/null +++ b/docs/integration-spec-redfish.md @@ -0,0 +1,2 @@ +# Redfish integration spec +Use cases: verify serials, power cycle. Credentials in Vault per site. diff --git a/docs/integration-spec-unifi.md b/docs/integration-spec-unifi.md new file mode 100644 index 0000000..1330387 --- /dev/null +++ b/docs/integration-spec-unifi.md @@ -0,0 +1,21 @@ +# UniFi integration spec + +UniFi is positioned as a **hardware identity and telemetry source**, a **product-line intelligence feed**, and a **procurement and lifecycle signal**—not only as networking gear. The platform integrates UniFi OS, UniFi Network Application, firmware catalogs, device generation, and support-horizon mapping so Sankofa Phoenix can answer: what exact hardware is deployed, what generation and firmware lineage, what support status, and is this infrastructure policy-compliant for this sovereign body? + +**Use cases:** Discover devices, map ports, push port profiles; plus hardware identity, EoL/support horizon, and compliance-relevant metadata. Auth: API token per site (Vault). Sync: nightly; store in integration_mappings. + +## UniFi Product Intelligence layer + +UniFi is used as a **hardware identity and telemetry source**, not only networking. The platform maintains a canonical **UniFi product catalog** (`unifi_product_catalog`) with: + +- SKU, model name, generation (Gen1 / Gen2 / Enterprise) +- Performance class, EoL date, support horizon +- `approved_sovereign_default` for purchasing and compliance + +**API:** `GET /api/v1/integrations/unifi/product-catalog` (optional `?generation=`, `?approved_sovereign=true`), `GET /api/v1/integrations/unifi/product-catalog/:sku`. Device list `GET .../unifi/sites/:siteId/devices` returns devices enriched with `generation` and `support_horizon` from the catalog when the device model matches. + +This layer feeds **purchasing** (approved buy lists, BOMs) and **compliance** (approved SKUs per sovereign, support-risk views). + +## Sovereign-safe controller architecture + +Per-sovereign UniFi controller domains with no cross-sovereign write. See [sovereign-controller-topology.md](sovereign-controller-topology.md) for the diagram and trust boundaries. Optionally store controller endpoints in the `unifi_controllers` table (org_id, site_id, base_url, role: sovereign_write | oversight_read_only, region); credentials remain in Vault. API: CRUD under `GET/POST/PATCH/DELETE /api/v1/unifi-controllers`, scoped by org_id. diff --git a/docs/next-steps-before-swagger-and-ui.md b/docs/next-steps-before-swagger-and-ui.md new file mode 100644 index 0000000..33fccbc --- /dev/null +++ b/docs/next-steps-before-swagger-and-ui.md @@ -0,0 +1,111 @@ +# Next steps before full Swagger docs and UX/UI + +Do these in order so the API contract is stable and the front end has a clear target. + +--- + +## 1. Auth and identity + +- **Login / token endpoint** + There is no in-app login. JWTs are assumed to come from an external IdP. Before UI: + - Either add **POST /auth/login** (or /auth/token) that accepts credentials, looks up `users` + `user_roles`, and returns a JWT with `roles` and (for vendor users) `vendorId`, **or** + - Document the exact JWT shape and how your IdP must set `roles` and `vendorId` so the UI can integrate. +- **User and role management (optional)** + Schema has `users`, `roles`, `user_roles`, but no API. For a self-contained product, add **CRUD for users** and **assignment of roles** (and `vendor_id` for vendor users) so admins can onboard users and vendors without touching the DB directly. + +--- + +## 2. API contract and behavior + +- **Request validation** + Add JSON Schema (or Zod) for request bodies and path/query params on all routes so invalid input returns **400** with a consistent error shape instead of 500 or undefined behavior. +- **Error response format** + Standardize error payloads (e.g. `{ error: string, code?: string, details?: unknown }`) and document them so Swagger and the UI can show the same errors. +- **Optional: list pagination** + List endpoints (vendors, offers, assets, sites, etc.) return full arrays. Add `limit`/`offset` or `page`/`pageSize` and a total/cursor so the UI and docs can assume a stable list contract. + +--- + +## 3. RBAC enforcement + +- **Wire permissions to routes** + `requirePermission` exists but is not used on route handlers. For each route, add the appropriate `requirePermission(...)` (or equivalent) so that missing permission returns **403** with a clear message. This makes the API safe to document and use from the UI. + +--- + +## 4. OpenAPI completeness (prerequisite for Swagger) + +- **Document all paths** + OpenAPI currently documents only health, vendors, offers, purchase-orders, and ingestion. Add the rest so Swagger matches the real API: + - **Assets**: GET/POST /assets, GET/PATCH/DELETE /assets/:id + - **Sites**: GET/POST /sites, GET/PATCH/DELETE /sites/:id, and nested (rooms, rows, racks, positions) if exposed + - **Workflow**: POST /workflow/offers/:id/risk-score, POST /workflow/purchase-orders/:id/submit, approve, reject, PATCH status + - **Inspection**: templates and runs + - **Shipments**: CRUD + - **Asset components**: CRUD + - **Capacity**: GET endpoints + - **Integrations**: UniFi, product-catalog, Proxmox, mappings + - **Maintenances**: CRUD + - **Compliance profiles**: CRUD + - **UniFi controllers**: CRUD + - **Reports**: BOM, support-risk + - **Upload**: POST /upload (multipart) +- **Request/response schemas** + For each path, add `requestBody` and `responses` with schema (or $ref to `components/schemas`) so Swagger can show request/response bodies and generate client types. +- **Security per path** + Mark which paths use BearerAuth, which use IngestionApiKey, and which are public (e.g. health). + +--- + +## 5. Environment and config + +- **env.example** + Add `INGESTION_API_KEY` (and any OIDC/SSO vars if you add login) so deployers and the docs know what to set. +- **API base URL for web** + Ensure the web app can be configured with the API base URL (e.g. env `VITE_API_URL` or similar) so Swagger and the UI both target the same backend. + +--- + +## 6. Testing + +- **Stabilize the contract** + Add or expand API tests for critical paths (e.g. vendors, offers, purchase-orders, workflow, ingestion) so that when you add Swagger and the UI, changes to the API are caught by tests. +- **Optional: contract tests** + Consider testing that responses match a minimal schema (e.g. required fields) so the OpenAPI spec and the implementation stay in sync. + +--- + +## 7. Web app baseline (before full UX/UI) + +- **API client** + Add a minimal API client (fetch or axios) that sends the JWT (and `x-org-id` if required) so all UI calls go through one place and can be swapped for generated clients later. +- **Auth in the client** + Implement login (or redirect to IdP), store the token, and attach it to every request; handle 401 (e.g. redirect to login or refresh). +- **Feature flags or minimal nav** + Add a simple nav or list of areas (e.g. Vendors, Offers, Purchase orders, Assets, Sites) so the “full UX/UI” phase can fill in one screen at a time without redoing routing. + +--- + +## 8. Then: full Swagger and UX/UI + +After the above: + +- **Full Swagger** + Serve the OpenAPI spec (e.g. from `/api/openapi.json` or `/api/docs`) and mount Swagger UI (or Redoc) so all operations and schemas are discoverable and try-it-now works. +- **Full UX/UI** + Build out screens, forms, and flows using the stable API and client; keep OpenAPI and the UI in sync via the same base URL and error format. + +--- + +## Summary checklist + +| # | Area | Action | +|---|-------------------|--------| +| 1 | Auth | Login/token endpoint or IdP contract; optional users/roles API | +| 2 | API contract | Request validation; consistent error format; optional pagination | +| 3 | RBAC | Use requirePermission on routes; return 403 where appropriate | +| 4 | OpenAPI | Document all paths, request/response schemas, security | +| 5 | Env | env.example (INGESTION_API_KEY, etc.); web API base URL | +| 6 | Tests | Broader API tests; optional contract/schema tests | +| 7 | Web baseline | API client, auth (token + 401), minimal nav/routes | +| 8 | Swagger + UI | Serve spec + Swagger UI; build out full screens | diff --git a/docs/observability.md b/docs/observability.md new file mode 100644 index 0000000..864ad1d --- /dev/null +++ b/docs/observability.md @@ -0,0 +1,2 @@ +# Observability +Central logging: ELK or OpenSearch. Metrics: Prometheus + Grafana. Alerting on API errors and integration sync failures. API uses Fastify logger (structured). diff --git a/docs/offer-ingestion.md b/docs/offer-ingestion.md new file mode 100644 index 0000000..62c1952 --- /dev/null +++ b/docs/offer-ingestion.md @@ -0,0 +1,99 @@ +# Offer ingestion (scrape and email) + +Offers can be ingested from external sources so they appear in the database for potential purchases, without manual data entry. + +## Sources + +1. **Scraped** – e.g. site content from theserverstore.com (Peter as Manager). A scraper job fetches pages, parses offer-like content, and creates offer records. +2. **Email** – a dedicated mailbox accepts messages (e.g. from Sergio and others); a pipeline parses them and creates offer records. + +Ingested offers are stored with: + +- `source`: `scraped` or `email` +- `source_ref`: URL (scrape) or email message id (email) +- `source_metadata`: optional JSON (e.g. sender, subject, page title, contact name) +- `ingested_at`: timestamp of ingestion +- `vendor_id`: optional; may be null until procurement assigns the offer to a vendor + +## API: ingestion endpoint + +Internal or automated callers use a dedicated endpoint, secured by an API key (no user JWT). + +**POST** `/api/v1/ingestion/offers` + +- **Auth:** Header `x-ingestion-api-key` must equal the environment variable `INGESTION_API_KEY`. If missing or wrong, returns `401`. +- **Org:** Header `x-org-id` (default `default`) specifies the org for the new offer. + +**Body (JSON):** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `source` | `"scraped"` \| `"email"` | yes | Ingestion source | +| `source_ref` | string | no | URL or message id | +| `source_metadata` | object | no | e.g. `{ "sender": "Sergio", "subject": "...", "page_url": "..." }` | +| `vendor_id` | UUID | no | Vendor to attach; omit for unassigned | +| `sku` | string | no | | +| `mpn` | string | no | | +| `quantity` | number | yes | | +| `unit_price` | string | yes | Decimal | +| `incoterms` | string | no | | +| `lead_time_days` | number | no | | +| `country_of_origin` | string | no | | +| `condition` | string | no | | +| `warranty` | string | no | | +| `evidence_refs` | array | no | `[{ "key": "s3-key", "hash": "..." }]` | + +**Response:** `201` with the created offer (including `id`, `source`, `source_ref`, `source_metadata`, `ingested_at`). + +Example (scrape): + +```json +{ + "source": "scraped", + "source_ref": "https://theserverstore.com/...", + "source_metadata": { "contact": "Peter", "site": "theserverstore.com" }, + "vendor_id": null, + "sku": "DL380-G9", + "quantity": 2, + "unit_price": "450.00", + "condition": "refurbished" +} +``` + +Example (email): + +```json +{ + "source": "email", + "source_ref": "msg-12345", + "source_metadata": { "from": "sergio@example.com", "subject": "Quote for R630" }, + "vendor_id": null, + "mpn": "PowerEdge R630", + "quantity": 1, + "unit_price": "320.00" +} +``` + +## Scraper (e.g. theserverstore.com) + +- **Responsibility:** Fetch pages (respecting robots.txt and rate limits), extract product/offer fields, then POST to `POST /api/v1/ingestion/offers` for each offer. +- **Where:** Can run as a scheduled job in `apps/` or `packages/`, or as an external service that calls the API. No scraper implementation is in-repo yet; this doc defines the contract. +- **Vendor:** If the site is known (e.g. The Server Store, Peter as Manager), the scraper can resolve or create a vendor and pass `vendor_id`; otherwise leave null for procurement to assign later. +- **Idempotency:** Use `source_ref` (e.g. canonical product URL) so the same offer is not duplicated; downstream you can upsert by `(org_id, source, source_ref)` if desired. + +## Email intake (e.g. Sergio and others) + +- **Flow:** Incoming messages to a dedicated mailbox (e.g. `offers@your-org.com`) are read by an IMAP poller or processed via an inbound webhook (SendGrid, Mailgun, etc.). The pipeline parses sender, subject, body, and optional attachments, then POSTs one or more payloads to `POST /api/v1/ingestion/offers`. +- **Storing raw email:** Attachments or full message can be uploaded to object storage (e.g. S3/MinIO) and referenced in `evidence_refs` or `source_metadata` (e.g. `raw_message_key`). +- **Vendor matching:** Match sender address or name to an existing vendor and set `vendor_id` when possible; otherwise leave null and set `source_metadata.sender` / `from` for later assignment. + +## Configuration + +- Set `INGESTION_API_KEY` in the environment where the API runs. Scraper and email pipeline must use the same value in `x-ingestion-api-key`. +- Use `x-org-id` on each request to target the correct org. + +## Procurement workflow + +- Ingested offers appear in the offers list with `source` = `scraped` or `email` and optional `vendor_id`. +- Offers with `vendor_id` null are “unassigned”; procurement can assign them to a vendor (PATCH offer or create/link vendor then update offer). +- Existing RBAC and org/site scoping apply; audit can track creation via `ingested_at` and `source_metadata`. diff --git a/docs/openapi.yaml b/docs/openapi.yaml new file mode 100644 index 0000000..6be0f94 --- /dev/null +++ b/docs/openapi.yaml @@ -0,0 +1,175 @@ +openapi: 3.0.3 +info: + title: Sankofa HW Infra API + version: 0.1.0 +servers: + - url: /api/v1 +security: + - BearerAuth: [] +components: + schemas: + ApiError: + type: object + properties: + error: { type: string, description: Human-readable message } + code: { type: string, enum: [BAD_REQUEST, UNAUTHORIZED, FORBIDDEN, NOT_FOUND, CONFLICT, INTERNAL_ERROR] } + details: { type: object, description: Optional validation or extra data } + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: JWT with optional vendorId for vendor users + IngestionApiKey: + type: apiKey + in: header + name: x-ingestion-api-key + description: Required for POST /ingestion/offers (env INGESTION_API_KEY) +paths: + /health: + get: + summary: Health + security: [] + /auth/token: + post: + summary: Get JWT token + description: Exchange email (and optional password) for a JWT with roles and vendorId. No auth required. + security: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [email] + properties: + email: { type: string, format: email } + password: { type: string } + responses: + "200": + description: Token and user info + "401": + description: Invalid credentials + /vendors: + get: + summary: List vendors + description: If JWT contains vendorId (vendor user), returns only that vendor. + post: + summary: Create vendor + description: Forbidden for vendor users. + /vendors/{id}: + get: + summary: Get vendor + description: Vendor users may only request their own vendor id. + /offers: + get: + summary: List offers + description: If JWT contains vendorId, returns only that vendor's offers. + post: + summary: Create offer + description: Vendor users' vendorId is forced to their vendor. + /offers/{id}: + get: + summary: Get offer + patch: + summary: Update offer + delete: + summary: Delete offer + /purchase-orders: + get: + summary: List purchase orders + description: If JWT contains vendorId, returns only POs for that vendor. + /purchase-orders/{id}: + get: + summary: Get purchase order + /ingestion/offers: + post: + summary: Ingest offer (scrape or email) + description: Creates an offer with source (scraped|email), source_ref, source_metadata. Secured by x-ingestion-api-key only; no JWT. Use x-org-id for target org. + security: + - IngestionApiKey: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [source, quantity, unit_price] + properties: + source: + type: string + enum: [scraped, email] + source_ref: + type: string + description: URL or email message id + source_metadata: + type: object + vendor_id: + type: string + format: uuid + nullable: true + sku: + type: string + mpn: + type: string + quantity: + type: integer + unit_price: + type: string + incoterms: + type: string + lead_time_days: + type: integer + country_of_origin: + type: string + condition: + type: string + warranty: + type: string + evidence_refs: + type: array + items: + type: object + properties: + key: { type: string } + hash: { type: string } + responses: + "201": + description: Offer created + "401": + description: Invalid or missing x-ingestion-api-key + /capacity/sites/{siteId}: + get: + summary: RU utilization for a site + description: Returns usedRu, totalRu, utilizationPercent for the site (from racks and assigned positions). + parameters: + - name: siteId + in: path + required: true + schema: { type: string, format: uuid } + responses: + "200": + description: Site capacity (usedRu, totalRu, utilizationPercent) + "404": + description: Site not found + /capacity/sites/{siteId}/power: + get: + summary: Power headroom for a site + description: Returns circuitLimitWatts from rack power_feeds; measuredDrawWatts/headroomWatts null until Phase 4. + parameters: + - name: siteId + in: path + required: true + schema: { type: string, format: uuid } + responses: + "200": + description: Power info (circuitLimitWatts, measuredDrawWatts, headroomWatts) + "404": + description: Site not found + /capacity/gpu-inventory: + get: + summary: GPU inventory + description: Returns total, bySite, and byType (part number) counts. + responses: + "200": + description: GPU counts (total, bySite, byType) diff --git a/docs/operational-baseline.md b/docs/operational-baseline.md new file mode 100644 index 0000000..b0ea8ff --- /dev/null +++ b/docs/operational-baseline.md @@ -0,0 +1,95 @@ +# Operational baseline — current hardware running / in-hand + +Hardware already deployed, active, or physically in-hand (not part of available wholesale inventory). Quantities marked **TBD** are to be confirmed and locked during physical audit. Once confirmed, this document is the **authoritative operational baseline** for Sankofa Phoenix. + +--- + +## A1. Compute servers (operational) + +### HPE ProLiant ML110 series + +- **Role:** Core services / management / utility workloads +- **Form factor:** Tower / rack-convertible +- **Status:** Running / in-hand +- **Quantity:** TBD +- **Notes:** Suitable for control-plane services, monitoring, identity, light virtualization + +### Dell PowerEdge R630 + +- **Role:** General compute / virtualization / legacy workloads +- **Form factor:** 1U rackmount +- **Status:** Running / in-hand +- **Quantity:** TBD +- **Notes:** Ideal for Proxmox clusters, utility VMs, staging environments + +--- + +## A2. Network and edge infrastructure + +### UniFi Dream Machine Pro (UDM Pro) + +- **Role:** Edge gateway, UniFi OS controller, firewall +- **Status:** Running / in-hand +- **Quantity:** TBD +- **Notes:** Per-site edge control; candidate for per-sovereign controller domains + +### UniFi XG switches + +- **Role:** High-throughput aggregation / core switching +- **Status:** Running / in-hand +- **Quantity:** TBD +- **Notes:** 10G/25G backbone for compute and storage traffic + +--- + +## A3. ISP and external connectivity + +### Spectrum Business cable modems + +- **Role:** Primary or secondary WAN connectivity +- **Status:** Installed / in-hand +- **Quantity:** TBD +- **Notes:** Business-class internet access; typically paired with UDM Pro + +--- + +## A4. Physical infrastructure and power + +### APC equipment cabinets + +- **Role:** Secure rack enclosure +- **Status:** Installed / in-hand +- **Quantity:** TBD +- **Notes:** Houses compute, network, and power equipment + +### APC UPS units + +- **Role:** Power conditioning and battery backup +- **Status:** Installed / in-hand +- **Quantity:** TBD +- **Notes:** Runtime and load to be captured per site for capacity planning + +--- + +## A5. Operational classification summary + +| Category | Status | Quantity | +| ------------------- | --------- | -------- | +| ML110 servers | Running | TBD | +| Dell R630 servers | Running | TBD | +| UDM Pro | Running | TBD | +| UniFi XG switches | Running | TBD | +| Spectrum modems | Installed | TBD | +| APC cabinets | Installed | TBD | +| APC UPS units | Installed | TBD | + +--- + +## A6. Next actions (to finalize baseline) + +1. **Physical audit** — Lock quantities and serials per site/rack. +2. **Import into sankofa-hw-infra** — Create as **Operational Assets** (assets with category, site, rack position). +3. **Attach to sites, racks, power feeds** — Populate site hierarchy and power metadata. +4. **Enable integrations** — UniFi (device mapping), Proxmox (node ↔ server), UPS monitoring where supported. + +After quantities and serials are confirmed, this appendix is the authoritative operational baseline for capacity planning, BOM, and compliance. diff --git a/docs/purchasing-feedback-loop.md b/docs/purchasing-feedback-loop.md new file mode 100644 index 0000000..832325b --- /dev/null +++ b/docs/purchasing-feedback-loop.md @@ -0,0 +1,29 @@ +# Purchasing feedback loop + +How UniFi telemetry and product intelligence drive approved buy lists, BOMs, and support-risk views. + +## Data flow + +1. **UniFi device sync** — Devices are synced from each sovereign’s controller; device list includes model/SKU. +2. **Product catalog lookup** — Each device model/SKU is matched against `unifi_product_catalog` (generation, EoL, support horizon). +3. **Outputs:** + - **SKU-normalized BOM** per sovereign/site: which exact hardware is deployed, with generation and support status. + - **Support-risk heatmap:** devices near EoL or with short support horizon. + - **Firmware divergence alerts:** when firmware versions drift from policy (see compliance profiles). + - **Approved purchasing catalog:** only SKUs that meet the sovereign’s compliance profile (allowed generations, approved_skus). + +## Approved buy list + +The “approved buy list” is the intersection of: + +- Devices in use or recommended (from UniFi + catalog), and +- Catalog entries with `approved_sovereign_default` or matching the org’s **compliance profile** (allowed_generations, approved_skus). + +So operations (what we have and what’s supported) drives procurement (what we’re allowed to buy), not the other way around. + +## Optional API + +- `GET /api/v1/reports/bom?org_id=&site_id=` — Aggregate assets + UniFi mappings + catalog for a BOM. +- `GET /api/v1/reports/support-risk?org_id=&horizon_months=12` — Devices with EoL or support horizon within the next N months. + +These can be implemented as thin wrappers over existing schema, `unifi_product_catalog`, and `integration_mappings`. diff --git a/docs/rbac-sovereign-operations.md b/docs/rbac-sovereign-operations.md new file mode 100644 index 0000000..7d867b3 --- /dev/null +++ b/docs/rbac-sovereign-operations.md @@ -0,0 +1,36 @@ +# RBAC matrix for sovereign operations + +Who can **see**, who can **change**, and who can **approve** (by role and by site/sovereign) for UniFi, compliance, and purchasing. + +## Permissions + +| Permission | Description | +|------------|-------------| +| unifi:read | Read UniFi devices and product catalog within assigned site/org | +| unifi:write | Change UniFi mappings and controller config within assigned site/org | +| unifi_oversight:read | Read-only across sovereigns (central oversight; no write) | +| compliance:read | View compliance profiles | +| compliance:write | Create/update/delete compliance profiles | +| purchasing_catalog:read | View approved buy lists and BOMs | + +## Role vs permission (sovereign-relevant) + +| Role | unifi:read | unifi:write | unifi_oversight:read | compliance:read | compliance:write | purchasing_catalog:read | +|------|:----------:|:-----------:|:--------------------:|:----------------:|:-----------------:|:------------------------:| +| super_admin | yes | yes | yes | yes | yes | yes | +| security_admin | | | yes | yes | yes | | +| procurement_manager | yes | | | | | yes | +| finance_approver | | | | | | yes | +| site_admin | yes | yes | | yes | | | +| noc_operator | yes | | | | | | +| read_only_auditor | yes | | | yes | | yes | +| partner_inspector | | | | | | | + +## Scoping rules + +- **unifi:read** and **unifi:write** apply only within the operator’s assigned **site** or **org** (via `user_roles.scope_site_id` / org). No cross-sovereign write. +- **unifi_oversight:read** is the only cross-sovereign read; used by central Sankofa Phoenix oversight. No write authority. +- **compliance:read** / **compliance:write** are scoped by org (sovereign); enforce in API so users only see/edit profiles for their org. +- **purchasing_catalog:read** is scoped by org/site so approved lists and BOMs are sovereign-specific. + +Existing ABAC (e.g. `scope_site_id` on user_roles) enforces these boundaries; ensure new integration and compliance endpoints check permission and org/site scope. diff --git a/docs/runbooks/incident-response.md b/docs/runbooks/incident-response.md new file mode 100644 index 0000000..95e8aba --- /dev/null +++ b/docs/runbooks/incident-response.md @@ -0,0 +1,4 @@ +# Incident response runbook +1. Triage: check audit log and health endpoints. +2. Isolate affected assets or revoke credentials if compromise. +3. Notify; post-mortem and update runbooks. diff --git a/docs/runbooks/provisioning-and-integration.md b/docs/runbooks/provisioning-and-integration.md new file mode 100644 index 0000000..5ef6997 --- /dev/null +++ b/docs/runbooks/provisioning-and-integration.md @@ -0,0 +1,6 @@ +# Runbook: Provisioning and integration checks + +- **Proxmox**: Register node; add mapping via POST /api/v1/integrations/mappings (provider=proxmox, externalId=node name). Sync nodes via scheduled job or manual trigger. +- **UniFi**: Map switch/port to rack position; store in integration_mappings with metadata (device id, port index). +- **Redfish**: At receiving, optionally call Redfish to verify serial and firmware; store result in asset proof artifacts. +- **Checks**: Verify mapping exists for asset before provisioning; confirm credentials in Vault for the site. diff --git a/docs/runbooks/receiving-and-inspection.md b/docs/runbooks/receiving-and-inspection.md new file mode 100644 index 0000000..8a176dd --- /dev/null +++ b/docs/runbooks/receiving-and-inspection.md @@ -0,0 +1,9 @@ +# Runbook: Receiving and Inspection + +## Inspection +1. Create inspection run from template. +2. Upload evidence; set pass/fail. +3. If fail: claim. If pass: approve release. + +## Receiving +1. Reconcile shipment with PO. 2. Assign rack; set asset Received then Staged. diff --git a/docs/runbooks/receiving-and-racking.md b/docs/runbooks/receiving-and-racking.md new file mode 100644 index 0000000..b46ec28 --- /dev/null +++ b/docs/runbooks/receiving-and-racking.md @@ -0,0 +1,13 @@ +# Runbook: Receiving and Racking + +## Receiving +1. Create shipment for PO; scan items. +2. POST /api/v1/shipments/:id/receive with assetIds to set assets to received. +3. Update shipment status to received. + +## Racking +1. Assign asset to position: PATCH /api/v1/assets/:id with positionId. +2. Set asset status to staged. + +## Capacity +GET /api/v1/capacity/sites/:siteId and GET /api/v1/capacity/gpu-inventory for dashboards. diff --git a/docs/security.md b/docs/security.md new file mode 100644 index 0000000..70efc75 --- /dev/null +++ b/docs/security.md @@ -0,0 +1,2 @@ +# Security +Secrets: Vault/KMS; rotate API tokens. MFA for privileged roles. Dual control: vendor bank details and PO final approval (Phase 1). Attachment malware scanning (Phase 4). Data retention policies by doc type. diff --git a/docs/sovereign-controller-topology.md b/docs/sovereign-controller-topology.md new file mode 100644 index 0000000..cdd6d2a --- /dev/null +++ b/docs/sovereign-controller-topology.md @@ -0,0 +1,42 @@ +# Sovereign controller topology + +Per-sovereign UniFi controller domains, regionally isolated management planes, and a central read-only oversight layer. No cross-sovereign write authority. + +## Diagram + +```mermaid +flowchart TB + subgraph sovereignA [Sovereign A] + CtrlA[UniFi Controller A] + CtrlA -->|write| NetA[Network A] + end + + subgraph sovereignB [Sovereign B] + CtrlB[UniFi Controller B] + CtrlB -->|write| NetB[Network B] + end + + subgraph oversight [Central oversight] + Phoenix[Sankofa Phoenix] + end + + Phoenix -->|read only| CtrlA + Phoenix -->|read only| CtrlB + CtrlA -.->|no write| CtrlB + CtrlB -.->|no write| CtrlA +``` + +## Architecture + +- **Per-sovereign controller domains:** Each sovereign (org/tenant) has its own UniFi controller(s). Write authority stays within that sovereign. +- **Regional isolation:** Controllers and management planes can be deployed per region so data and control stay in-region. +- **Central read-only oversight:** Sankofa Phoenix has a read-only view across controllers for audit, BOM, support-risk, and compliance—no write into any sovereign’s controller. +- **Trust boundaries:** No cross-sovereign write; sovereign A cannot change sovereign B’s network or config. + +This satisfies sovereignty, auditability, compartmentalization, and trust boundaries between different bodies (e.g. governmental). + +## Optional: controller registry + +If you store controller endpoints in the DB, use a table `unifi_controllers` with: org_id, site_id (optional), base_url, role (sovereign_write | oversight_read_only), region. Credentials remain in Vault; the table only stores topology and role. API: CRUD for controllers scoped by org_id. + +See [integration-spec-unifi.md](integration-spec-unifi.md) for the “Sovereign-safe controller architecture” subsection. diff --git a/docs/vendor-portal.md b/docs/vendor-portal.md new file mode 100644 index 0000000..1c38553 --- /dev/null +++ b/docs/vendor-portal.md @@ -0,0 +1,49 @@ +# Vendor portal and vendor users + +Selected vendors can log in to assist in fulfilling needs: view and update their offers, and see purchase orders relevant to them. + +## Model + +- **Vendor user:** A user record with `vendor_id` set is a *vendor user*. That user can be assigned the role `vendor_user` and receive a JWT that includes `vendorId` in the payload. +- **Scoping:** When the API sees `req.user.vendorId`, it restricts: + - **Vendors:** List returns only that vendor; GET/PATCH/DELETE only for that vendor; POST (create vendor) is forbidden. + - **Offers:** List/GET/PATCH/DELETE only offers for that vendor; on create, `vendorId` is forced to the logged-in vendor. + - **Purchase orders:** List/GET only POs for that vendor. + +## Onboarding a vendor user + +1. Create or select a **Vendor** in the org (e.g. "The Server Store", "Sergio's Hardware"). +2. Create a **User** with: + - `org_id` = same as org + - `vendor_id` = that vendor's ID + - `email` / `name` as needed +3. Assign the role **vendor_user** to that user (via your IdP or `user_roles` if you manage roles in-app). +4. At **login**, ensure the issued JWT includes: + - `roles`: e.g. `["vendor_user"]` + - `vendorId`: the vendor's UUID + +Then the vendor can call the same API under `/api/v1` with that JWT (and `x-org-id`). They will only see and modify data for their vendor. + +## Permissions for vendor_user + +The role `vendor_user` has: + +- `vendor:read_own` – read own vendor +- `vendor:write_offers_own` – create/update own offers +- `vendor:view_pos_own` – view POs for their vendor +- `offers:read`, `offers:write` – used in combination with `vendorId` scoping above +- `purchase_orders:read` – used with vendor filter + +Vendor users cannot create/update/delete vendor records, nor see other vendors' offers or POs. + +## API surface (vendor portal) + +Vendor users use the same endpoints as procurement, with automatic scoping: + +- **GET /api/v1/vendors** – Returns only their vendor. +- **GET /api/v1/vendors/:id** – Allowed only when `:id` is their vendor. +- **GET/POST/PATCH/DELETE /api/v1/offers** – Only their vendor's offers; POST forces their vendorId. +- **GET /api/v1/purchase-orders** – Only POs where vendorId is their vendor. +- **GET /api/v1/purchase-orders/:id** – Allowed only for POs of their vendor. + +No changes to URLs or request bodies are required; scoping is derived from the JWT `vendorId`. diff --git a/env.example b/env.example new file mode 100644 index 0000000..b5dfe6e --- /dev/null +++ b/env.example @@ -0,0 +1,30 @@ +# API +NODE_ENV=development +API_PORT=4000 +API_HOST=0.0.0.0 + +# Database (match infra/docker-compose.yml for local dev) +DATABASE_URL=postgres://sankofa:sankofa_dev@localhost:5432/sankofa + +# Object storage (MinIO for dev when using profile 'full') +S3_ENDPOINT=http://localhost:9000 +S3_ACCESS_KEY=sankofa +S3_SECRET_KEY=sankofa_dev_minio +S3_BUCKET=sankofa-documents +S3_REGION=us-east-1 +S3_USE_SSL=false + +# JWT (generate a secret in production) +JWT_SECRET=change-me-in-production-use-openssl-rand-base64-32 + +# Ingestion (scraper / email pipeline) +INGESTION_API_KEY=set-a-secret-key-for-ingestion-endpoint + +# Web app (optional; dev proxy uses /api -> localhost:4000) +# VITE_API_URL=http://localhost:4000 + +# Optional: SSO placeholder +# OIDC_ISSUER= +# OIDC_CLIENT_ID= +# OIDC_CLIENT_SECRET= +# SAML_ENTRY_POINT= diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..bd9be12 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,17 @@ +import js from "@eslint/js"; +import tseslint from "typescript-eslint"; + +export default [ + js.configs.recommended, + ...tseslint.configs.recommended, + { + ignores: [ + "**/dist/**", + "**/node_modules/**", + "**/build/**", + "**/coverage/**", + "**/*.config.js", + "**/*.config.ts", + ], + }, +]; diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml new file mode 100644 index 0000000..3d6426a --- /dev/null +++ b/infra/docker-compose.yml @@ -0,0 +1,40 @@ +# Development infrastructure: Postgres, S3-compatible (MinIO), optional Vault dev +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_USER: sankofa + POSTGRES_PASSWORD: sankofa_dev + POSTGRES_DB: sankofa + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U sankofa -d sankofa"] + interval: 5s + timeout: 5s + retries: 5 + + minio: + image: minio/minio:latest + command: server /data + environment: + MINIO_ROOT_USER: sankofa + MINIO_ROOT_PASSWORD: sankofa_dev_minio + ports: + - "9000:9000" + - "9001:9001" + volumes: + - minio_data:/data + healthcheck: + test: ["CMD", "mc", "ready", "local"] + interval: 5s + timeout: 5s + retries: 5 + profiles: + - full + +volumes: + postgres_data: + minio_data: diff --git a/package.json b/package.json new file mode 100644 index 0000000..efc1cc5 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "sankofa-hw-infra", + "version": "0.1.0", + "private": true, + "description": "Hardware procurement, inventory, and operations platform", + "scripts": { + "build": "pnpm -r run build", + "test": "pnpm -r run test", + "lint": "eslint apps packages", + "dev": "pnpm -r run dev --parallel", + "db:migrate": "pnpm --filter @sankofa/schema run db:migrate", + "db:generate": "pnpm --filter @sankofa/schema run db:generate" + }, + "engines": { + "node": ">=20" + }, + "packageManager": "pnpm@9.14.2", + "devDependencies": { + "@eslint/js": "^9.15.0", + "eslint": "^9.15.0", + "typescript-eslint": "^8.15.0" + } +} diff --git a/packages/auth/package.json b/packages/auth/package.json new file mode 100644 index 0000000..422eee1 --- /dev/null +++ b/packages/auth/package.json @@ -0,0 +1,22 @@ +{ + "name": "@sankofa/auth", + "version": "0.1.0", + "private": true, + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "test": "vitest run", + "lint": "eslint src --ext .ts" + }, + "dependencies": { + "@sankofa/schema": "workspace:*" + }, + "devDependencies": { + "@types/node": "^22.10.0", + "eslint": "^9.15.0", + "typescript": "^5.7.0", + "vitest": "^2.1.0" + } +} diff --git a/packages/auth/src/index.test.ts b/packages/auth/src/index.test.ts new file mode 100644 index 0000000..7cf4da5 --- /dev/null +++ b/packages/auth/src/index.test.ts @@ -0,0 +1,6 @@ +import { describe, it, expect } from "vitest"; +import { hasPermission, ROLES } from "./index"; +describe("auth", () => { + it("super_admin has permission", () => { expect(hasPermission(["super_admin"], "vendors:write")).toBe(true); }); + it("ROLES non-empty", () => { expect(ROLES.length).toBeGreaterThan(0); }); +}); diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts new file mode 100644 index 0000000..9ae9e05 --- /dev/null +++ b/packages/auth/src/index.ts @@ -0,0 +1,64 @@ +export const ROLES = [ + "super_admin", + "security_admin", + "procurement_manager", + "finance_approver", + "site_admin", + "noc_operator", + "read_only_auditor", + "partner_inspector", + "vendor_user", +] as const; + +export type RoleName = (typeof ROLES)[number]; + +export const PERMISSIONS = [ + "vendors:read", "vendors:write", "offers:read", "offers:write", + "purchase_orders:read", "purchase_orders:write", "purchase_orders:approve", + "assets:read", "assets:write", "sites:read", "sites:write", + "users:read", "users:write", "roles:read", "roles:write", + "audit:read", "audit:export", "upload:write", "inspection:write", + "unifi:read", "unifi:write", "unifi_oversight:read", + "compliance:read", "compliance:write", "purchasing_catalog:read", + "vendor:read_own", "vendor:write_offers_own", "vendor:view_pos_own", +] as const; + +export type Permission = (typeof PERMISSIONS)[number]; + +const ROLE_PERMISSIONS: Record = { + super_admin: [...PERMISSIONS], + security_admin: ["users:read", "users:write", "roles:read", "roles:write", "audit:read", "audit:export", "vendors:read", "offers:read", "purchase_orders:read", "assets:read", "sites:read", "compliance:read", "compliance:write", "unifi_oversight:read"], + procurement_manager: ["vendors:read", "vendors:write", "offers:read", "offers:write", "purchase_orders:read", "purchase_orders:write", "assets:read", "sites:read", "upload:write", "unifi:read", "purchasing_catalog:read"], + finance_approver: ["vendors:read", "offers:read", "purchase_orders:read", "purchase_orders:approve", "assets:read", "sites:read", "purchasing_catalog:read"], + site_admin: ["vendors:read", "offers:read", "purchase_orders:read", "assets:read", "assets:write", "sites:read", "sites:write", "upload:write", "unifi:read", "unifi:write", "compliance:read"], + noc_operator: ["assets:read", "assets:write", "sites:read", "upload:write", "unifi:read"], + read_only_auditor: ["vendors:read", "offers:read", "purchase_orders:read", "assets:read", "sites:read", "audit:read", "audit:export", "unifi:read", "compliance:read", "purchasing_catalog:read"], + partner_inspector: ["offers:read", "assets:read", "upload:write", "inspection:write"], + vendor_user: ["vendor:read_own", "vendor:write_offers_own", "vendor:view_pos_own", "vendors:read", "offers:read", "offers:write", "purchase_orders:read"], +}; + +export function hasPermission(roleNames: RoleName[], permission: Permission): boolean { + for (const r of roleNames) { + const perms = ROLE_PERMISSIONS[r]; + if (perms?.includes(permission)) return true; + } + return false; +} + +export function hasAnyPermission(roleNames: RoleName[], permissions: Permission[]): boolean { + return permissions.some((p) => hasPermission(roleNames, p)); +} + +export interface ABACContext { + site_id?: string; + project_id?: string; + asset_category?: string; + sensitivity_tier?: string; + vendor_trust_tier?: string; +} + +export function checkABAC(resource: ABACContext, context: ABACContext): boolean { + if (context.site_id != null && resource.site_id != null && resource.site_id !== context.site_id) return false; + if (context.project_id != null && resource.project_id != null && resource.project_id !== context.project_id) return false; + return true; +} diff --git a/packages/auth/tsconfig.json b/packages/auth/tsconfig.json new file mode 100644 index 0000000..19f8ab9 --- /dev/null +++ b/packages/auth/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "declarationMap": true, + "skipLibCheck": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/schema/drizzle.config.ts b/packages/schema/drizzle.config.ts new file mode 100644 index 0000000..cef0b9b --- /dev/null +++ b/packages/schema/drizzle.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + schema: "./src/db/schema.ts", + out: "./drizzle", + dialect: "postgresql", + dbCredentials: { + url: process.env.DATABASE_URL ?? "postgres://sankofa:sankofa_dev@localhost:5432/sankofa", + }, +}); diff --git a/packages/schema/drizzle/0000_initial.sql b/packages/schema/drizzle/0000_initial.sql new file mode 100644 index 0000000..8e5b20a --- /dev/null +++ b/packages/schema/drizzle/0000_initial.sql @@ -0,0 +1,287 @@ +-- Sankofa HW Infra initial schema +CREATE TABLE IF NOT EXISTS "org_units" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" text NOT NULL, + "parent_id" uuid, + "org_id" text NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); + +CREATE TABLE IF NOT EXISTS "users" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "email" text NOT NULL UNIQUE, + "name" text, + "org_unit_id" uuid, + "org_id" text NOT NULL, + "external_id" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); + +ALTER TABLE "users" ADD CONSTRAINT "users_org_unit_id_org_units_id_fk" FOREIGN KEY ("org_unit_id") REFERENCES "public"."org_units"("id"); +ALTER TABLE "org_units" ADD CONSTRAINT "org_units_parent_id_org_units_id_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."org_units"("id"); + +CREATE TABLE IF NOT EXISTS "vendors" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "org_id" text NOT NULL, + "legal_name" text NOT NULL, + "contacts" jsonb, + "trust_tier" text DEFAULT 'unknown' NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); + +CREATE TABLE IF NOT EXISTS "vendor_bank_details" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "vendor_id" uuid NOT NULL, + "version" integer DEFAULT 1 NOT NULL, + "instructions" jsonb, + "approved_by_1" uuid, + "approved_by_2" uuid, + "approved_at" timestamp, + "created_at" timestamp DEFAULT now() NOT NULL +); + +ALTER TABLE "vendor_bank_details" ADD CONSTRAINT "vendor_bank_details_vendor_id_vendors_id_fk" FOREIGN KEY ("vendor_id") REFERENCES "public"."vendors"("id"); + +CREATE TABLE IF NOT EXISTS "offers" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "org_id" text NOT NULL, + "vendor_id" uuid NOT NULL, + "sku" text, + "mpn" text, + "quantity" integer NOT NULL, + "unit_price" decimal(20, 4) NOT NULL, + "incoterms" text, + "lead_time_days" integer, + "country_of_origin" text, + "condition" text, + "warranty" text, + "evidence_refs" jsonb, + "risk_score" decimal(5, 2), + "risk_factors" jsonb, + "status" text DEFAULT 'draft' NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); + +CREATE INDEX IF NOT EXISTS "offers_vendor_idx" ON "offers" ("vendor_id"); +CREATE INDEX IF NOT EXISTS "offers_org_idx" ON "offers" ("org_id"); +ALTER TABLE "offers" ADD CONSTRAINT "offers_vendor_id_vendors_id_fk" FOREIGN KEY ("vendor_id") REFERENCES "public"."vendors"("id"); + +CREATE TABLE IF NOT EXISTS "regions" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "org_id" text NOT NULL, + "name" text NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); + +CREATE TABLE IF NOT EXISTS "sites" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "org_id" text NOT NULL, + "region_id" uuid, + "name" text NOT NULL, + "address" text, + "network_metadata" jsonb, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); + +CREATE INDEX IF NOT EXISTS "sites_org_idx" ON "sites" ("org_id"); +ALTER TABLE "sites" ADD CONSTRAINT "sites_region_id_regions_id_fk" FOREIGN KEY ("region_id") REFERENCES "public"."regions"("id"); + +CREATE TABLE IF NOT EXISTS "rooms" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "site_id" uuid NOT NULL, + "name" text NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); + +ALTER TABLE "rooms" ADD CONSTRAINT "rooms_site_id_sites_id_fk" FOREIGN KEY ("site_id") REFERENCES "public"."sites"("id"); + +CREATE TABLE IF NOT EXISTS "rows" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "room_id" uuid NOT NULL, + "name" text NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); + +ALTER TABLE "rows" ADD CONSTRAINT "rows_room_id_rooms_id_fk" FOREIGN KEY ("room_id") REFERENCES "public"."rooms"("id"); + +CREATE TABLE IF NOT EXISTS "racks" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "row_id" uuid NOT NULL, + "name" text NOT NULL, + "ru_total" integer NOT NULL, + "power_feeds" jsonb, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); + +CREATE INDEX IF NOT EXISTS "racks_row_idx" ON "racks" ("row_id"); +ALTER TABLE "racks" ADD CONSTRAINT "racks_row_id_rows_id_fk" FOREIGN KEY ("row_id") REFERENCES "public"."rows"("id"); + +CREATE TABLE IF NOT EXISTS "assets" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "org_id" text NOT NULL, + "asset_id" text NOT NULL UNIQUE, + "category" text NOT NULL, + "manufacturer_serial" text, + "service_tag" text, + "macs" jsonb, + "wwns" jsonb, + "part_number" text, + "condition" text, + "warranty" text, + "proof_artifact_refs" jsonb, + "site_id" uuid, + "position_id" uuid, + "owner_id" uuid, + "project_id" text, + "sensitivity_tier" text, + "status" text DEFAULT 'pending' NOT NULL, + "chain_of_custody" jsonb, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); + +CREATE INDEX IF NOT EXISTS "assets_org_idx" ON "assets" ("org_id"); +CREATE INDEX IF NOT EXISTS "assets_site_idx" ON "assets" ("site_id"); +CREATE INDEX IF NOT EXISTS "assets_category_idx" ON "assets" ("category"); +CREATE INDEX IF NOT EXISTS "assets_status_idx" ON "assets" ("status"); +ALTER TABLE "assets" ADD CONSTRAINT "assets_site_id_sites_id_fk" FOREIGN KEY ("site_id") REFERENCES "public"."sites"("id"); +ALTER TABLE "assets" ADD CONSTRAINT "assets_owner_id_users_id_fk" FOREIGN KEY ("owner_id") REFERENCES "public"."users"("id"); + +CREATE TABLE IF NOT EXISTS "positions" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "rack_id" uuid NOT NULL, + "ru_start" integer NOT NULL, + "ru_end" integer NOT NULL, + "asset_id" uuid, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); + +CREATE INDEX IF NOT EXISTS "positions_rack_idx" ON "positions" ("rack_id"); +ALTER TABLE "positions" ADD CONSTRAINT "positions_rack_id_racks_id_fk" FOREIGN KEY ("rack_id") REFERENCES "public"."racks"("id"); +ALTER TABLE "positions" ADD CONSTRAINT "positions_asset_id_assets_id_fk" FOREIGN KEY ("asset_id") REFERENCES "public"."assets"("id"); +ALTER TABLE "assets" ADD CONSTRAINT "assets_position_id_positions_id_fk" FOREIGN KEY ("position_id") REFERENCES "public"."positions"("id"); + +CREATE TABLE IF NOT EXISTS "purchase_orders" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "org_id" text NOT NULL, + "vendor_id" uuid NOT NULL, + "line_items" jsonb NOT NULL, + "status" text DEFAULT 'draft' NOT NULL, + "approval_stage" text, + "escrow_terms" text, + "inspection_site_id" uuid, + "delivery_site_id" uuid, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); + +CREATE INDEX IF NOT EXISTS "po_vendor_idx" ON "purchase_orders" ("vendor_id"); +CREATE INDEX IF NOT EXISTS "po_org_idx" ON "purchase_orders" ("org_id"); +ALTER TABLE "purchase_orders" ADD CONSTRAINT "purchase_orders_vendor_id_vendors_id_fk" FOREIGN KEY ("vendor_id") REFERENCES "public"."vendors"("id"); +ALTER TABLE "purchase_orders" ADD CONSTRAINT "purchase_orders_inspection_site_id_sites_id_fk" FOREIGN KEY ("inspection_site_id") REFERENCES "public"."sites"("id"); +ALTER TABLE "purchase_orders" ADD CONSTRAINT "purchase_orders_delivery_site_id_sites_id_fk" FOREIGN KEY ("delivery_site_id") REFERENCES "public"."sites"("id"); + +CREATE TABLE IF NOT EXISTS "shipments" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "org_id" text NOT NULL, + "purchase_order_id" uuid NOT NULL, + "tracking" text, + "cartons_pallets" jsonb, + "customs_docs_refs" jsonb, + "status" text DEFAULT 'pending' NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); + +ALTER TABLE "shipments" ADD CONSTRAINT "shipments_purchase_order_id_purchase_orders_id_fk" FOREIGN KEY ("purchase_order_id") REFERENCES "public"."purchase_orders"("id"); + +CREATE TABLE IF NOT EXISTS "asset_components" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "parent_asset_id" uuid NOT NULL, + "child_asset_id" uuid NOT NULL, + "role" text NOT NULL, + "slot_index" integer, + "created_at" timestamp DEFAULT now() NOT NULL +); + +CREATE INDEX IF NOT EXISTS "asset_components_parent_idx" ON "asset_components" ("parent_asset_id"); +CREATE INDEX IF NOT EXISTS "asset_components_child_idx" ON "asset_components" ("child_asset_id"); +ALTER TABLE "asset_components" ADD CONSTRAINT "asset_components_parent_asset_id_assets_id_fk" FOREIGN KEY ("parent_asset_id") REFERENCES "public"."assets"("id"); +ALTER TABLE "asset_components" ADD CONSTRAINT "asset_components_child_asset_id_assets_id_fk" FOREIGN KEY ("child_asset_id") REFERENCES "public"."assets"("id"); + +CREATE TABLE IF NOT EXISTS "provisioning_records" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "asset_id" uuid NOT NULL, + "os_image" text, + "hypervisor_node" text, + "cluster_id" text, + "metadata" jsonb, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); + +ALTER TABLE "provisioning_records" ADD CONSTRAINT "provisioning_records_asset_id_assets_id_fk" FOREIGN KEY ("asset_id") REFERENCES "public"."assets"("id"); + +CREATE TABLE IF NOT EXISTS "maintenances" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "org_id" text NOT NULL, + "asset_id" uuid NOT NULL, + "type" text NOT NULL, + "vendor_ticket_ref" text, + "description" text, + "status" text DEFAULT 'open' NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); + +ALTER TABLE "maintenances" ADD CONSTRAINT "maintenances_asset_id_assets_id_fk" FOREIGN KEY ("asset_id") REFERENCES "public"."assets"("id"); + +CREATE TABLE IF NOT EXISTS "audit_events" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "org_id" text NOT NULL, + "actor_id" uuid, + "actor_email" text, + "action" text NOT NULL, + "resource_type" text NOT NULL, + "resource_id" text NOT NULL, + "before_state" jsonb, + "after_state" jsonb, + "occurred_at" timestamp DEFAULT now() NOT NULL +); + +CREATE INDEX IF NOT EXISTS "audit_events_org_idx" ON "audit_events" ("org_id"); +CREATE INDEX IF NOT EXISTS "audit_events_resource_idx" ON "audit_events" ("resource_type", "resource_id"); +CREATE INDEX IF NOT EXISTS "audit_events_occurred_idx" ON "audit_events" ("occurred_at"); +ALTER TABLE "audit_events" ADD CONSTRAINT "audit_events_actor_id_users_id_fk" FOREIGN KEY ("actor_id") REFERENCES "public"."users"("id"); + +CREATE TABLE IF NOT EXISTS "roles" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" text NOT NULL UNIQUE, + "description" text, + "permissions" jsonb DEFAULT '[]' NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); + +CREATE TABLE IF NOT EXISTS "user_roles" ( + "user_id" uuid NOT NULL, + "role_id" uuid NOT NULL, + "scope_site_id" uuid, + "scope_project_id" text, + "assigned_at" timestamp DEFAULT now() NOT NULL +); + +ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id"); +ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_role_id_roles_id_fk" FOREIGN KEY ("role_id") REFERENCES "public"."roles"("id"); +ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_scope_site_id_sites_id_fk" FOREIGN KEY ("scope_site_id") REFERENCES "public"."sites"("id"); +ALTER TABLE "user_roles" ADD PRIMARY KEY ("user_id", "role_id"); diff --git a/packages/schema/drizzle/0001_inspection_workflow.sql b/packages/schema/drizzle/0001_inspection_workflow.sql new file mode 100644 index 0000000..5810751 --- /dev/null +++ b/packages/schema/drizzle/0001_inspection_workflow.sql @@ -0,0 +1,41 @@ +CREATE TABLE IF NOT EXISTS "inspection_templates" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "org_id" text NOT NULL, + "category" text NOT NULL, + "name" text NOT NULL, + "steps" jsonb NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); + +CREATE TABLE IF NOT EXISTS "inspection_runs" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "org_id" text NOT NULL, + "template_id" uuid NOT NULL, + "offer_id" uuid, + "asset_id" uuid, + "status" text DEFAULT 'in_progress' NOT NULL, + "evidence_refs" jsonb, + "result_notes" text, + "completed_at" timestamp, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); + +ALTER TABLE "inspection_runs" ADD CONSTRAINT "inspection_runs_template_id_fk" FOREIGN KEY ("template_id") REFERENCES "public"."inspection_templates"("id"); +ALTER TABLE "inspection_runs" ADD CONSTRAINT "inspection_runs_offer_id_fk" FOREIGN KEY ("offer_id") REFERENCES "public"."offers"("id"); +ALTER TABLE "inspection_runs" ADD CONSTRAINT "inspection_runs_asset_id_fk" FOREIGN KEY ("asset_id") REFERENCES "public"."assets"("id"); + +CREATE TABLE IF NOT EXISTS "integration_mappings" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "org_id" text NOT NULL, + "asset_id" uuid, + "site_id" uuid, + "provider" text NOT NULL, + "external_id" text NOT NULL, + "metadata" jsonb, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +ALTER TABLE "integration_mappings" ADD CONSTRAINT "integration_mappings_asset_id_fk" FOREIGN KEY ("asset_id") REFERENCES "public"."assets"("id"); +ALTER TABLE "integration_mappings" ADD CONSTRAINT "integration_mappings_site_id_fk" FOREIGN KEY ("site_id") REFERENCES "public"."sites"("id"); diff --git a/packages/schema/drizzle/0002_unifi_product_intelligence.sql b/packages/schema/drizzle/0002_unifi_product_intelligence.sql new file mode 100644 index 0000000..a05ba9d --- /dev/null +++ b/packages/schema/drizzle/0002_unifi_product_intelligence.sql @@ -0,0 +1,24 @@ +CREATE TABLE IF NOT EXISTS "unifi_product_catalog" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "sku" text NOT NULL UNIQUE, + "model_name" text NOT NULL, + "generation" text NOT NULL, + "performance_class" text, + "eol_date" text, + "support_horizon" text, + "approved_sovereign_default" boolean DEFAULT false NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); + +CREATE INDEX IF NOT EXISTS "unifi_catalog_sku_idx" ON "unifi_product_catalog" ("sku"); +CREATE INDEX IF NOT EXISTS "unifi_catalog_generation_idx" ON "unifi_product_catalog" ("generation"); + +INSERT INTO "unifi_product_catalog" ("sku", "model_name", "generation", "performance_class", "eol_date", "support_horizon", "approved_sovereign_default") VALUES + ('USW-24-PoE', 'UniFi Switch 24 PoE', 'Gen1', 'prosumer', '2026-12-31', '2027-06', true), + ('USW-48-PoE', 'UniFi Switch 48 PoE', 'Gen1', 'prosumer', '2026-12-31', '2027-06', true), + ('U6-Pro', 'UniFi 6 Pro', 'Gen2', 'enterprise', '2028-06-30', '2029-12', true), + ('U6-Enterprise', 'UniFi 6 Enterprise', 'Enterprise', 'enterprise', '2029-12-31', '2030-06', true), + ('U7-Pro', 'UniFi 7 Pro', 'Gen2', 'enterprise', null, null, true), + ('USW-Enterprise-24-PoE', 'UniFi Enterprise 24 PoE', 'Enterprise', 'enterprise', '2030-12-31', '2031-06', true) +ON CONFLICT ("sku") DO NOTHING; diff --git a/packages/schema/drizzle/0003_compliance_profiles.sql b/packages/schema/drizzle/0003_compliance_profiles.sql new file mode 100644 index 0000000..5607470 --- /dev/null +++ b/packages/schema/drizzle/0003_compliance_profiles.sql @@ -0,0 +1,14 @@ +CREATE TABLE IF NOT EXISTS "compliance_profiles" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "org_id" text NOT NULL, + "name" text NOT NULL, + "firmware_freeze_policy" jsonb, + "allowed_generations" jsonb, + "approved_skus" jsonb, + "site_id" uuid, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); + +CREATE INDEX IF NOT EXISTS "compliance_profiles_org_idx" ON "compliance_profiles" ("org_id"); +ALTER TABLE "compliance_profiles" ADD CONSTRAINT "compliance_profiles_site_id_fk" FOREIGN KEY ("site_id") REFERENCES "public"."sites"("id"); diff --git a/packages/schema/drizzle/0004_unifi_controllers.sql b/packages/schema/drizzle/0004_unifi_controllers.sql new file mode 100644 index 0000000..c2a685f --- /dev/null +++ b/packages/schema/drizzle/0004_unifi_controllers.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS "unifi_controllers" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "org_id" text NOT NULL, + "site_id" uuid, + "base_url" text NOT NULL, + "role" text NOT NULL, + "region" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); + +ALTER TABLE "unifi_controllers" ADD CONSTRAINT "unifi_controllers_site_id_fk" FOREIGN KEY ("site_id") REFERENCES "public"."sites"("id"); diff --git a/packages/schema/drizzle/0005_vendor_user_and_ingestion.sql b/packages/schema/drizzle/0005_vendor_user_and_ingestion.sql new file mode 100644 index 0000000..f845d46 --- /dev/null +++ b/packages/schema/drizzle/0005_vendor_user_and_ingestion.sql @@ -0,0 +1,10 @@ +-- Vendor user: users can be linked to a vendor for vendor-portal login +ALTER TABLE "users" ADD COLUMN IF NOT EXISTS "vendor_id" uuid REFERENCES "vendors"("id"); + +-- Offer ingestion: source tracking and optional vendor (unassigned when ingested) +ALTER TABLE "offers" ALTER COLUMN "vendor_id" DROP NOT NULL; +ALTER TABLE "offers" ADD COLUMN IF NOT EXISTS "source" text NOT NULL DEFAULT 'manual'; +ALTER TABLE "offers" ADD COLUMN IF NOT EXISTS "source_ref" text; +ALTER TABLE "offers" ADD COLUMN IF NOT EXISTS "source_metadata" jsonb; +ALTER TABLE "offers" ADD COLUMN IF NOT EXISTS "ingested_at" timestamp; +CREATE INDEX IF NOT EXISTS "offers_source_idx" ON "offers" ("source"); diff --git a/packages/schema/drizzle/meta/_journal.json b/packages/schema/drizzle/meta/_journal.json new file mode 100644 index 0000000..f2ef440 --- /dev/null +++ b/packages/schema/drizzle/meta/_journal.json @@ -0,0 +1 @@ +{"version":"7","dialect":"pg","entries":[{"idx":0,"when":1739059200000,"tag":"0000_initial","breakpoints":true},{"idx":1,"when":1739062800000,"tag":"0001_inspection_workflow","breakpoints":true},{"idx":2,"when":1739066400000,"tag":"0002_unifi_product_intelligence","breakpoints":true},{"idx":3,"when":1739070000000,"tag":"0003_compliance_profiles","breakpoints":true},{"idx":4,"when":1739073600000,"tag":"0004_unifi_controllers","breakpoints":true},{"idx":5,"when":1739077200000,"tag":"0005_vendor_user_and_ingestion","breakpoints":true}]} diff --git a/packages/schema/package.json b/packages/schema/package.json new file mode 100644 index 0000000..71ffa78 --- /dev/null +++ b/packages/schema/package.json @@ -0,0 +1,28 @@ +{ + "name": "@sankofa/schema", + "version": "0.1.0", + "private": true, + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", "default": "./dist/index.js" } }, + "scripts": { + "build": "tsc", + "test": "vitest run", + "lint": "eslint src --ext .ts", + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate", + "db:studio": "drizzle-kit studio" + }, + "dependencies": { + "drizzle-orm": "^0.36.0", + "postgres": "^3.4.5" + }, + "devDependencies": { + "@types/node": "^22.10.0", + "drizzle-kit": "^0.28.0", + "eslint": "^9.15.0", + "typescript": "^5.7.0", + "vitest": "^2.1.0" + } +} diff --git a/packages/schema/src/db/client.ts b/packages/schema/src/db/client.ts new file mode 100644 index 0000000..c82d2fc --- /dev/null +++ b/packages/schema/src/db/client.ts @@ -0,0 +1,12 @@ +import { drizzle } from "drizzle-orm/postgres-js"; +import postgres from "postgres"; +import * as schema from "./schema.js"; + +const connectionString = process.env.DATABASE_URL ?? "postgres://sankofa:sankofa_dev@localhost:5432/sankofa"; + +export function getDb() { + const client = postgres(connectionString, { max: 10 }); + return drizzle(client, { schema }); +} + +export type Db = ReturnType; diff --git a/packages/schema/src/db/schema.test.ts b/packages/schema/src/db/schema.test.ts new file mode 100644 index 0000000..58e7465 --- /dev/null +++ b/packages/schema/src/db/schema.test.ts @@ -0,0 +1,10 @@ +import { describe, it, expect } from "vitest"; +import { vendors, assets, auditEvents } from "./schema"; + +describe("schema", () => { + it("exports core tables", () => { + expect(vendors).toBeDefined(); + expect(assets).toBeDefined(); + expect(auditEvents).toBeDefined(); + }); +}); diff --git a/packages/schema/src/db/schema.ts b/packages/schema/src/db/schema.ts new file mode 100644 index 0000000..400b2c9 --- /dev/null +++ b/packages/schema/src/db/schema.ts @@ -0,0 +1,417 @@ +import { + pgTable, + uuid, + text, + timestamp, + decimal, + integer, + jsonb, + boolean, + primaryKey, + index, +} from "drizzle-orm/pg-core"; + +// --- Org & users --- +export const orgUnits = pgTable("org_units", { + id: uuid("id").primaryKey().defaultRandom(), + name: text("name").notNull(), + parentId: uuid("parent_id"), + orgId: text("org_id").notNull(), // tenancy + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}); + +export const users = pgTable("users", { + id: uuid("id").primaryKey().defaultRandom(), + email: text("email").notNull().unique(), + name: text("name"), + orgUnitId: uuid("org_unit_id").references((): any => orgUnits.id), + orgId: text("org_id").notNull(), + vendorId: uuid("vendor_id").references((): any => vendors.id), // when set, user is a vendor user (login for that vendor) + externalId: text("external_id"), // SSO + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}); + +// --- Vendors --- +export const vendors = pgTable("vendors", { + id: uuid("id").primaryKey().defaultRandom(), + orgId: text("org_id").notNull(), + legalName: text("legal_name").notNull(), + contacts: jsonb("contacts").$type<{ email?: string; phone?: string; name?: string }[]>(), + trustTier: text("trust_tier").notNull().default("unknown"), // unknown | low | medium | high + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}); + +export const vendorBankDetails = pgTable("vendor_bank_details", { + id: uuid("id").primaryKey().defaultRandom(), + vendorId: uuid("vendor_id").notNull().references((): any => vendors.id), + version: integer("version").notNull().default(1), + instructions: jsonb("instructions").$type>(), + approvedBy1: uuid("approved_by_1"), + approvedBy2: uuid("approved_by_2"), + approvedAt: timestamp("approved_at"), + createdAt: timestamp("created_at").defaultNow().notNull(), +}); + +// --- Offers --- +export const offers = pgTable( + "offers", + { + id: uuid("id").primaryKey().defaultRandom(), + orgId: text("org_id").notNull(), + vendorId: uuid("vendor_id").references((): any => vendors.id), // null when ingested and not yet assigned + sku: text("sku"), + mpn: text("mpn"), + quantity: integer("quantity").notNull(), + unitPrice: decimal("unit_price", { precision: 20, scale: 4 }).notNull(), + incoterms: text("incoterms"), + leadTimeDays: integer("lead_time_days"), + countryOfOrigin: text("country_of_origin"), + condition: text("condition"), + warranty: text("warranty"), + evidenceRefs: jsonb("evidence_refs").$type<{ key: string; hash?: string }[]>(), + riskScore: decimal("risk_score", { precision: 5, scale: 2 }), + riskFactors: jsonb("risk_factors").$type>(), + status: text("status").notNull().default("draft"), // draft | rejected | clarification | advanced + source: text("source").notNull().default("manual"), // manual | scraped | email + sourceRef: text("source_ref"), // URL or email message id + sourceMetadata: jsonb("source_metadata").$type>(), // parsed sender, subject, page url, etc. + ingestedAt: timestamp("ingested_at"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), + }, + (t) => [index("offers_vendor_idx").on(t.vendorId), index("offers_org_idx").on(t.orgId), index("offers_source_idx").on(t.source)] +); + +// --- Purchase orders --- +export const purchaseOrders = pgTable( + "purchase_orders", + { + id: uuid("id").primaryKey().defaultRandom(), + orgId: text("org_id").notNull(), + vendorId: uuid("vendor_id").notNull().references((): any => vendors.id), + lineItems: jsonb("line_items").$type<{ offerId?: string; sku?: string; quantity: number; unitPrice: string }[]>().notNull(), + status: text("status").notNull().default("draft"), // draft | pending_approval | approved | rejected | ordered | received + approvalStage: text("approval_stage"), + escrowTerms: text("escrow_terms"), + inspectionSiteId: uuid("inspection_site_id"), + deliverySiteId: uuid("delivery_site_id"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), + }, + (t) => [index("po_vendor_idx").on(t.vendorId), index("po_org_idx").on(t.orgId)] +); + +// --- Shipments --- +export const shipments = pgTable("shipments", { + id: uuid("id").primaryKey().defaultRandom(), + orgId: text("org_id").notNull(), + purchaseOrderId: uuid("purchase_order_id").notNull().references((): any => purchaseOrders.id), + tracking: text("tracking"), + cartonsPallets: jsonb("cartons_pallets").$type>(), + customsDocsRefs: jsonb("customs_docs_refs").$type<{ key: string }[]>(), + status: text("status").notNull().default("pending"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}); + +// --- Sites & racks --- +export const regions = pgTable("regions", { + id: uuid("id").primaryKey().defaultRandom(), + orgId: text("org_id").notNull(), + name: text("name").notNull(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}); + +export const sites = pgTable( + "sites", + { + id: uuid("id").primaryKey().defaultRandom(), + orgId: text("org_id").notNull(), + regionId: uuid("region_id").references((): any => regions.id), + name: text("name").notNull(), + address: text("address"), + networkMetadata: jsonb("network_metadata").$type<{ uplinks?: string[]; vlans?: string[]; portProfiles?: string[]; ipRanges?: string[] }>(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), + }, + (t) => [index("sites_org_idx").on(t.orgId)] +); + +export const rooms = pgTable("rooms", { + id: uuid("id").primaryKey().defaultRandom(), + siteId: uuid("site_id").notNull().references((): any => sites.id), + name: text("name").notNull(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}); + +export const rows = pgTable("rows", { + id: uuid("id").primaryKey().defaultRandom(), + roomId: uuid("room_id").notNull().references((): any => rooms.id), + name: text("name").notNull(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}); + +export const racks = pgTable( + "racks", + { + id: uuid("id").primaryKey().defaultRandom(), + rowId: uuid("row_id").notNull().references((): any => rows.id), + name: text("name").notNull(), + ruTotal: integer("ru_total").notNull(), + powerFeeds: jsonb("power_feeds").$type<{ feed: string; pduModel?: string; circuitLimitWatts?: number }[]>(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), + }, + (t) => [index("racks_row_idx").on(t.rowId)] +); + +export const positions = pgTable( + "positions", + { + id: uuid("id").primaryKey().defaultRandom(), + rackId: uuid("rack_id").notNull().references((): any => racks.id), + ruStart: integer("ru_start").notNull(), + ruEnd: integer("ru_end").notNull(), + assetId: uuid("asset_id"), // FK to assets.id set in migration if needed + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), + }, + (t) => [index("positions_rack_idx").on(t.rackId)] +); + +// --- Assets --- +export const assets = pgTable( + "assets", + { + id: uuid("id").primaryKey().defaultRandom(), + orgId: text("org_id").notNull(), + assetId: text("asset_id").notNull().unique(), // business identifier + category: text("category").notNull(), // server | gpu | cpu | dimm | nic | ssd + manufacturerSerial: text("manufacturer_serial"), + serviceTag: text("service_tag"), + macs: jsonb("macs").$type(), + wwns: jsonb("wwns").$type(), + partNumber: text("part_number"), + condition: text("condition"), + warranty: text("warranty"), + proofArtifactRefs: jsonb("proof_artifact_refs").$type<{ key: string; hash?: string }[]>(), + siteId: uuid("site_id").references((): any => sites.id), + positionId: uuid("position_id").references((): any => positions.id), + ownerId: uuid("owner_id").references((): any => users.id), + projectId: text("project_id"), + sensitivityTier: text("sensitivity_tier"), + status: text("status").notNull().default("pending"), // pending | received | staged | provisioned | in_use | maintenance | decommissioned + chainOfCustody: jsonb("chain_of_custody").$type<{ who: string; where?: string; when: string }[]>(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), + }, + (t) => [ + index("assets_org_idx").on(t.orgId), + index("assets_site_idx").on(t.siteId), + index("assets_category_idx").on(t.category), + index("assets_status_idx").on(t.status), + ] +); + +// Asset -> components (server has many GPUs/CPUs/DIMMs/NICs) +export const assetComponents = pgTable( + "asset_components", + { + id: uuid("id").primaryKey().defaultRandom(), + parentAssetId: uuid("parent_asset_id").notNull().references((): any => assets.id), + childAssetId: uuid("child_asset_id").notNull().references((): any => assets.id), + role: text("role").notNull(), // gpu | cpu | dimm | nic | ssd + slotIndex: integer("slot_index"), + createdAt: timestamp("created_at").defaultNow().notNull(), + }, + (t) => [ + index("asset_components_parent_idx").on(t.parentAssetId), + index("asset_components_child_idx").on(t.childAssetId), + ] +); + +// Provisioning (OS image, hypervisor, cluster) +export const provisioningRecords = pgTable("provisioning_records", { + id: uuid("id").primaryKey().defaultRandom(), + assetId: uuid("asset_id").notNull().references((): any => assets.id), + osImage: text("os_image"), + hypervisorNode: text("hypervisor_node"), + clusterId: text("cluster_id"), + metadata: jsonb("metadata").$type>(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}); + +// Inspection templates and runs +export const inspectionTemplates = pgTable("inspection_templates", { + id: uuid("id").primaryKey().defaultRandom(), + orgId: text("org_id").notNull(), + category: text("category").notNull(), // server | gpu | memory | nic + name: text("name").notNull(), + steps: jsonb("steps").$type<{ id: string; label: string; required?: boolean }[]>().notNull(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}); + +export const inspectionRuns = pgTable("inspection_runs", { + id: uuid("id").primaryKey().defaultRandom(), + orgId: text("org_id").notNull(), + templateId: uuid("template_id").notNull().references((): any => inspectionTemplates.id), + offerId: uuid("offer_id").references((): any => offers.id), + assetId: uuid("asset_id").references((): any => assets.id), + status: text("status").notNull().default("in_progress"), // in_progress | pass | fail + evidenceRefs: jsonb("evidence_refs").$type<{ key: string; hash?: string }[]>(), + resultNotes: text("result_notes"), + completedAt: timestamp("completed_at"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}); + +// Compliance profiles (firmware freeze, hardware class, approved SKUs per sovereign) +export const complianceProfiles = pgTable( + "compliance_profiles", + { + id: uuid("id").primaryKey().defaultRandom(), + orgId: text("org_id").notNull(), + name: text("name").notNull(), + firmwareFreezePolicy: jsonb("firmware_freeze_policy").$type<{ lockedVersion?: string; minVersion?: string; maxVersion?: string }>(), + allowedGenerations: jsonb("allowed_generations").$type(), + approvedSkus: jsonb("approved_skus").$type(), + siteId: uuid("site_id").references((): any => sites.id), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), + }, + (t) => [index("compliance_profiles_org_idx").on(t.orgId)] +); + +// UniFi Product Intelligence (SKU -> generation, EoL, support horizon) +export const unifiProductCatalog = pgTable( + "unifi_product_catalog", + { + id: uuid("id").primaryKey().defaultRandom(), + sku: text("sku").notNull().unique(), + modelName: text("model_name").notNull(), + generation: text("generation").notNull(), // Gen1 | Gen2 | Enterprise + performanceClass: text("performance_class"), + eolDate: text("eol_date"), // ISO date or null + supportHorizon: text("support_horizon"), + approvedSovereignDefault: boolean("approved_sovereign_default").notNull().default(false), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), + }, + (t) => [index("unifi_catalog_sku_idx").on(t.sku), index("unifi_catalog_generation_idx").on(t.generation)] +); + +// UniFi controller registry (topology and role; credentials in Vault) +export const unifiControllers = pgTable("unifi_controllers", { + id: uuid("id").primaryKey().defaultRandom(), + orgId: text("org_id").notNull(), + siteId: uuid("site_id").references((): any => sites.id), + baseUrl: text("base_url").notNull(), + role: text("role").notNull(), // sovereign_write | oversight_read_only + region: text("region"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}); + +// Integration mappings (Asset <-> external system ID) +export const integrationMappings = pgTable("integration_mappings", { + id: uuid("id").primaryKey().defaultRandom(), + orgId: text("org_id").notNull(), + assetId: uuid("asset_id").references((): any => assets.id), + siteId: uuid("site_id").references((): any => sites.id), + provider: text("provider").notNull(), // unifi | proxmox | redfish + externalId: text("external_id").notNull(), + metadata: jsonb("metadata").$type>(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}); + +// Maintenance / RMA +export const maintenances = pgTable("maintenances", { + id: uuid("id").primaryKey().defaultRandom(), + orgId: text("org_id").notNull(), + assetId: uuid("asset_id").notNull().references((): any => assets.id), + type: text("type").notNull(), // incident | rma | part_swap + vendorTicketRef: text("vendor_ticket_ref"), + description: text("description"), + status: text("status").notNull().default("open"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}); + +// Audit log (append-only, who/when/what/before/after) +export const auditEvents = pgTable( + "audit_events", + { + id: uuid("id").primaryKey().defaultRandom(), + orgId: text("org_id").notNull(), + actorId: uuid("actor_id").references((): any => users.id), + actorEmail: text("actor_email"), + action: text("action").notNull(), + resourceType: text("resource_type").notNull(), + resourceId: text("resource_id").notNull(), + beforeState: jsonb("before_state").$type>(), + afterState: jsonb("after_state").$type>(), + occurredAt: timestamp("occurred_at").defaultNow().notNull(), + }, + (t) => [ + index("audit_events_org_idx").on(t.orgId), + index("audit_events_resource_idx").on(t.resourceType, t.resourceId), + index("audit_events_occurred_idx").on(t.occurredAt), + ] +); + +// Roles and permissions (RBAC) +export const roles = pgTable("roles", { + id: uuid("id").primaryKey().defaultRandom(), + name: text("name").notNull().unique(), + description: text("description"), + permissions: jsonb("permissions").$type().notNull().default([]), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}); + +export const userRoles = pgTable( + "user_roles", + { + userId: uuid("user_id").notNull().references((): any => users.id), + roleId: uuid("role_id").notNull().references((): any => roles.id), + scopeSiteId: uuid("scope_site_id"), + scopeProjectId: text("scope_project_id"), + assignedAt: timestamp("assigned_at").defaultNow().notNull(), + }, + (t) => [primaryKey({ columns: [t.userId, t.roleId] })] +); + +export type OrgUnit = typeof orgUnits.$inferSelect; +export type User = typeof users.$inferSelect; +export type Vendor = typeof vendors.$inferSelect; +export type Offer = typeof offers.$inferSelect; +export type PurchaseOrder = typeof purchaseOrders.$inferSelect; +export type Shipment = typeof shipments.$inferSelect; +export type Region = typeof regions.$inferSelect; +export type Site = typeof sites.$inferSelect; +export type Room = typeof rooms.$inferSelect; +export type Row = typeof rows.$inferSelect; +export type Rack = typeof racks.$inferSelect; +export type Position = typeof positions.$inferSelect; +export type Asset = typeof assets.$inferSelect; +export type AssetComponent = typeof assetComponents.$inferSelect; +export type ProvisioningRecord = typeof provisioningRecords.$inferSelect; +export type InspectionTemplate = typeof inspectionTemplates.$inferSelect; +export type InspectionRun = typeof inspectionRuns.$inferSelect; +export type ComplianceProfile = typeof complianceProfiles.$inferSelect; +export type UnifiController = typeof unifiControllers.$inferSelect; +export type UnifiProductCatalog = typeof unifiProductCatalog.$inferSelect; +export type IntegrationMapping = typeof integrationMappings.$inferSelect; +export type Maintenance = typeof maintenances.$inferSelect; +export type AuditEvent = typeof auditEvents.$inferSelect; +export type Role = typeof roles.$inferSelect; +export type UserRole = typeof userRoles.$inferSelect; diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts new file mode 100644 index 0000000..baa504d --- /dev/null +++ b/packages/schema/src/index.ts @@ -0,0 +1,2 @@ +export * from "./db/schema.js"; +export { getDb } from "./db/client.js"; diff --git a/packages/schema/tsconfig.json b/packages/schema/tsconfig.json new file mode 100644 index 0000000..19f8ab9 --- /dev/null +++ b/packages/schema/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "declarationMap": true, + "skipLibCheck": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/schema/vitest.config.ts b/packages/schema/vitest.config.ts new file mode 100644 index 0000000..c1433e6 --- /dev/null +++ b/packages/schema/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["src/**/*.test.ts"], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..34eed70 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,5636 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@eslint/js': + specifier: ^9.15.0 + version: 9.39.2 + eslint: + specifier: ^9.15.0 + version: 9.39.2 + typescript-eslint: + specifier: ^8.15.0 + version: 8.54.0(eslint@9.39.2)(typescript@5.9.3) + + apps/api: + dependencies: + '@aws-sdk/client-s3': + specifier: ^3.700.0 + version: 3.985.0 + '@aws-sdk/s3-request-presigner': + specifier: ^3.700.0 + version: 3.985.0 + '@fastify/cors': + specifier: ^10.0.0 + version: 10.1.0 + '@fastify/jwt': + specifier: ^9.0.0 + version: 9.1.0 + '@fastify/multipart': + specifier: ^9.0.0 + version: 9.4.0 + '@fastify/sensible': + specifier: ^6.0.0 + version: 6.0.4 + '@fastify/swagger': + specifier: ^9.7.0 + version: 9.7.0 + '@fastify/swagger-ui': + specifier: ^5.2.5 + version: 5.2.5 + '@sankofa/auth': + specifier: workspace:* + version: link:../../packages/auth + '@sankofa/schema': + specifier: workspace:* + version: link:../../packages/schema + '@sankofa/workflow': + specifier: workspace:* + version: link:../workflow + drizzle-orm: + specifier: ^0.36.0 + version: 0.36.4(@types/react@18.3.28)(postgres@3.4.8)(react@18.3.1) + fastify: + specifier: ^5.1.0 + version: 5.7.4 + yaml: + specifier: ^2.8.2 + version: 2.8.2 + devDependencies: + '@types/node': + specifier: ^22.10.0 + version: 22.19.10 + eslint: + specifier: ^9.15.0 + version: 9.39.2 + tsx: + specifier: ^4.19.0 + version: 4.21.0 + typescript: + specifier: ^5.7.0 + version: 5.9.3 + vitest: + specifier: ^2.1.0 + version: 2.1.9(@types/node@22.19.10) + + apps/web: + dependencies: + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + react-router-dom: + specifier: ^7.0.0 + version: 7.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + devDependencies: + '@types/react': + specifier: ^18.3.0 + version: 18.3.28 + '@types/react-dom': + specifier: ^18.3.0 + version: 18.3.7(@types/react@18.3.28) + '@vitejs/plugin-react': + specifier: ^4.3.4 + version: 4.7.0(vite@6.4.1(@types/node@22.19.10)(tsx@4.21.0)(yaml@2.8.2)) + eslint: + specifier: ^9.15.0 + version: 9.39.2 + typescript: + specifier: ^5.7.0 + version: 5.9.3 + vite: + specifier: ^6.0.0 + version: 6.4.1(@types/node@22.19.10)(tsx@4.21.0)(yaml@2.8.2) + + apps/workflow: + dependencies: + '@sankofa/schema': + specifier: workspace:* + version: link:../../packages/schema + devDependencies: + '@types/node': + specifier: ^22.10.0 + version: 22.19.10 + eslint: + specifier: ^9.15.0 + version: 9.39.2 + tsx: + specifier: ^4.19.0 + version: 4.21.0 + typescript: + specifier: ^5.7.0 + version: 5.9.3 + vitest: + specifier: ^2.1.0 + version: 2.1.9(@types/node@22.19.10) + + packages/auth: + dependencies: + '@sankofa/schema': + specifier: workspace:* + version: link:../schema + devDependencies: + '@types/node': + specifier: ^22.10.0 + version: 22.19.10 + eslint: + specifier: ^9.15.0 + version: 9.39.2 + typescript: + specifier: ^5.7.0 + version: 5.9.3 + vitest: + specifier: ^2.1.0 + version: 2.1.9(@types/node@22.19.10) + + packages/schema: + dependencies: + drizzle-orm: + specifier: ^0.36.0 + version: 0.36.4(@types/react@18.3.28)(postgres@3.4.8)(react@18.3.1) + postgres: + specifier: ^3.4.5 + version: 3.4.8 + devDependencies: + '@types/node': + specifier: ^22.10.0 + version: 22.19.10 + drizzle-kit: + specifier: ^0.28.0 + version: 0.28.1 + eslint: + specifier: ^9.15.0 + version: 9.39.2 + typescript: + specifier: ^5.7.0 + version: 5.9.3 + vitest: + specifier: ^2.1.0 + version: 2.1.9(@types/node@22.19.10) + +packages: + + '@aws-crypto/crc32@5.2.0': + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/crc32c@5.2.0': + resolution: {integrity: sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==} + + '@aws-crypto/sha1-browser@5.2.0': + resolution: {integrity: sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==} + + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/client-s3@3.985.0': + resolution: {integrity: sha512-S9TqjzzZEEIKBnC7yFpvqM7CG9ALpY5qhQ5BnDBJtdG20NoGpjKLGUUfD2wmZItuhbrcM4Z8c6m6Fg0XYIOVvw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-sso@3.985.0': + resolution: {integrity: sha512-81J8iE8MuXhdbMfIz4sWFj64Pe41bFi/uqqmqOC5SlGv+kwoyLsyKS/rH2tW2t5buih4vTUxskRjxlqikTD4oQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.973.7': + resolution: {integrity: sha512-wNZZQQNlJ+hzD49cKdo+PY6rsTDElO8yDImnrI69p2PLBa7QomeUKAJWYp9xnaR38nlHqWhMHZuYLCQ3oSX+xg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/crc64-nvme@3.972.0': + resolution: {integrity: sha512-ThlLhTqX68jvoIVv+pryOdb5coP1cX1/MaTbB9xkGDCbWbsqQcLqzPxuSoW1DCnAAIacmXCWpzUNOB9pv+xXQw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.5': + resolution: {integrity: sha512-LxJ9PEO4gKPXzkufvIESUysykPIdrV7+Ocb9yAhbhJLE4TiAYqbCVUE+VuKP1leGR1bBfjWjYgSV5MxprlX3mQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.7': + resolution: {integrity: sha512-L2uOGtvp2x3bTcxFTpSM+GkwFIPd8pHfGWO1764icMbo7e5xJh0nfhx1UwkXLnwvocTNEf8A7jISZLYjUSNaTg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.972.5': + resolution: {integrity: sha512-SdDTYE6jkARzOeL7+kudMIM4DaFnP5dZVeatzw849k4bSXDdErDS188bgeNzc/RA2WGrlEpsqHUKP6G7sVXhZg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.5': + resolution: {integrity: sha512-uYq1ILyTSI6ZDCMY5+vUsRM0SOCVI7kaW4wBrehVVkhAxC6y+e9rvGtnoZqCOWL1gKjTMouvsf4Ilhc5NCg1Aw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.6': + resolution: {integrity: sha512-DZ3CnAAtSVtVz+G+ogqecaErMLgzph4JH5nYbHoBMgBkwTUV+SUcjsjOJwdBJTHu3Dm6l5LBYekZoU2nDqQk2A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.5': + resolution: {integrity: sha512-HDKF3mVbLnuqGg6dMnzBf1VUOywE12/N286msI9YaK9mEIzdsGCtLTvrDhe3Up0R9/hGFbB+9l21/TwF5L1C6g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.972.5': + resolution: {integrity: sha512-8urj3AoeNeQisjMmMBhFeiY2gxt6/7wQQbEGun0YV/OaOOiXrIudTIEYF8ZfD+NQI6X1FY5AkRsx6O/CaGiybA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.5': + resolution: {integrity: sha512-OK3cULuJl6c+RcDZfPpaK5o3deTOnKZbxm7pzhFNGA3fI2hF9yDih17fGRazJzGGWaDVlR9ejZrpDef4DJCEsw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-bucket-endpoint@3.972.3': + resolution: {integrity: sha512-fmbgWYirF67YF1GfD7cg5N6HHQ96EyRNx/rDIrTF277/zTWVuPI2qS/ZHgofwR1NZPe/NWvoppflQY01LrbVLg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-expect-continue@3.972.3': + resolution: {integrity: sha512-4msC33RZsXQpUKR5QR4HnvBSNCPLGHmB55oDiROqqgyOc+TOfVu2xgi5goA7ms6MdZLeEh2905UfWMnMMF4mRg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-flexible-checksums@3.972.5': + resolution: {integrity: sha512-SF/1MYWx67OyCrLA4icIpWUfCkdlOi8Y1KecQ9xYxkL10GMjVdPTGPnYhAg0dw5U43Y9PVUWhAV2ezOaG+0BLg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-host-header@3.972.3': + resolution: {integrity: sha512-aknPTb2M+G3s+0qLCx4Li/qGZH8IIYjugHMv15JTYMe6mgZO8VBpYgeGYsNMGCqCZOcWzuf900jFBG5bopfzmA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-location-constraint@3.972.3': + resolution: {integrity: sha512-nIg64CVrsXp67vbK0U1/Is8rik3huS3QkRHn2DRDx4NldrEFMgdkZGI/+cZMKD9k4YOS110Dfu21KZLHrFA/1g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-logger@3.972.3': + resolution: {integrity: sha512-Ftg09xNNRqaz9QNzlfdQWfpqMCJbsQdnZVJP55jfhbKi1+FTWxGuvfPoBhDHIovqWKjqbuiew3HuhxbJ0+OjgA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-recursion-detection@3.972.3': + resolution: {integrity: sha512-PY57QhzNuXHnwbJgbWYTrqIDHYSeOlhfYERTAuc16LKZpTZRJUjzBFokp9hF7u1fuGeE3D70ERXzdbMBOqQz7Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-sdk-s3@3.972.7': + resolution: {integrity: sha512-VtZ7tMIw18VzjG+I6D6rh2eLkJfTtByiFoCIauGDtTTPBEUMQUiGaJ/zZrPlCY6BsvLLeFKz3+E5mntgiOWmIg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-ssec@3.972.3': + resolution: {integrity: sha512-dU6kDuULN3o3jEHcjm0c4zWJlY1zWVkjG9NPe9qxYLLpcbdj5kRYBS2DdWYD+1B9f910DezRuws7xDEqKkHQIg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-user-agent@3.972.7': + resolution: {integrity: sha512-HUD+geASjXSCyL/DHPQc/Ua7JhldTcIglVAoCV8kiVm99IaFSlAbTvEnyhZwdE6bdFyTL+uIaWLaCFSRsglZBQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/nested-clients@3.985.0': + resolution: {integrity: sha512-TsWwKzb/2WHafAY0CE7uXgLj0FmnkBTgfioG9HO+7z/zCPcl1+YU+i7dW4o0y+aFxFgxTMG+ExBQpqT/k2ao8g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/region-config-resolver@3.972.3': + resolution: {integrity: sha512-v4J8qYAWfOMcZ4MJUyatntOicTzEMaU7j3OpkRCGGFSL2NgXQ5VbxauIyORA+pxdKZ0qQG2tCQjQjZDlXEC3Ow==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/s3-request-presigner@3.985.0': + resolution: {integrity: sha512-lPnf977GFM4cMLJ7X+ThktKMe/0CXIfX+wz1z+sUT7yagPL2IRyiNUPFZ0VTEGBo1gRhHEDPWy6yzk8WWRFsvg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.985.0': + resolution: {integrity: sha512-W6hTSOPiSbh4IdTYVxN7xHjpCh0qvfQU1GKGBzGQm0ZEIOaMmWqiDEvFfyGYKmfBvumT8vHKxQRTX0av9omtIg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.985.0': + resolution: {integrity: sha512-+hwpHZyEq8k+9JL2PkE60V93v2kNhUIv7STFt+EAez1UJsJOQDhc5LpzEX66pNjclI5OTwBROs/DhJjC/BtMjQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.973.1': + resolution: {integrity: sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-arn-parser@3.972.2': + resolution: {integrity: sha512-VkykWbqMjlSgBFDyrY3nOSqupMc6ivXuGmvci6Q3NnLq5kC+mKQe2QBZ4nrWRE/jqOxeFP2uYzLtwncYYcvQDg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-endpoints@3.985.0': + resolution: {integrity: sha512-vth7UfGSUR3ljvaq8V4Rc62FsM7GUTH/myxPWkaEgOrprz1/Pc72EgTXxj+cPPPDAfHFIpjhkB7T7Td0RJx+BA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-format-url@3.972.3': + resolution: {integrity: sha512-n7F2ycckcKFXa01vAsT/SJdjFHfKH9s96QHcs5gn8AaaigASICeME8WdUL9uBp8XV/OVwEt8+6gzn6KFUgQa8g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-locate-window@3.965.4': + resolution: {integrity: sha512-H1onv5SkgPBK2P6JR2MjGgbOnttoNzSPIRoeZTNPZYyaplwGg50zS3amXvXqF0/qfXpWEC9rLWU564QTB9bSog==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-user-agent-browser@3.972.3': + resolution: {integrity: sha512-JurOwkRUcXD/5MTDBcqdyQ9eVedtAsZgw5rBwktsPTN7QtPiS2Ld1jkJepNgYoCufz1Wcut9iup7GJDoIHp8Fw==} + + '@aws-sdk/util-user-agent-node@3.972.5': + resolution: {integrity: sha512-GsUDF+rXyxDZkkJxUsDxnA67FG+kc5W1dnloCFLl6fWzceevsCYzJpASBzT+BPjwUgREE6FngfJYYYMQUY5fZQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + + '@aws-sdk/xml-builder@3.972.4': + resolution: {integrity: sha512-0zJ05ANfYqI6+rGqj8samZBFod0dPPousBjLEqg8WdxSgbMAkRgLyn81lP215Do0rFJ/17LIXwr7q0yK24mP6Q==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.2.3': + resolution: {integrity: sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==} + engines: {node: '>=18.0.0'} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.6': + resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@drizzle-team/brocli@0.10.2': + resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + + '@esbuild-kit/core-utils@3.3.2': + resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild-kit/esm-loader@2.6.5': + resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild/aix-ppc64@0.19.12': + resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.19.12': + resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.19.12': + resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.19.12': + resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.19.12': + resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.19.12': + resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.19.12': + resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.19.12': + resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.19.12': + resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.19.12': + resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.19.12': + resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.19.12': + resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.19.12': + resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.19.12': + resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.19.12': + resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.19.12': + resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.19.12': + resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.19.12': + resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.19.12': + resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.19.12': + resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.19.12': + resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.19.12': + resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.19.12': + resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.1': + resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.3': + resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.2': + resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@fastify/accept-negotiator@2.0.1': + resolution: {integrity: sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==} + + '@fastify/ajv-compiler@4.0.5': + resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==} + + '@fastify/busboy@3.2.0': + resolution: {integrity: sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==} + + '@fastify/cors@10.1.0': + resolution: {integrity: sha512-MZyBCBJtII60CU9Xme/iE4aEy8G7QpzGR8zkdXZkDFt7ElEMachbE61tfhAG/bvSaULlqlf0huMT12T7iqEmdQ==} + + '@fastify/deepmerge@3.2.0': + resolution: {integrity: sha512-aO5giNgFN+rD4fMUAkro9nEL7c9gh5Q3lh0ZGKMDAhQAytf22HLicF/qZ2EYTDmH+XL2WvdazwBfOdmp6NiwBg==} + + '@fastify/error@4.2.0': + resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} + + '@fastify/fast-json-stringify-compiler@5.0.3': + resolution: {integrity: sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==} + + '@fastify/forwarded@3.0.1': + resolution: {integrity: sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==} + + '@fastify/jwt@9.1.0': + resolution: {integrity: sha512-CiGHCnS5cPMdb004c70sUWhQTfzrJHAeTywt7nVw6dAiI0z1o4WRvU94xfijhkaId4bIxTCOjFgn4sU+Gvk43w==} + + '@fastify/merge-json-schemas@0.2.1': + resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} + + '@fastify/multipart@9.4.0': + resolution: {integrity: sha512-Z404bzZeLSXTBmp/trCBuoVFX28pM7rhv849Q5TsbTFZHuk1lc4QjQITTPK92DKVpXmNtJXeHSSc7GYvqFpxAQ==} + + '@fastify/proxy-addr@5.1.0': + resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} + + '@fastify/send@4.1.0': + resolution: {integrity: sha512-TMYeQLCBSy2TOFmV95hQWkiTYgC/SEx7vMdV+wnZVX4tt8VBLKzmH8vV9OzJehV0+XBfg+WxPMt5wp+JBUKsVw==} + + '@fastify/sensible@6.0.4': + resolution: {integrity: sha512-1vxcCUlPMew6WroK8fq+LVOwbsLtX+lmuRuqpcp6eYqu6vmkLwbKTdBWAZwbeaSgCfW4tzUpTIHLLvTiQQ1BwQ==} + + '@fastify/static@9.0.0': + resolution: {integrity: sha512-r64H8Woe/vfilg5RTy7lwWlE8ZZcTrc3kebYFMEUBrMqlydhQyoiExQXdYAy2REVpST/G35+stAM8WYp1WGmMA==} + + '@fastify/swagger-ui@5.2.5': + resolution: {integrity: sha512-ky3I0LAkXKX/prwSDpoQ3kscBKsj2Ha6Gp1/JfgQSqyx0bm9F2bE//XmGVGj2cR9l5hUjZYn60/hqn7e+OLgWQ==} + + '@fastify/swagger@9.7.0': + resolution: {integrity: sha512-Vp1SC1GC2Hrkd3faFILv86BzUNyFz5N4/xdExqtCgkGASOzn/x+eMe4qXIGq7cdT6wif/P/oa6r1Ruqx19paZA==} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@isaacs/balanced-match@4.0.1': + resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} + engines: {node: 20 || >=22} + + '@isaacs/brace-expansion@5.0.1': + resolution: {integrity: sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==} + engines: {node: 20 || >=22} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@lukeed/ms@2.0.2': + resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} + engines: {node: '>=8'} + + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@rollup/rollup-android-arm-eabi@4.57.1': + resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.57.1': + resolution: {integrity: sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.57.1': + resolution: {integrity: sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.57.1': + resolution: {integrity: sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.57.1': + resolution: {integrity: sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.57.1': + resolution: {integrity: sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.57.1': + resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.57.1': + resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.57.1': + resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.57.1': + resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.57.1': + resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.57.1': + resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.57.1': + resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.57.1': + resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.57.1': + resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.57.1': + resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.57.1': + resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.57.1': + resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.57.1': + resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.57.1': + resolution: {integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.57.1': + resolution: {integrity: sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.57.1': + resolution: {integrity: sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.57.1': + resolution: {integrity: sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.57.1': + resolution: {integrity: sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.57.1': + resolution: {integrity: sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==} + cpu: [x64] + os: [win32] + + '@smithy/abort-controller@4.2.8': + resolution: {integrity: sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw==} + engines: {node: '>=18.0.0'} + + '@smithy/chunked-blob-reader-native@4.2.1': + resolution: {integrity: sha512-lX9Ay+6LisTfpLid2zZtIhSEjHMZoAR5hHCR4H7tBz/Zkfr5ea8RcQ7Tk4mi0P76p4cN+Btz16Ffno7YHpKXnQ==} + engines: {node: '>=18.0.0'} + + '@smithy/chunked-blob-reader@5.2.0': + resolution: {integrity: sha512-WmU0TnhEAJLWvfSeMxBNe5xtbselEO8+4wG0NtZeL8oR21WgH1xiO37El+/Y+H/Ie4SCwBy3MxYWmOYaGgZueA==} + engines: {node: '>=18.0.0'} + + '@smithy/config-resolver@4.4.6': + resolution: {integrity: sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ==} + engines: {node: '>=18.0.0'} + + '@smithy/core@3.22.1': + resolution: {integrity: sha512-x3ie6Crr58MWrm4viHqqy2Du2rHYZjwu8BekasrQx4ca+Y24dzVAwq3yErdqIbc2G3I0kLQA13PQ+/rde+u65g==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.2.8': + resolution: {integrity: sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-codec@4.2.8': + resolution: {integrity: sha512-jS/O5Q14UsufqoGhov7dHLOPCzkYJl9QDzusI2Psh4wyYx/izhzvX9P4D69aTxcdfVhEPhjK+wYyn/PzLjKbbw==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-browser@4.2.8': + resolution: {integrity: sha512-MTfQT/CRQz5g24ayXdjg53V0mhucZth4PESoA5IhvaWVDTOQLfo8qI9vzqHcPsdd2v6sqfTYqF5L/l+pea5Uyw==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-config-resolver@4.3.8': + resolution: {integrity: sha512-ah12+luBiDGzBruhu3efNy1IlbwSEdNiw8fOZksoKoWW1ZHvO/04MQsdnws/9Aj+5b0YXSSN2JXKy/ClIsW8MQ==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-node@4.2.8': + resolution: {integrity: sha512-cYpCpp29z6EJHa5T9WL0KAlq3SOKUQkcgSoeRfRVwjGgSFl7Uh32eYGt7IDYCX20skiEdRffyDpvF2efEZPC0A==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-universal@4.2.8': + resolution: {integrity: sha512-iJ6YNJd0bntJYnX6s52NC4WFYcZeKrPUr1Kmmr5AwZcwCSzVpS7oavAmxMR7pMq7V+D1G4s9F5NJK0xwOsKAlQ==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.3.9': + resolution: {integrity: sha512-I4UhmcTYXBrct03rwzQX1Y/iqQlzVQaPxWjCjula++5EmWq9YGBrx6bbGqluGc1f0XEfhSkiY4jhLgbsJUMKRA==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-blob-browser@4.2.9': + resolution: {integrity: sha512-m80d/iicI7DlBDxyQP6Th7BW/ejDGiF0bgI754+tiwK0lgMkcaIBgvwwVc7OFbY4eUzpGtnig52MhPAEJ7iNYg==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-node@4.2.8': + resolution: {integrity: sha512-7ZIlPbmaDGxVoxErDZnuFG18WekhbA/g2/i97wGj+wUBeS6pcUeAym8u4BXh/75RXWhgIJhyC11hBzig6MljwA==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-stream-node@4.2.8': + resolution: {integrity: sha512-v0FLTXgHrTeheYZFGhR+ehX5qUm4IQsjAiL9qehad2cyjMWcN2QG6/4mSwbSgEQzI7jwfoXj7z4fxZUx/Mhj2w==} + engines: {node: '>=18.0.0'} + + '@smithy/invalid-dependency@4.2.8': + resolution: {integrity: sha512-N9iozRybwAQ2dn9Fot9kI6/w9vos2oTXLhtK7ovGqwZjlOcxu6XhPlpLpC+INsxktqHinn5gS2DXDjDF2kG5sQ==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/is-array-buffer@4.2.0': + resolution: {integrity: sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==} + engines: {node: '>=18.0.0'} + + '@smithy/md5-js@4.2.8': + resolution: {integrity: sha512-oGMaLj4tVZzLi3itBa9TCswgMBr7k9b+qKYowQ6x1rTyTuO1IU2YHdHUa+891OsOH+wCsH7aTPRsTJO3RMQmjQ==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-content-length@4.2.8': + resolution: {integrity: sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-endpoint@4.4.13': + resolution: {integrity: sha512-x6vn0PjYmGdNuKh/juUJJewZh7MoQ46jYaJ2mvekF4EesMuFfrl4LaW/k97Zjf8PTCPQmPgMvwewg7eNoH9n5w==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-retry@4.4.30': + resolution: {integrity: sha512-CBGyFvN0f8hlnqKH/jckRDz78Snrp345+PVk8Ux7pnkUCW97Iinse59lY78hBt04h1GZ6hjBN94BRwZy1xC8Bg==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-serde@4.2.9': + resolution: {integrity: sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-stack@4.2.8': + resolution: {integrity: sha512-w6LCfOviTYQjBctOKSwy6A8FIkQy7ICvglrZFl6Bw4FmcQ1Z420fUtIhxaUZZshRe0VCq4kvDiPiXrPZAe8oRA==} + engines: {node: '>=18.0.0'} + + '@smithy/node-config-provider@4.3.8': + resolution: {integrity: sha512-aFP1ai4lrbVlWjfpAfRSL8KFcnJQYfTl5QxLJXY32vghJrDuFyPZ6LtUL+JEGYiFRG1PfPLHLoxj107ulncLIg==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.4.9': + resolution: {integrity: sha512-KX5Wml5mF+luxm1szW4QDz32e3NObgJ4Fyw+irhph4I/2geXwUy4jkIMUs5ZPGflRBeR6BUkC2wqIab4Llgm3w==} + engines: {node: '>=18.0.0'} + + '@smithy/property-provider@4.2.8': + resolution: {integrity: sha512-EtCTbyIveCKeOXDSWSdze3k612yCPq1YbXsbqX3UHhkOSW8zKsM9NOJG5gTIya0vbY2DIaieG8pKo1rITHYL0w==} + engines: {node: '>=18.0.0'} + + '@smithy/protocol-http@5.3.8': + resolution: {integrity: sha512-QNINVDhxpZ5QnP3aviNHQFlRogQZDfYlCkQT+7tJnErPQbDhysondEjhikuANxgMsZrkGeiAxXy4jguEGsDrWQ==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-builder@4.2.8': + resolution: {integrity: sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-parser@4.2.8': + resolution: {integrity: sha512-vUurovluVy50CUlazOiXkPq40KGvGWSdmusa3130MwrR1UNnNgKAlj58wlOe61XSHRpUfIIh6cE0zZ8mzKaDPA==} + engines: {node: '>=18.0.0'} + + '@smithy/service-error-classification@4.2.8': + resolution: {integrity: sha512-mZ5xddodpJhEt3RkCjbmUQuXUOaPNTkbMGR0bcS8FE0bJDLMZlhmpgrvPNCYglVw5rsYTpSnv19womw9WWXKQQ==} + engines: {node: '>=18.0.0'} + + '@smithy/shared-ini-file-loader@4.4.3': + resolution: {integrity: sha512-DfQjxXQnzC5UbCUPeC3Ie8u+rIWZTvuDPAGU/BxzrOGhRvgUanaP68kDZA+jaT3ZI+djOf+4dERGlm9mWfFDrg==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.3.8': + resolution: {integrity: sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg==} + engines: {node: '>=18.0.0'} + + '@smithy/smithy-client@4.11.2': + resolution: {integrity: sha512-SCkGmFak/xC1n7hKRsUr6wOnBTJ3L22Qd4e8H1fQIuKTAjntwgU8lrdMe7uHdiT2mJAOWA/60qaW9tiMu69n1A==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.12.0': + resolution: {integrity: sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw==} + engines: {node: '>=18.0.0'} + + '@smithy/url-parser@4.2.8': + resolution: {integrity: sha512-NQho9U68TGMEU639YkXnVMV3GEFFULmmaWdlu1E9qzyIePOHsoSnagTGSDv1Zi8DCNN6btxOSdgmy5E/hsZwhA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-base64@4.3.0': + resolution: {integrity: sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-browser@4.2.0': + resolution: {integrity: sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-node@4.2.1': + resolution: {integrity: sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-buffer-from@4.2.0': + resolution: {integrity: sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==} + engines: {node: '>=18.0.0'} + + '@smithy/util-config-provider@4.2.0': + resolution: {integrity: sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-browser@4.3.29': + resolution: {integrity: sha512-nIGy3DNRmOjaYaaKcQDzmWsro9uxlaqUOhZDHQed9MW/GmkBZPtnU70Pu1+GT9IBmUXwRdDuiyaeiy9Xtpn3+Q==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-node@4.2.32': + resolution: {integrity: sha512-7dtFff6pu5fsjqrVve0YMhrnzJtccCWDacNKOkiZjJ++fmjGExmmSu341x+WU6Oc1IccL7lDuaUj7SfrHpWc5Q==} + engines: {node: '>=18.0.0'} + + '@smithy/util-endpoints@3.2.8': + resolution: {integrity: sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-hex-encoding@4.2.0': + resolution: {integrity: sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-middleware@4.2.8': + resolution: {integrity: sha512-PMqfeJxLcNPMDgvPbbLl/2Vpin+luxqTGPpW3NAQVLbRrFRzTa4rNAASYeIGjRV9Ytuhzny39SpyU04EQreF+A==} + engines: {node: '>=18.0.0'} + + '@smithy/util-retry@4.2.8': + resolution: {integrity: sha512-CfJqwvoRY0kTGe5AkQokpURNCT1u/MkRzMTASWMPPo2hNSnKtF1D45dQl3DE2LKLr4m+PW9mCeBMJr5mCAVThg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-stream@4.5.11': + resolution: {integrity: sha512-lKmZ0S/3Qj2OF5H1+VzvDLb6kRxGzZHq6f3rAsoSu5cTLGsn3v3VQBA8czkNNXlLjoFEtVu3OQT2jEeOtOE2CA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-uri-escape@4.2.0': + resolution: {integrity: sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@4.2.0': + resolution: {integrity: sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-waiter@4.2.8': + resolution: {integrity: sha512-n+lahlMWk+aejGuax7DPWtqav8HYnWxQwR+LCG2BgCUmaGcTe9qZCFsmw8TMg9iG75HOwhrJCX9TCJRLH+Yzqg==} + engines: {node: '>=18.0.0'} + + '@smithy/uuid@1.1.0': + resolution: {integrity: sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==} + engines: {node: '>=18.0.0'} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@22.19.10': + resolution: {integrity: sha512-tF5VOugLS/EuDlTBijk0MqABfP8UxgYazTLo3uIn3b4yJgg26QRbVYJYsDtHrjdDUIRfP70+VfhTTc+CE1yskw==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react@18.3.28': + resolution: {integrity: sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==} + + '@typescript-eslint/eslint-plugin@8.54.0': + resolution: {integrity: sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.54.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.54.0': + resolution: {integrity: sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.54.0': + resolution: {integrity: sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.54.0': + resolution: {integrity: sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.54.0': + resolution: {integrity: sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.54.0': + resolution: {integrity: sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.54.0': + resolution: {integrity: sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.54.0': + resolution: {integrity: sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.54.0': + resolution: {integrity: sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.54.0': + resolution: {integrity: sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + + abstract-logging@2.0.1: + resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + asn1.js@5.4.1: + resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + avvio@9.1.0: + resolution: {integrity: sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + baseline-browser-mapping@2.9.19: + resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} + hasBin: true + + bn.js@4.12.2: + resolution: {integrity: sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==} + + bowser@2.13.1: + resolution: {integrity: sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001769: + resolution: {integrity: sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + drizzle-kit@0.28.1: + resolution: {integrity: sha512-JimOV+ystXTWMgZkLHYHf2w3oS28hxiH1FR0dkmJLc7GHzdGJoJAQtQS5DRppnabsRZwE2U1F6CuezVBgmsBBQ==} + hasBin: true + + drizzle-orm@0.36.4: + resolution: {integrity: sha512-1OZY3PXD7BR00Gl61UUOFihslDldfH4NFRH2MbP54Yxi0G/PKn4HfO65JYZ7c16DeP3SpM3Aw+VXVG9j6CRSXA==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=3' + '@electric-sql/pglite': '>=0.2.0' + '@libsql/client': '>=0.10.0' + '@libsql/client-wasm': '>=0.10.0' + '@neondatabase/serverless': '>=0.10.0' + '@op-engineering/op-sqlite': '>=2' + '@opentelemetry/api': ^1.4.1 + '@planetscale/database': '>=1' + '@prisma/client': '*' + '@tidbcloud/serverless': '*' + '@types/better-sqlite3': '*' + '@types/pg': '*' + '@types/react': '>=18' + '@types/sql.js': '*' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' + better-sqlite3: '>=7' + bun-types: '*' + expo-sqlite: '>=14.0.0' + knex: '*' + kysely: '*' + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + prisma: '*' + react: '>=18' + sql.js: '>=1' + sqlite3: '>=5' + peerDependenciesMeta: + '@aws-sdk/client-rds-data': + optional: true + '@cloudflare/workers-types': + optional: true + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + '@libsql/client-wasm': + optional: true + '@neondatabase/serverless': + optional: true + '@op-engineering/op-sqlite': + optional: true + '@opentelemetry/api': + optional: true + '@planetscale/database': + optional: true + '@prisma/client': + optional: true + '@tidbcloud/serverless': + optional: true + '@types/better-sqlite3': + optional: true + '@types/pg': + optional: true + '@types/react': + optional: true + '@types/sql.js': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + prisma: + optional: true + react: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + electron-to-chromium@1.5.286: + resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild-register@3.6.0: + resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} + peerDependencies: + esbuild: '>=0.12 <1' + + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.19.12: + resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.39.2: + resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-json-stringify@6.3.0: + resolution: {integrity: sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA==} + + fast-jwt@5.0.6: + resolution: {integrity: sha512-LPE7OCGUl11q3ZgW681cEU2d0d2JZ37hhJAmetCgNyW8waVaJVZXhyFF6U2so1Iim58Yc7pfxJe2P7MNetQH2g==} + engines: {node: '>=20'} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fast-xml-parser@5.3.4: + resolution: {integrity: sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA==} + hasBin: true + + fastfall@1.5.1: + resolution: {integrity: sha512-KH6p+Z8AKPXnmA7+Iz2Lh8ARCMr+8WNPVludm1LGkZoD2MjY6LVnRMtTKhkdzI+jr0RzQWXKzKyBJm1zoHEL4Q==} + engines: {node: '>=0.10.0'} + + fastify-plugin@5.1.0: + resolution: {integrity: sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==} + + fastify@5.7.4: + resolution: {integrity: sha512-e6l5NsRdaEP8rdD8VR0ErJASeyaRbzXYpmkrpr2SuvuMq6Si3lvsaVy5C+7gLanEkvjpMDzBXWE5HPeb/hgTxA==} + + fastparallel@2.4.1: + resolution: {integrity: sha512-qUmhxPgNHmvRjZKBFUNI0oZuuH9OlSIOXmJ98lhKPxMZZ7zS/Fi0wRHOihDSz0R1YiIOjxzOY4bq65YTcdBi2Q==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fastseries@1.7.2: + resolution: {integrity: sha512-dTPFrPGS8SNSzAt7u/CbMKCJ3s01N04s4JFbORHcmyvVfVKmbhMD1VtRbh5enGHxkaQDqWyLefiKOGGmohGDDQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + find-my-way@9.4.0: + resolution: {integrity: sha512-5Ye4vHsypZRYtS01ob/iwHzGRUDELlsoCftI/OZFhcLs1M0tkGPcXldE80TAZC5yYuJMBPJQQ43UHlqbJWiX2w==} + engines: {node: '>=20'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-tsconfig@4.13.6: + resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@13.0.1: + resolution: {integrity: sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w==} + engines: {node: 20 || >=22} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ipaddr.js@2.3.0: + resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==} + engines: {node: '>= 10'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-ref-resolver@3.0.0: + resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} + + json-schema-resolver@3.0.0: + resolution: {integrity: sha512-HqMnbz0tz2DaEJ3ntsqtx3ezzZyDE7G56A/pPY/NGmrPu76UzsWquOpHFRAf5beTNXoH2LU5cQePVvRli1nchA==} + engines: {node: '>=20'} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + light-my-request@6.6.0: + resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@11.2.5: + resolution: {integrity: sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + + minimatch@10.1.2: + resolution: {integrity: sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==} + engines: {node: 20 || >=22} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + mnemonist@0.40.0: + resolution: {integrity: sha512-kdd8AFNig2AD5Rkih7EPCXhu/iMvwevQFX/uEiGhZyPZi7fHqOoF4V4kHLpCfysxXMgQ4B52kdPMCwARshKvEg==} + + mnemonist@0.40.3: + resolution: {integrity: sha512-Vjyr90sJ23CKKH/qPAgUKicw/v6pRoamxIEDFOF8uSgFME7DqPRpHgRTejWVjkdGg5dXj0/NyxZHZ9bcjH+2uQ==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + + obliterator@2.0.5: + resolution: {integrity: sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==} + + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + + openapi-types@12.1.3: + resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@2.0.1: + resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} + engines: {node: 20 || >=22} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@10.3.0: + resolution: {integrity: sha512-0GNPNzHXBKw6U/InGe79A3Crzyk9bcSyObF9/Gfo9DLEf5qj5RF50RSjsu0W1rZ6ZqRGdzDFCRBQvi9/rSGPtA==} + hasBin: true + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + postgres@3.4.8: + resolution: {integrity: sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg==} + engines: {node: '>=12'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + process-warning@4.0.1: + resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} + + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react-router-dom@7.13.0: + resolution: {integrity: sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.13.0: + resolution: {integrity: sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + ret@0.5.0: + resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} + engines: {node: '>=10'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rollup@4.57.1: + resolution: {integrity: sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-regex2@5.0.0: + resolution: {integrity: sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw==} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + secure-json-parse@4.1.0: + resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + sonic-boom@4.2.0: + resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + steed@1.1.3: + resolution: {integrity: sha512-EUkci0FAUiE4IvGTSKcDJIQ/eRUP2JJb56+fvZ4sdnguLTqIdKjSxUe138poW8mkvKWXW2sFPrgTsxqoISnmoA==} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strnum@2.1.2: + resolution: {integrity: sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + thread-stream@4.0.0: + resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} + engines: {node: '>=20'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + + toad-cache@3.7.0: + resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} + engines: {node: '>=12'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + typescript-eslint@8.54.0: + resolution: {integrity: sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vite@6.4.1: + resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yaml@2.8.2: + resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} + engines: {node: '>= 14.6'} + hasBin: true + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@aws-crypto/crc32@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.1 + tslib: 2.8.1 + + '@aws-crypto/crc32c@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.1 + tslib: 2.8.1 + + '@aws-crypto/sha1-browser@5.2.0': + dependencies: + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.1 + '@aws-sdk/util-locate-window': 3.965.4 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.1 + '@aws-sdk/util-locate-window': 3.965.4 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.1 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.973.1 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/client-s3@3.985.0': + dependencies: + '@aws-crypto/sha1-browser': 5.2.0 + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.973.7 + '@aws-sdk/credential-provider-node': 3.972.6 + '@aws-sdk/middleware-bucket-endpoint': 3.972.3 + '@aws-sdk/middleware-expect-continue': 3.972.3 + '@aws-sdk/middleware-flexible-checksums': 3.972.5 + '@aws-sdk/middleware-host-header': 3.972.3 + '@aws-sdk/middleware-location-constraint': 3.972.3 + '@aws-sdk/middleware-logger': 3.972.3 + '@aws-sdk/middleware-recursion-detection': 3.972.3 + '@aws-sdk/middleware-sdk-s3': 3.972.7 + '@aws-sdk/middleware-ssec': 3.972.3 + '@aws-sdk/middleware-user-agent': 3.972.7 + '@aws-sdk/region-config-resolver': 3.972.3 + '@aws-sdk/signature-v4-multi-region': 3.985.0 + '@aws-sdk/types': 3.973.1 + '@aws-sdk/util-endpoints': 3.985.0 + '@aws-sdk/util-user-agent-browser': 3.972.3 + '@aws-sdk/util-user-agent-node': 3.972.5 + '@smithy/config-resolver': 4.4.6 + '@smithy/core': 3.22.1 + '@smithy/eventstream-serde-browser': 4.2.8 + '@smithy/eventstream-serde-config-resolver': 4.3.8 + '@smithy/eventstream-serde-node': 4.2.8 + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/hash-blob-browser': 4.2.9 + '@smithy/hash-node': 4.2.8 + '@smithy/hash-stream-node': 4.2.8 + '@smithy/invalid-dependency': 4.2.8 + '@smithy/md5-js': 4.2.8 + '@smithy/middleware-content-length': 4.2.8 + '@smithy/middleware-endpoint': 4.4.13 + '@smithy/middleware-retry': 4.4.30 + '@smithy/middleware-serde': 4.2.9 + '@smithy/middleware-stack': 4.2.8 + '@smithy/node-config-provider': 4.3.8 + '@smithy/node-http-handler': 4.4.9 + '@smithy/protocol-http': 5.3.8 + '@smithy/smithy-client': 4.11.2 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.29 + '@smithy/util-defaults-mode-node': 4.2.32 + '@smithy/util-endpoints': 3.2.8 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-retry': 4.2.8 + '@smithy/util-stream': 4.5.11 + '@smithy/util-utf8': 4.2.0 + '@smithy/util-waiter': 4.2.8 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/client-sso@3.985.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.973.7 + '@aws-sdk/middleware-host-header': 3.972.3 + '@aws-sdk/middleware-logger': 3.972.3 + '@aws-sdk/middleware-recursion-detection': 3.972.3 + '@aws-sdk/middleware-user-agent': 3.972.7 + '@aws-sdk/region-config-resolver': 3.972.3 + '@aws-sdk/types': 3.973.1 + '@aws-sdk/util-endpoints': 3.985.0 + '@aws-sdk/util-user-agent-browser': 3.972.3 + '@aws-sdk/util-user-agent-node': 3.972.5 + '@smithy/config-resolver': 4.4.6 + '@smithy/core': 3.22.1 + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/hash-node': 4.2.8 + '@smithy/invalid-dependency': 4.2.8 + '@smithy/middleware-content-length': 4.2.8 + '@smithy/middleware-endpoint': 4.4.13 + '@smithy/middleware-retry': 4.4.30 + '@smithy/middleware-serde': 4.2.9 + '@smithy/middleware-stack': 4.2.8 + '@smithy/node-config-provider': 4.3.8 + '@smithy/node-http-handler': 4.4.9 + '@smithy/protocol-http': 5.3.8 + '@smithy/smithy-client': 4.11.2 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.29 + '@smithy/util-defaults-mode-node': 4.2.32 + '@smithy/util-endpoints': 3.2.8 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-retry': 4.2.8 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/core@3.973.7': + dependencies: + '@aws-sdk/types': 3.973.1 + '@aws-sdk/xml-builder': 3.972.4 + '@smithy/core': 3.22.1 + '@smithy/node-config-provider': 4.3.8 + '@smithy/property-provider': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/signature-v4': 5.3.8 + '@smithy/smithy-client': 4.11.2 + '@smithy/types': 4.12.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@aws-sdk/crc64-nvme@3.972.0': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.5': + dependencies: + '@aws-sdk/core': 3.973.7 + '@aws-sdk/types': 3.973.1 + '@smithy/property-provider': 4.2.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.7': + dependencies: + '@aws-sdk/core': 3.973.7 + '@aws-sdk/types': 3.973.1 + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/node-http-handler': 4.4.9 + '@smithy/property-provider': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/smithy-client': 4.11.2 + '@smithy/types': 4.12.0 + '@smithy/util-stream': 4.5.11 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.972.5': + dependencies: + '@aws-sdk/core': 3.973.7 + '@aws-sdk/credential-provider-env': 3.972.5 + '@aws-sdk/credential-provider-http': 3.972.7 + '@aws-sdk/credential-provider-login': 3.972.5 + '@aws-sdk/credential-provider-process': 3.972.5 + '@aws-sdk/credential-provider-sso': 3.972.5 + '@aws-sdk/credential-provider-web-identity': 3.972.5 + '@aws-sdk/nested-clients': 3.985.0 + '@aws-sdk/types': 3.973.1 + '@smithy/credential-provider-imds': 4.2.8 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-login@3.972.5': + dependencies: + '@aws-sdk/core': 3.973.7 + '@aws-sdk/nested-clients': 3.985.0 + '@aws-sdk/types': 3.973.1 + '@smithy/property-provider': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-node@3.972.6': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.5 + '@aws-sdk/credential-provider-http': 3.972.7 + '@aws-sdk/credential-provider-ini': 3.972.5 + '@aws-sdk/credential-provider-process': 3.972.5 + '@aws-sdk/credential-provider-sso': 3.972.5 + '@aws-sdk/credential-provider-web-identity': 3.972.5 + '@aws-sdk/types': 3.973.1 + '@smithy/credential-provider-imds': 4.2.8 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-process@3.972.5': + dependencies: + '@aws-sdk/core': 3.973.7 + '@aws-sdk/types': 3.973.1 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.972.5': + dependencies: + '@aws-sdk/client-sso': 3.985.0 + '@aws-sdk/core': 3.973.7 + '@aws-sdk/token-providers': 3.985.0 + '@aws-sdk/types': 3.973.1 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-web-identity@3.972.5': + dependencies: + '@aws-sdk/core': 3.973.7 + '@aws-sdk/nested-clients': 3.985.0 + '@aws-sdk/types': 3.973.1 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/middleware-bucket-endpoint@3.972.3': + dependencies: + '@aws-sdk/types': 3.973.1 + '@aws-sdk/util-arn-parser': 3.972.2 + '@smithy/node-config-provider': 4.3.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + '@smithy/util-config-provider': 4.2.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-expect-continue@3.972.3': + dependencies: + '@aws-sdk/types': 3.973.1 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-flexible-checksums@3.972.5': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@aws-crypto/crc32c': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/core': 3.973.7 + '@aws-sdk/crc64-nvme': 3.972.0 + '@aws-sdk/types': 3.973.1 + '@smithy/is-array-buffer': 4.2.0 + '@smithy/node-config-provider': 4.3.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-stream': 4.5.11 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-host-header@3.972.3': + dependencies: + '@aws-sdk/types': 3.973.1 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-location-constraint@3.972.3': + dependencies: + '@aws-sdk/types': 3.973.1 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-logger@3.972.3': + dependencies: + '@aws-sdk/types': 3.973.1 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-recursion-detection@3.972.3': + dependencies: + '@aws-sdk/types': 3.973.1 + '@aws/lambda-invoke-store': 0.2.3 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-s3@3.972.7': + dependencies: + '@aws-sdk/core': 3.973.7 + '@aws-sdk/types': 3.973.1 + '@aws-sdk/util-arn-parser': 3.972.2 + '@smithy/core': 3.22.1 + '@smithy/node-config-provider': 4.3.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/signature-v4': 5.3.8 + '@smithy/smithy-client': 4.11.2 + '@smithy/types': 4.12.0 + '@smithy/util-config-provider': 4.2.0 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-stream': 4.5.11 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-ssec@3.972.3': + dependencies: + '@aws-sdk/types': 3.973.1 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-user-agent@3.972.7': + dependencies: + '@aws-sdk/core': 3.973.7 + '@aws-sdk/types': 3.973.1 + '@aws-sdk/util-endpoints': 3.985.0 + '@smithy/core': 3.22.1 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.985.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.973.7 + '@aws-sdk/middleware-host-header': 3.972.3 + '@aws-sdk/middleware-logger': 3.972.3 + '@aws-sdk/middleware-recursion-detection': 3.972.3 + '@aws-sdk/middleware-user-agent': 3.972.7 + '@aws-sdk/region-config-resolver': 3.972.3 + '@aws-sdk/types': 3.973.1 + '@aws-sdk/util-endpoints': 3.985.0 + '@aws-sdk/util-user-agent-browser': 3.972.3 + '@aws-sdk/util-user-agent-node': 3.972.5 + '@smithy/config-resolver': 4.4.6 + '@smithy/core': 3.22.1 + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/hash-node': 4.2.8 + '@smithy/invalid-dependency': 4.2.8 + '@smithy/middleware-content-length': 4.2.8 + '@smithy/middleware-endpoint': 4.4.13 + '@smithy/middleware-retry': 4.4.30 + '@smithy/middleware-serde': 4.2.9 + '@smithy/middleware-stack': 4.2.8 + '@smithy/node-config-provider': 4.3.8 + '@smithy/node-http-handler': 4.4.9 + '@smithy/protocol-http': 5.3.8 + '@smithy/smithy-client': 4.11.2 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.29 + '@smithy/util-defaults-mode-node': 4.2.32 + '@smithy/util-endpoints': 3.2.8 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-retry': 4.2.8 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/region-config-resolver@3.972.3': + dependencies: + '@aws-sdk/types': 3.973.1 + '@smithy/config-resolver': 4.4.6 + '@smithy/node-config-provider': 4.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/s3-request-presigner@3.985.0': + dependencies: + '@aws-sdk/signature-v4-multi-region': 3.985.0 + '@aws-sdk/types': 3.973.1 + '@aws-sdk/util-format-url': 3.972.3 + '@smithy/middleware-endpoint': 4.4.13 + '@smithy/protocol-http': 5.3.8 + '@smithy/smithy-client': 4.11.2 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.985.0': + dependencies: + '@aws-sdk/middleware-sdk-s3': 3.972.7 + '@aws-sdk/types': 3.973.1 + '@smithy/protocol-http': 5.3.8 + '@smithy/signature-v4': 5.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.985.0': + dependencies: + '@aws-sdk/core': 3.973.7 + '@aws-sdk/nested-clients': 3.985.0 + '@aws-sdk/types': 3.973.1 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/types@3.973.1': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/util-arn-parser@3.972.2': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-endpoints@3.985.0': + dependencies: + '@aws-sdk/types': 3.973.1 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + '@smithy/util-endpoints': 3.2.8 + tslib: 2.8.1 + + '@aws-sdk/util-format-url@3.972.3': + dependencies: + '@aws-sdk/types': 3.973.1 + '@smithy/querystring-builder': 4.2.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.965.4': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-browser@3.972.3': + dependencies: + '@aws-sdk/types': 3.973.1 + '@smithy/types': 4.12.0 + bowser: 2.13.1 + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-node@3.972.5': + dependencies: + '@aws-sdk/middleware-user-agent': 3.972.7 + '@aws-sdk/types': 3.973.1 + '@smithy/node-config-provider': 4.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.4': + dependencies: + '@smithy/types': 4.12.0 + fast-xml-parser: 5.3.4 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.2.3': {} + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.28.6 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.6': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@drizzle-team/brocli@0.10.2': {} + + '@esbuild-kit/core-utils@3.3.2': + dependencies: + esbuild: 0.18.20 + source-map-support: 0.5.21 + + '@esbuild-kit/esm-loader@2.6.5': + dependencies: + '@esbuild-kit/core-utils': 3.3.2 + get-tsconfig: 4.13.6 + + '@esbuild/aix-ppc64@0.19.12': + optional: true + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/aix-ppc64@0.27.3': + optional: true + + '@esbuild/android-arm64@0.18.20': + optional: true + + '@esbuild/android-arm64@0.19.12': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.27.3': + optional: true + + '@esbuild/android-arm@0.18.20': + optional: true + + '@esbuild/android-arm@0.19.12': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-arm@0.27.3': + optional: true + + '@esbuild/android-x64@0.18.20': + optional: true + + '@esbuild/android-x64@0.19.12': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/android-x64@0.27.3': + optional: true + + '@esbuild/darwin-arm64@0.18.20': + optional: true + + '@esbuild/darwin-arm64@0.19.12': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.27.3': + optional: true + + '@esbuild/darwin-x64@0.18.20': + optional: true + + '@esbuild/darwin-x64@0.19.12': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.27.3': + optional: true + + '@esbuild/freebsd-arm64@0.18.20': + optional: true + + '@esbuild/freebsd-arm64@0.19.12': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.27.3': + optional: true + + '@esbuild/freebsd-x64@0.18.20': + optional: true + + '@esbuild/freebsd-x64@0.19.12': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.27.3': + optional: true + + '@esbuild/linux-arm64@0.18.20': + optional: true + + '@esbuild/linux-arm64@0.19.12': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.27.3': + optional: true + + '@esbuild/linux-arm@0.18.20': + optional: true + + '@esbuild/linux-arm@0.19.12': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-arm@0.27.3': + optional: true + + '@esbuild/linux-ia32@0.18.20': + optional: true + + '@esbuild/linux-ia32@0.19.12': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.27.3': + optional: true + + '@esbuild/linux-loong64@0.18.20': + optional: true + + '@esbuild/linux-loong64@0.19.12': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.27.3': + optional: true + + '@esbuild/linux-mips64el@0.18.20': + optional: true + + '@esbuild/linux-mips64el@0.19.12': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.27.3': + optional: true + + '@esbuild/linux-ppc64@0.18.20': + optional: true + + '@esbuild/linux-ppc64@0.19.12': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.27.3': + optional: true + + '@esbuild/linux-riscv64@0.18.20': + optional: true + + '@esbuild/linux-riscv64@0.19.12': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.27.3': + optional: true + + '@esbuild/linux-s390x@0.18.20': + optional: true + + '@esbuild/linux-s390x@0.19.12': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.27.3': + optional: true + + '@esbuild/linux-x64@0.18.20': + optional: true + + '@esbuild/linux-x64@0.19.12': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/linux-x64@0.27.3': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.27.3': + optional: true + + '@esbuild/netbsd-x64@0.18.20': + optional: true + + '@esbuild/netbsd-x64@0.19.12': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.27.3': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.27.3': + optional: true + + '@esbuild/openbsd-x64@0.18.20': + optional: true + + '@esbuild/openbsd-x64@0.19.12': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.27.3': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.27.3': + optional: true + + '@esbuild/sunos-x64@0.18.20': + optional: true + + '@esbuild/sunos-x64@0.19.12': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.27.3': + optional: true + + '@esbuild/win32-arm64@0.18.20': + optional: true + + '@esbuild/win32-arm64@0.19.12': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.27.3': + optional: true + + '@esbuild/win32-ia32@0.18.20': + optional: true + + '@esbuild/win32-ia32@0.19.12': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.27.3': + optional: true + + '@esbuild/win32-x64@0.18.20': + optional: true + + '@esbuild/win32-x64@0.19.12': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@esbuild/win32-x64@0.27.3': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2)': + dependencies: + eslint: 9.39.2 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.1': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.3': + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.2': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@fastify/accept-negotiator@2.0.1': {} + + '@fastify/ajv-compiler@4.0.5': + dependencies: + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + fast-uri: 3.1.0 + + '@fastify/busboy@3.2.0': {} + + '@fastify/cors@10.1.0': + dependencies: + fastify-plugin: 5.1.0 + mnemonist: 0.40.0 + + '@fastify/deepmerge@3.2.0': {} + + '@fastify/error@4.2.0': {} + + '@fastify/fast-json-stringify-compiler@5.0.3': + dependencies: + fast-json-stringify: 6.3.0 + + '@fastify/forwarded@3.0.1': {} + + '@fastify/jwt@9.1.0': + dependencies: + '@fastify/error': 4.2.0 + '@lukeed/ms': 2.0.2 + fast-jwt: 5.0.6 + fastify-plugin: 5.1.0 + steed: 1.1.3 + + '@fastify/merge-json-schemas@0.2.1': + dependencies: + dequal: 2.0.3 + + '@fastify/multipart@9.4.0': + dependencies: + '@fastify/busboy': 3.2.0 + '@fastify/deepmerge': 3.2.0 + '@fastify/error': 4.2.0 + fastify-plugin: 5.1.0 + secure-json-parse: 4.1.0 + + '@fastify/proxy-addr@5.1.0': + dependencies: + '@fastify/forwarded': 3.0.1 + ipaddr.js: 2.3.0 + + '@fastify/send@4.1.0': + dependencies: + '@lukeed/ms': 2.0.2 + escape-html: 1.0.3 + fast-decode-uri-component: 1.0.1 + http-errors: 2.0.1 + mime: 3.0.0 + + '@fastify/sensible@6.0.4': + dependencies: + '@lukeed/ms': 2.0.2 + dequal: 2.0.3 + fastify-plugin: 5.1.0 + forwarded: 0.2.0 + http-errors: 2.0.1 + type-is: 2.0.1 + vary: 1.1.2 + + '@fastify/static@9.0.0': + dependencies: + '@fastify/accept-negotiator': 2.0.1 + '@fastify/send': 4.1.0 + content-disposition: 1.0.1 + fastify-plugin: 5.1.0 + fastq: 1.20.1 + glob: 13.0.1 + + '@fastify/swagger-ui@5.2.5': + dependencies: + '@fastify/static': 9.0.0 + fastify-plugin: 5.1.0 + openapi-types: 12.1.3 + rfdc: 1.4.1 + yaml: 2.8.2 + + '@fastify/swagger@9.7.0': + dependencies: + fastify-plugin: 5.1.0 + json-schema-resolver: 3.0.0 + openapi-types: 12.1.3 + rfdc: 1.4.1 + yaml: 2.8.2 + transitivePeerDependencies: + - supports-color + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@isaacs/balanced-match@4.0.1': {} + + '@isaacs/brace-expansion@5.0.1': + dependencies: + '@isaacs/balanced-match': 4.0.1 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@lukeed/ms@2.0.2': {} + + '@pinojs/redact@0.4.0': {} + + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@rollup/rollup-android-arm-eabi@4.57.1': + optional: true + + '@rollup/rollup-android-arm64@4.57.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.57.1': + optional: true + + '@rollup/rollup-darwin-x64@4.57.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.57.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.57.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.57.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.57.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.57.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.57.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.57.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.57.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.57.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.57.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.57.1': + optional: true + + '@smithy/abort-controller@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/chunked-blob-reader-native@4.2.1': + dependencies: + '@smithy/util-base64': 4.3.0 + tslib: 2.8.1 + + '@smithy/chunked-blob-reader@5.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/config-resolver@4.4.6': + dependencies: + '@smithy/node-config-provider': 4.3.8 + '@smithy/types': 4.12.0 + '@smithy/util-config-provider': 4.2.0 + '@smithy/util-endpoints': 3.2.8 + '@smithy/util-middleware': 4.2.8 + tslib: 2.8.1 + + '@smithy/core@3.22.1': + dependencies: + '@smithy/middleware-serde': 4.2.9 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-stream': 4.5.11 + '@smithy/util-utf8': 4.2.0 + '@smithy/uuid': 1.1.0 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.2.8': + dependencies: + '@smithy/node-config-provider': 4.3.8 + '@smithy/property-provider': 4.2.8 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + tslib: 2.8.1 + + '@smithy/eventstream-codec@4.2.8': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.12.0 + '@smithy/util-hex-encoding': 4.2.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-browser@4.2.8': + dependencies: + '@smithy/eventstream-serde-universal': 4.2.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-config-resolver@4.3.8': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-node@4.2.8': + dependencies: + '@smithy/eventstream-serde-universal': 4.2.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-universal@4.2.8': + dependencies: + '@smithy/eventstream-codec': 4.2.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.3.9': + dependencies: + '@smithy/protocol-http': 5.3.8 + '@smithy/querystring-builder': 4.2.8 + '@smithy/types': 4.12.0 + '@smithy/util-base64': 4.3.0 + tslib: 2.8.1 + + '@smithy/hash-blob-browser@4.2.9': + dependencies: + '@smithy/chunked-blob-reader': 5.2.0 + '@smithy/chunked-blob-reader-native': 4.2.1 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/hash-node@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + '@smithy/util-buffer-from': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/hash-stream-node@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/invalid-dependency@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/is-array-buffer@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/md5-js@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/middleware-content-length@4.2.8': + dependencies: + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/middleware-endpoint@4.4.13': + dependencies: + '@smithy/core': 3.22.1 + '@smithy/middleware-serde': 4.2.9 + '@smithy/node-config-provider': 4.3.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + '@smithy/util-middleware': 4.2.8 + tslib: 2.8.1 + + '@smithy/middleware-retry@4.4.30': + dependencies: + '@smithy/node-config-provider': 4.3.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/service-error-classification': 4.2.8 + '@smithy/smithy-client': 4.11.2 + '@smithy/types': 4.12.0 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-retry': 4.2.8 + '@smithy/uuid': 1.1.0 + tslib: 2.8.1 + + '@smithy/middleware-serde@4.2.9': + dependencies: + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/middleware-stack@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/node-config-provider@4.3.8': + dependencies: + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.4.9': + dependencies: + '@smithy/abort-controller': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/querystring-builder': 4.2.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/property-provider@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/protocol-http@5.3.8': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/querystring-builder@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + '@smithy/util-uri-escape': 4.2.0 + tslib: 2.8.1 + + '@smithy/querystring-parser@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/service-error-classification@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + + '@smithy/shared-ini-file-loader@4.4.3': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/signature-v4@5.3.8': + dependencies: + '@smithy/is-array-buffer': 4.2.0 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + '@smithy/util-hex-encoding': 4.2.0 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-uri-escape': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/smithy-client@4.11.2': + dependencies: + '@smithy/core': 3.22.1 + '@smithy/middleware-endpoint': 4.4.13 + '@smithy/middleware-stack': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + '@smithy/util-stream': 4.5.11 + tslib: 2.8.1 + + '@smithy/types@4.12.0': + dependencies: + tslib: 2.8.1 + + '@smithy/url-parser@4.2.8': + dependencies: + '@smithy/querystring-parser': 4.2.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/util-base64@4.3.0': + dependencies: + '@smithy/util-buffer-from': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/util-body-length-browser@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-body-length-node@4.2.1': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-buffer-from@4.2.0': + dependencies: + '@smithy/is-array-buffer': 4.2.0 + tslib: 2.8.1 + + '@smithy/util-config-provider@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-defaults-mode-browser@4.3.29': + dependencies: + '@smithy/property-provider': 4.2.8 + '@smithy/smithy-client': 4.11.2 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/util-defaults-mode-node@4.2.32': + dependencies: + '@smithy/config-resolver': 4.4.6 + '@smithy/credential-provider-imds': 4.2.8 + '@smithy/node-config-provider': 4.3.8 + '@smithy/property-provider': 4.2.8 + '@smithy/smithy-client': 4.11.2 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/util-endpoints@3.2.8': + dependencies: + '@smithy/node-config-provider': 4.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/util-hex-encoding@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-middleware@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/util-retry@4.2.8': + dependencies: + '@smithy/service-error-classification': 4.2.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/util-stream@4.5.11': + dependencies: + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/node-http-handler': 4.4.9 + '@smithy/types': 4.12.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-buffer-from': 4.2.0 + '@smithy/util-hex-encoding': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/util-uri-escape@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@4.2.0': + dependencies: + '@smithy/util-buffer-from': 4.2.0 + tslib: 2.8.1 + + '@smithy/util-waiter@4.2.8': + dependencies: + '@smithy/abort-controller': 4.2.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/uuid@1.1.0': + dependencies: + tslib: 2.8.1 + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/estree@1.0.8': {} + + '@types/json-schema@7.0.15': {} + + '@types/node@22.19.10': + dependencies: + undici-types: 6.21.0 + + '@types/prop-types@15.7.15': {} + + '@types/react-dom@18.3.7(@types/react@18.3.28)': + dependencies: + '@types/react': 18.3.28 + + '@types/react@18.3.28': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.2.3 + + '@typescript-eslint/eslint-plugin@8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.54.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.54.0 + '@typescript-eslint/type-utils': 8.54.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/utils': 8.54.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.54.0 + eslint: 9.39.2 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.54.0(eslint@9.39.2)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.54.0 + '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.54.0 + debug: 4.4.3 + eslint: 9.39.2 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.54.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.54.0(typescript@5.9.3) + '@typescript-eslint/types': 8.54.0 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.54.0': + dependencies: + '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/visitor-keys': 8.54.0 + + '@typescript-eslint/tsconfig-utils@8.54.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.54.0(eslint@9.39.2)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.54.0(eslint@9.39.2)(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.2 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.54.0': {} + + '@typescript-eslint/typescript-estree@8.54.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.54.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.54.0(typescript@5.9.3) + '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/visitor-keys': 8.54.0 + debug: 4.4.3 + minimatch: 9.0.5 + semver: 7.7.4 + tinyglobby: 0.2.15 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.54.0(eslint@9.39.2)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2) + '@typescript-eslint/scope-manager': 8.54.0 + '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) + eslint: 9.39.2 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.54.0': + dependencies: + '@typescript-eslint/types': 8.54.0 + eslint-visitor-keys: 4.2.1 + + '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@22.19.10)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 6.4.1(@types/node@22.19.10)(tsx@4.21.0)(yaml@2.8.2) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + tinyrainbow: 1.2.0 + + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@22.19.10))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 5.4.21(@types/node@22.19.10) + + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.21 + pathe: 1.1.2 + + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.1 + tinyrainbow: 1.2.0 + + abstract-logging@2.0.1: {} + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + asn1.js@5.4.1: + dependencies: + bn.js: 4.12.2 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + safer-buffer: 2.1.2 + + assertion-error@2.0.1: {} + + atomic-sleep@1.0.0: {} + + avvio@9.1.0: + dependencies: + '@fastify/error': 4.2.0 + fastq: 1.20.1 + + balanced-match@1.0.2: {} + + baseline-browser-mapping@2.9.19: {} + + bn.js@4.12.2: {} + + bowser@2.13.1: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.9.19 + caniuse-lite: 1.0.30001769 + electron-to-chromium: 1.5.286 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + + buffer-from@1.1.2: {} + + cac@6.7.14: {} + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001769: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + check-error@2.1.3: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concat-map@0.0.1: {} + + content-disposition@1.0.1: {} + + content-type@1.0.5: {} + + convert-source-map@2.0.0: {} + + cookie@1.1.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + deep-is@0.1.4: {} + + depd@2.0.0: {} + + dequal@2.0.3: {} + + drizzle-kit@0.28.1: + dependencies: + '@drizzle-team/brocli': 0.10.2 + '@esbuild-kit/esm-loader': 2.6.5 + esbuild: 0.19.12 + esbuild-register: 3.6.0(esbuild@0.19.12) + transitivePeerDependencies: + - supports-color + + drizzle-orm@0.36.4(@types/react@18.3.28)(postgres@3.4.8)(react@18.3.1): + optionalDependencies: + '@types/react': 18.3.28 + postgres: 3.4.8 + react: 18.3.1 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + electron-to-chromium@1.5.286: {} + + es-module-lexer@1.7.0: {} + + esbuild-register@3.6.0(esbuild@0.19.12): + dependencies: + debug: 4.4.3 + esbuild: 0.19.12 + transitivePeerDependencies: + - supports-color + + esbuild@0.18.20: + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + + esbuild@0.19.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.19.12 + '@esbuild/android-arm': 0.19.12 + '@esbuild/android-arm64': 0.19.12 + '@esbuild/android-x64': 0.19.12 + '@esbuild/darwin-arm64': 0.19.12 + '@esbuild/darwin-x64': 0.19.12 + '@esbuild/freebsd-arm64': 0.19.12 + '@esbuild/freebsd-x64': 0.19.12 + '@esbuild/linux-arm': 0.19.12 + '@esbuild/linux-arm64': 0.19.12 + '@esbuild/linux-ia32': 0.19.12 + '@esbuild/linux-loong64': 0.19.12 + '@esbuild/linux-mips64el': 0.19.12 + '@esbuild/linux-ppc64': 0.19.12 + '@esbuild/linux-riscv64': 0.19.12 + '@esbuild/linux-s390x': 0.19.12 + '@esbuild/linux-x64': 0.19.12 + '@esbuild/netbsd-x64': 0.19.12 + '@esbuild/openbsd-x64': 0.19.12 + '@esbuild/sunos-x64': 0.19.12 + '@esbuild/win32-arm64': 0.19.12 + '@esbuild/win32-ia32': 0.19.12 + '@esbuild/win32-x64': 0.19.12 + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@4.0.0: {} + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.39.2: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.1 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.3 + '@eslint/js': 9.39.2 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + esutils@2.0.3: {} + + expect-type@1.3.0: {} + + fast-decode-uri-component@1.0.1: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-json-stringify@6.3.0: + dependencies: + '@fastify/merge-json-schemas': 0.2.1 + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + fast-uri: 3.1.0 + json-schema-ref-resolver: 3.0.0 + rfdc: 1.4.1 + + fast-jwt@5.0.6: + dependencies: + '@lukeed/ms': 2.0.2 + asn1.js: 5.4.1 + ecdsa-sig-formatter: 1.0.11 + mnemonist: 0.40.3 + + fast-levenshtein@2.0.6: {} + + fast-querystring@1.1.2: + dependencies: + fast-decode-uri-component: 1.0.1 + + fast-uri@3.1.0: {} + + fast-xml-parser@5.3.4: + dependencies: + strnum: 2.1.2 + + fastfall@1.5.1: + dependencies: + reusify: 1.1.0 + + fastify-plugin@5.1.0: {} + + fastify@5.7.4: + dependencies: + '@fastify/ajv-compiler': 4.0.5 + '@fastify/error': 4.2.0 + '@fastify/fast-json-stringify-compiler': 5.0.3 + '@fastify/proxy-addr': 5.1.0 + abstract-logging: 2.0.1 + avvio: 9.1.0 + fast-json-stringify: 6.3.0 + find-my-way: 9.4.0 + light-my-request: 6.6.0 + pino: 10.3.0 + process-warning: 5.0.0 + rfdc: 1.4.1 + secure-json-parse: 4.1.0 + semver: 7.7.4 + toad-cache: 3.7.0 + + fastparallel@2.4.1: + dependencies: + reusify: 1.1.0 + xtend: 4.0.2 + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fastseries@1.7.2: + dependencies: + reusify: 1.1.0 + xtend: 4.0.2 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-my-way@9.4.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.2 + safe-regex2: 5.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + forwarded@0.2.0: {} + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + get-tsconfig@4.13.6: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@13.0.1: + dependencies: + minimatch: 10.1.2 + minipass: 7.1.2 + path-scurry: 2.0.1 + + globals@14.0.0: {} + + has-flag@4.0.0: {} + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + inherits@2.0.4: {} + + ipaddr.js@2.3.0: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + isexe@2.0.0: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-ref-resolver@3.0.0: + dependencies: + dequal: 2.0.3 + + json-schema-resolver@3.0.0: + dependencies: + debug: 4.4.3 + fast-uri: 3.1.0 + rfdc: 1.4.1 + transitivePeerDependencies: + - supports-color + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + light-my-request@6.6.0: + dependencies: + cookie: 1.1.1 + process-warning: 4.0.1 + set-cookie-parser: 2.7.2 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + loupe@3.2.1: {} + + lru-cache@11.2.5: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + media-typer@1.1.0: {} + + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mime@3.0.0: {} + + minimalistic-assert@1.0.1: {} + + minimatch@10.1.2: + dependencies: + '@isaacs/brace-expansion': 5.0.1 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minipass@7.1.2: {} + + mnemonist@0.40.0: + dependencies: + obliterator: 2.0.5 + + mnemonist@0.40.3: + dependencies: + obliterator: 2.0.5 + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + natural-compare@1.4.0: {} + + node-releases@2.0.27: {} + + obliterator@2.0.5: {} + + on-exit-leak-free@2.1.2: {} + + openapi-types@12.1.3: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-scurry@2.0.1: + dependencies: + lru-cache: 11.2.5 + minipass: 7.1.2 + + pathe@1.1.2: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.1.0: {} + + pino@10.3.0: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.0 + thread-stream: 4.0.0 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postgres@3.4.8: {} + + prelude-ls@1.2.1: {} + + process-warning@4.0.1: {} + + process-warning@5.0.0: {} + + punycode@2.3.1: {} + + quick-format-unescaped@4.0.4: {} + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-refresh@0.17.0: {} + + react-router-dom@7.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-router: 7.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + + react-router@7.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + cookie: 1.1.1 + react: 18.3.1 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + real-require@0.2.0: {} + + require-from-string@2.0.2: {} + + resolve-from@4.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + ret@0.5.0: {} + + reusify@1.1.0: {} + + rfdc@1.4.1: {} + + rollup@4.57.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.57.1 + '@rollup/rollup-android-arm64': 4.57.1 + '@rollup/rollup-darwin-arm64': 4.57.1 + '@rollup/rollup-darwin-x64': 4.57.1 + '@rollup/rollup-freebsd-arm64': 4.57.1 + '@rollup/rollup-freebsd-x64': 4.57.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.57.1 + '@rollup/rollup-linux-arm-musleabihf': 4.57.1 + '@rollup/rollup-linux-arm64-gnu': 4.57.1 + '@rollup/rollup-linux-arm64-musl': 4.57.1 + '@rollup/rollup-linux-loong64-gnu': 4.57.1 + '@rollup/rollup-linux-loong64-musl': 4.57.1 + '@rollup/rollup-linux-ppc64-gnu': 4.57.1 + '@rollup/rollup-linux-ppc64-musl': 4.57.1 + '@rollup/rollup-linux-riscv64-gnu': 4.57.1 + '@rollup/rollup-linux-riscv64-musl': 4.57.1 + '@rollup/rollup-linux-s390x-gnu': 4.57.1 + '@rollup/rollup-linux-x64-gnu': 4.57.1 + '@rollup/rollup-linux-x64-musl': 4.57.1 + '@rollup/rollup-openbsd-x64': 4.57.1 + '@rollup/rollup-openharmony-arm64': 4.57.1 + '@rollup/rollup-win32-arm64-msvc': 4.57.1 + '@rollup/rollup-win32-ia32-msvc': 4.57.1 + '@rollup/rollup-win32-x64-gnu': 4.57.1 + '@rollup/rollup-win32-x64-msvc': 4.57.1 + fsevents: 2.3.3 + + safe-buffer@5.2.1: {} + + safe-regex2@5.0.0: + dependencies: + ret: 0.5.0 + + safe-stable-stringify@2.5.0: {} + + safer-buffer@2.1.2: {} + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + secure-json-parse@4.1.0: {} + + semver@6.3.1: {} + + semver@7.7.4: {} + + set-cookie-parser@2.7.2: {} + + setprototypeof@1.2.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + sonic-boom@4.2.0: + dependencies: + atomic-sleep: 1.0.0 + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + split2@4.2.0: {} + + stackback@0.0.2: {} + + statuses@2.0.2: {} + + std-env@3.10.0: {} + + steed@1.1.3: + dependencies: + fastfall: 1.5.1 + fastparallel: 2.4.1 + fastq: 1.20.1 + fastseries: 1.7.2 + reusify: 1.1.0 + + strip-json-comments@3.1.1: {} + + strnum@2.1.2: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + thread-stream@4.0.0: + dependencies: + real-require: 0.2.0 + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinypool@1.1.1: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.2: {} + + toad-cache@3.7.0: {} + + toidentifier@1.0.1: {} + + ts-api-utils@2.4.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + tslib@2.8.1: {} + + tsx@4.21.0: + dependencies: + esbuild: 0.27.3 + get-tsconfig: 4.13.6 + optionalDependencies: + fsevents: 2.3.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + + typescript-eslint@8.54.0(eslint@9.39.2)(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/parser': 8.54.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.54.0(eslint@9.39.2)(typescript@5.9.3) + eslint: 9.39.2 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + vary@1.1.2: {} + + vite-node@2.1.9(@types/node@22.19.10): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.21(@types/node@22.19.10) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite@5.4.21(@types/node@22.19.10): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.57.1 + optionalDependencies: + '@types/node': 22.19.10 + fsevents: 2.3.3 + + vite@6.4.1(@types/node@22.19.10)(tsx@4.21.0)(yaml@2.8.2): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.57.1 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 22.19.10 + fsevents: 2.3.3 + tsx: 4.21.0 + yaml: 2.8.2 + + vitest@2.1.9(@types/node@22.19.10): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.19.10)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 1.1.2 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 5.4.21(@types/node@22.19.10) + vite-node: 2.1.9(@types/node@22.19.10) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.19.10 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + xtend@4.0.2: {} + + yallist@3.1.1: {} + + yaml@2.8.2: {} + + yocto-queue@0.1.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..3ff5faa --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - "apps/*" + - "packages/*"