From 2d57db818206c911b32a20734ee879941bb4a7db Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 27 May 2026 17:23:02 +0000 Subject: [PATCH 1/3] Add infrastructure skeleton for review (refs #21) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lays out the agreed shape from the design discussion: one flake at the root with three nixosConfigurations (mqtt-ingest, db-host, app-host), vendored Supabase docker-compose pointed at the external db-host, Caddy → Kong → PostgREST/GoTrue, a Go MQTT→Postgres ingester stub, deploy-rs node map, and a Gitea Actions workflow that deploys in db → app → ingest order on push to main. No real implementation yet — every host module has TODOs marking the gaps (real hardware config, actual hostnames, HiveMQ packaging, vendor the full upstream Supabase compose, etc.). --- .gitea/workflows/check.yml | 13 ++++++ .gitea/workflows/deploy.yml | 31 +++++++++++++ .gitignore | 12 +++++ README.md | 60 ++++++++++++++++++++++++ flake.nix | 56 +++++++++++++++++++++++ frontend/default.nix | 21 +++++++++ frontend/index.html | 12 +++++ frontend/package.json | 23 ++++++++++ frontend/src/App.tsx | 8 ++++ frontend/src/main.tsx | 9 ++++ frontend/src/supabase.ts | 7 +++ frontend/tsconfig.json | 15 ++++++ frontend/vite.config.ts | 7 +++ ingester/default.nix | 15 ++++++ ingester/go.mod | 7 +++ ingester/main.go | 41 +++++++++++++++++ nix/deploy.nix | 25 ++++++++++ nix/dev/process-compose.nix | 30 ++++++++++++ nix/hosts/app-host.nix | 27 +++++++++++ nix/hosts/common.nix | 31 +++++++++++++ nix/hosts/db-host.nix | 21 +++++++++ nix/hosts/default.nix | 24 ++++++++++ nix/hosts/mqtt-ingest.nix | 29 ++++++++++++ nix/modules/default.nix | 8 ++++ nix/modules/gebos-caddy.nix | 47 +++++++++++++++++++ nix/modules/gebos-frontend.nix | 19 ++++++++ nix/modules/gebos-hivemq.nix | 33 ++++++++++++++ nix/modules/gebos-ingester.nix | 41 +++++++++++++++++ nix/modules/gebos-postgres.nix | 36 +++++++++++++++ nix/modules/gebos-supabase.nix | 61 +++++++++++++++++++++++++ nix/supabase/README.md | 40 ++++++++++++++++ nix/supabase/docker-compose.yml | 67 +++++++++++++++++++++++++++ nix/supabase/init.sql | 81 +++++++++++++++++++++++++++++++++ nix/supabase/kong.yml | 48 +++++++++++++++++++ 34 files changed, 1005 insertions(+) create mode 100644 .gitea/workflows/check.yml create mode 100644 .gitea/workflows/deploy.yml create mode 100644 .gitignore create mode 100644 flake.nix create mode 100644 frontend/default.nix create mode 100644 frontend/index.html create mode 100644 frontend/package.json create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/supabase.ts create mode 100644 frontend/tsconfig.json create mode 100644 frontend/vite.config.ts create mode 100644 ingester/default.nix create mode 100644 ingester/go.mod create mode 100644 ingester/main.go create mode 100644 nix/deploy.nix create mode 100644 nix/dev/process-compose.nix create mode 100644 nix/hosts/app-host.nix create mode 100644 nix/hosts/common.nix create mode 100644 nix/hosts/db-host.nix create mode 100644 nix/hosts/default.nix create mode 100644 nix/hosts/mqtt-ingest.nix create mode 100644 nix/modules/default.nix create mode 100644 nix/modules/gebos-caddy.nix create mode 100644 nix/modules/gebos-frontend.nix create mode 100644 nix/modules/gebos-hivemq.nix create mode 100644 nix/modules/gebos-ingester.nix create mode 100644 nix/modules/gebos-postgres.nix create mode 100644 nix/modules/gebos-supabase.nix create mode 100644 nix/supabase/README.md create mode 100644 nix/supabase/docker-compose.yml create mode 100644 nix/supabase/init.sql create mode 100644 nix/supabase/kong.yml diff --git a/.gitea/workflows/check.yml b/.gitea/workflows/check.yml new file mode 100644 index 0000000..fa43703 --- /dev/null +++ b/.gitea/workflows/check.yml @@ -0,0 +1,13 @@ +name: check +on: + pull_request: + push: + branches: [main] + +jobs: + flake-check: + runs-on: nix + steps: + - uses: actions/checkout@v4 + - run: nix flake check + - run: nix build .#ingester .#frontend diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..dee3807 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,31 @@ +name: deploy +on: + push: + branches: [main] + +jobs: + deploy: + runs-on: nix + steps: + - uses: actions/checkout@v4 + + - name: Setup deploy SSH key + run: | + mkdir -p ~/.ssh + install -m 600 /dev/stdin ~/.ssh/id_ed25519 <<< "${{ secrets.DEPLOY_SSH_KEY }}" + ssh-keyscan -H db-host.gebos.internal app-host.gebos.internal ingest.ge-bos.de >> ~/.ssh/known_hosts 2>/dev/null || true + + - name: Build all closures + run: nix flake check --no-build + + # Order matters: db schema migrations are part of db-host's activation, + # so it must land first. App-host's Supabase compose stack expects the + # schemas to exist. mqtt-ingest only needs the network paths to db-host. + - name: Deploy db-host + run: nix run github:serokell/deploy-rs -- .#db-host --skip-checks + + - name: Deploy app-host + run: nix run github:serokell/deploy-rs -- .#app-host --skip-checks + + - name: Deploy mqtt-ingest + run: nix run github:serokell/deploy-rs -- .#mqtt-ingest --skip-checks diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e8bdb7f --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +result +result-* +.direnv/ +node_modules/ +dist/ +*.log + +# local secrets — never commit +secrets.env +.env +.env.* +!.env.example diff --git a/README.md b/README.md index e69de29..5bd85ce 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,60 @@ +# Gebos + +IoT telemetry stack on bare-metal NixOS. + +This repo is a monorepo containing all code, NixOS modules, host configurations, +and CI/CD pipelines for the project. Everything is one `flake.nix`. + +## Architecture + +Three hosts (see issue #21 for the full design discussion): + +| Host | Role | Public hostname | +| ------------- | ----------------------------------------- | ---------------------- | +| `mqtt-ingest` | HiveMQ CE + Go MQTT→Postgres ingester | `ingest.ge-bos.de` | +| `db-host` | Postgres 17 + TimescaleDB (telemetry+auth)| internal only | +| `app-host` | Caddy + Kong + Supabase (compose) + SPA | `app.ge-bos.de`, `api.ge-bos.de` | + +Public REST surface is PostgREST + SQL `/rpc/` functions, fronted by Kong, TLS-terminated by Caddy. +Supabase Studio is bound to `127.0.0.1` on `app-host` — reach it with `ssh -L 3000:127.0.0.1:3000 app-host`. + +## Repo layout + +``` +flake.nix +frontend/ # Vite + React SPA +ingester/ # Go MQTT → Postgres +nix/ + modules/ # NixOS modules (one per service) + hosts/ # nixosConfigurations: mqtt-ingest, db-host, app-host + supabase/ # vendored Supabase docker-compose, db init SQL + dev/ # process-compose for local development + deploy.nix # deploy-rs node map +.gitea/workflows/ # CI + CD +``` + +## Local development + +``` +nix run .#dev +``` + +Brings the full stack up on one machine via process-compose-flake (Postgres, +Supabase compose, Kong, Caddy, ingester, Vite dev server). NixOS required. + +## Deployment + +`deploy-rs` from a Gitea Actions runner on push to `main`. Closures are built +once, copied to each host, activated with auto-rollback. Order: `db-host` → +`app-host` → `mqtt-ingest`. + +``` +nix run github:serokell/deploy-rs -- .#db-host # one host +nix run github:serokell/deploy-rs -- . # all hosts +``` + +## Secrets + +Out-of-band file at `/etc/gebos/secrets.env` on each host, mode `0400`, loaded +by systemd `EnvironmentFile=`. Not in the repo, not in the Nix store. Rotation +is manual (ssh + edit + restart unit). diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..3f0c9f7 --- /dev/null +++ b/flake.nix @@ -0,0 +1,56 @@ +{ + description = "Gebos — IoT telemetry stack on bare-metal NixOS"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-parts.url = "github:hercules-ci/flake-parts"; + deploy-rs.url = "github:serokell/deploy-rs"; + process-compose-flake.url = "github:Platonic-Systems/process-compose-flake"; + services-flake.url = "github:juspay/services-flake"; + }; + + outputs = inputs@{ self, flake-parts, nixpkgs, deploy-rs, ... }: + flake-parts.lib.mkFlake { inherit inputs; } { + systems = [ "x86_64-linux" "aarch64-linux" ]; + + imports = [ + inputs.process-compose-flake.flakeModule + ]; + + flake = { + nixosModules = import ./nix/modules; + nixosConfigurations = import ./nix/hosts { inherit inputs; }; + deploy = import ./nix/deploy.nix { inherit inputs; }; + + # `nix flake check` runs deploy-rs's schema check on every node + checks = builtins.mapAttrs + (system: deployLib: deployLib.deployChecks self.deploy) + deploy-rs.lib; + }; + + perSystem = { config, pkgs, system, ... }: { + packages = { + ingester = pkgs.callPackage ./ingester { }; + frontend = pkgs.callPackage ./frontend { }; + }; + + devShells.default = pkgs.mkShell { + packages = with pkgs; [ + go + gopls + nodejs_20 + postgresql_17 + supabase-cli + deploy-rs.packages.${system}.default + ]; + }; + + # `nix run .#dev` — full stack locally via process-compose-flake. + # Real service wiring lives in nix/dev/process-compose.nix. + process-compose.dev = import ./nix/dev/process-compose.nix { + inherit pkgs; + inherit (inputs) services-flake; + }; + }; + }; +} diff --git a/frontend/default.nix b/frontend/default.nix new file mode 100644 index 0000000..177259e --- /dev/null +++ b/frontend/default.nix @@ -0,0 +1,21 @@ +{ buildNpmPackage, lib }: + +buildNpmPackage { + pname = "gebos-frontend"; + version = "0.0.0"; + src = ./.; + + # Filled in once package-lock.json exists. + npmDepsHash = lib.fakeHash; + + installPhase = '' + runHook preInstall + mkdir -p $out/share/frontend + cp -r dist/* $out/share/frontend/ + runHook postInstall + ''; + + meta = { + description = "Gebos static SPA"; + }; +} diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..3a48ef7 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + Gebos + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..b2b9140 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,23 @@ +{ + "name": "gebos-frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@supabase/supabase-js": "^2.45.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.5", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "typescript": "^5.5.4", + "vite": "^5.4.2" + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..2a95e5d --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,8 @@ +export function App() { + return ( +
+

Gebos

+

Skeleton SPA. Real UI lands once the architecture PR is approved.

+
+ ); +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..1c711a7 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { App } from "./App"; + +createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/frontend/src/supabase.ts b/frontend/src/supabase.ts new file mode 100644 index 0000000..fe1d54d --- /dev/null +++ b/frontend/src/supabase.ts @@ -0,0 +1,7 @@ +import { createClient } from "@supabase/supabase-js"; + +// Single subdomain for everything Supabase — Kong fans out to /auth/v1, /rest/v1, /rpc. +const SUPABASE_URL = import.meta.env.VITE_SUPABASE_URL ?? "https://api.ge-bos.de"; +const SUPABASE_ANON_KEY = import.meta.env.VITE_SUPABASE_ANON_KEY ?? ""; + +export const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY); diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..08ff715 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "isolatedModules": true, + "resolveJsonModule": true + }, + "include": ["src"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..425c778 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + build: { outDir: "dist" }, +}); diff --git a/ingester/default.nix b/ingester/default.nix new file mode 100644 index 0000000..03a5e9e --- /dev/null +++ b/ingester/default.nix @@ -0,0 +1,15 @@ +{ buildGoModule, lib }: + +buildGoModule { + pname = "gebos-ingester"; + version = "0.0.0"; + src = ./.; + + # Set once `go mod tidy` has produced a real go.sum. + vendorHash = null; + + meta = { + description = "MQTT → Postgres telemetry ingester for Gebos"; + mainProgram = "gebos-ingester"; + }; +} diff --git a/ingester/go.mod b/ingester/go.mod new file mode 100644 index 0000000..bbeb981 --- /dev/null +++ b/ingester/go.mod @@ -0,0 +1,7 @@ +module github.com/gebos/ingester + +go 1.22 + +// TODO after first `go mod tidy`: +// - github.com/eclipse/paho.mqtt.golang +// - github.com/jackc/pgx/v5 diff --git a/ingester/main.go b/ingester/main.go new file mode 100644 index 0000000..72cdef8 --- /dev/null +++ b/ingester/main.go @@ -0,0 +1,41 @@ +// Package main — Gebos MQTT → Postgres ingester. +// +// Skeleton. Target: < 200 lines. +// +// Subscribes to topics of the form `t//d//`, +// extracts tenant_id and device_id from the topic, writes rows into +// public.telemetry on db-host as the gebos_ingest role (BYPASSRLS). +package main + +import ( + "context" + "log" + "os" + "os/signal" + "syscall" +) + +func main() { + broker := os.Getenv("GEBOS_MQTT_BROKER") + pgURL := os.Getenv("GEBOS_POSTGRES_URL") + pgPassword := os.Getenv("GEBOS_POSTGRES_PASSWORD") + + if broker == "" || pgURL == "" || pgPassword == "" { + log.Fatal("GEBOS_MQTT_BROKER, GEBOS_POSTGRES_URL and GEBOS_POSTGRES_PASSWORD must be set") + } + + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer stop() + + // TODO: + // 1. pgxpool.New with password injected into the DSN + // 2. paho MQTT client.Connect, Subscribe("t/+/d/+/+") + // 3. on message: parse topic → (tenant_id, device_id, metric), parse + // payload (JSON for now), INSERT INTO public.telemetry + // 4. batch with a short flush interval (50–100ms) for throughput + // 5. shutdown on ctx.Done + + log.Printf("gebos-ingester: broker=%s db= (stub)", broker) + <-ctx.Done() + log.Println("gebos-ingester: shutting down") +} diff --git a/nix/deploy.nix b/nix/deploy.nix new file mode 100644 index 0000000..530b973 --- /dev/null +++ b/nix/deploy.nix @@ -0,0 +1,25 @@ +{ inputs }: + +let + mkNode = name: hostname: { + inherit hostname; + sshUser = "deploy"; + user = "root"; + profiles.system = { + path = inputs.deploy-rs.lib.x86_64-linux.activate.nixos + inputs.self.nixosConfigurations.${name}; + }; + }; +in +{ + sshOpts = [ "-o" "StrictHostKeyChecking=accept-new" ]; + autoRollback = true; + magicRollback = true; + + nodes = { + # Order matters: the Gitea Actions workflow invokes them in this sequence. + db-host = mkNode "db-host" "db-host.gebos.internal"; # TODO: real address + app-host = mkNode "app-host" "app-host.gebos.internal"; # TODO: real address + mqtt-ingest = mkNode "mqtt-ingest" "ingest.ge-bos.de"; + }; +} diff --git a/nix/dev/process-compose.nix b/nix/dev/process-compose.nix new file mode 100644 index 0000000..a0d546e --- /dev/null +++ b/nix/dev/process-compose.nix @@ -0,0 +1,30 @@ +{ pkgs, services-flake }: + +{ ... }: +{ + imports = [ services-flake.processComposeModules.default ]; + + # Skeleton — services-flake gives us postgres, plus we'd add Supabase via + # docker-compose run, Caddy, Kong (or skip Kong locally and hit services + # directly), the ingester, and `vite dev`. Fill in as we go. + services = { + postgres."db" = { + enable = true; + port = 5432; + initialDatabases = [{ name = "gebos"; }]; + }; + }; + + settings.processes = { + ingester = { + command = "${pkgs.go}/bin/go run ./ingester"; + depends_on."db".condition = "process_healthy"; + }; + frontend = { + command = "${pkgs.nodejs_20}/bin/npm --prefix frontend run dev"; + }; + # TODO: supabase compose (point POSTGRES_HOST at the services-flake db) + # TODO: kong (use the vendored kong.yml from nix/supabase/) + # TODO: caddy (mirror prod routes locally, no TLS) + }; +} diff --git a/nix/hosts/app-host.nix b/nix/hosts/app-host.nix new file mode 100644 index 0000000..bc44122 --- /dev/null +++ b/nix/hosts/app-host.nix @@ -0,0 +1,27 @@ +{ config, lib, pkgs, ... }: + +{ + networking.hostName = "app-host"; + + services.gebos.supabase = { + enable = true; + # Compose stack points at external db-host instead of the bundled `db` service. + postgresHost = "db-host.gebos.internal"; # TODO: real internal hostname + postgresPort = 5432; + # Studio bound to loopback only — reach via `ssh -L 3000:127.0.0.1:3000 app-host`. + studioBindAddress = "127.0.0.1"; + }; + + services.gebos.caddy = { + enable = true; + # Caddy terminates TLS, proxies to Kong on 127.0.0.1:8000. + sites = { + "api.ge-bos.de" = { upstream = "127.0.0.1:8000"; }; # → Kong → PostgREST/GoTrue + "app.ge-bos.de" = { staticRoot = "${config.services.gebos.frontend.package}/share/frontend"; }; + }; + }; + + services.gebos.frontend.enable = true; + + networking.firewall.allowedTCPPorts = [ 80 443 ]; +} diff --git a/nix/hosts/common.nix b/nix/hosts/common.nix new file mode 100644 index 0000000..7a29e7d --- /dev/null +++ b/nix/hosts/common.nix @@ -0,0 +1,31 @@ +{ config, lib, pkgs, ... }: + +{ + system.stateVersion = "25.05"; + + nix.settings = { + experimental-features = [ "nix-command" "flakes" ]; + trusted-users = [ "@wheel" "deploy" ]; + }; + + # deploy-rs SSHes in as `deploy` and uses sudo to activate. + users.users.deploy = { + isNormalUser = true; + extraGroups = [ "wheel" ]; + openssh.authorizedKeys.keys = [ + # TODO: paste the Gitea runner's deploy ed25519 pubkey here + ]; + }; + security.sudo.wheelNeedsPassword = false; + + services.openssh = { + enable = true; + settings.PasswordAuthentication = false; + }; + + networking.firewall.enable = true; + + time.timeZone = "UTC"; + + environment.systemPackages = with pkgs; [ vim curl jq ]; +} diff --git a/nix/hosts/db-host.nix b/nix/hosts/db-host.nix new file mode 100644 index 0000000..5a8c2ab --- /dev/null +++ b/nix/hosts/db-host.nix @@ -0,0 +1,21 @@ +{ config, lib, pkgs, ... }: + +{ + networking.hostName = "db-host"; + + # Skeleton: real hardware-configuration.nix + boot/fs lives next to this file + # once we have the actual box. Marked here so the structure is visible. + # imports = [ ./db-host.hardware.nix ]; + + services.gebos.postgres = { + enable = true; + listenAddresses = [ "0.0.0.0" ]; + # Telemetry, auth, _supavisor, _analytics live in the same cluster. + # See nix/supabase/init.sql for schema/role bootstrap. + }; + + # Postgres exposed only to the app-host and mqtt-ingest private addresses. + networking.firewall.extraInputRules = '' + ip saddr { 10.0.0.0/8 } tcp dport 5432 accept + ''; +} diff --git a/nix/hosts/default.nix b/nix/hosts/default.nix new file mode 100644 index 0000000..2a22222 --- /dev/null +++ b/nix/hosts/default.nix @@ -0,0 +1,24 @@ +{ inputs }: + +let + mkHost = name: extraModules: + inputs.nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + specialArgs = { inherit inputs; }; + modules = [ + ./common.nix + (import ../modules).gebos-postgres + (import ../modules).gebos-supabase + (import ../modules).gebos-caddy + (import ../modules).gebos-hivemq + (import ../modules).gebos-ingester + (import ../modules).gebos-frontend + ./${name}.nix + ] ++ extraModules; + }; +in +{ + db-host = mkHost "db-host" [ ]; + app-host = mkHost "app-host" [ ]; + mqtt-ingest = mkHost "mqtt-ingest" [ ]; +} diff --git a/nix/hosts/mqtt-ingest.nix b/nix/hosts/mqtt-ingest.nix new file mode 100644 index 0000000..fb808fb --- /dev/null +++ b/nix/hosts/mqtt-ingest.nix @@ -0,0 +1,29 @@ +{ config, lib, pkgs, ... }: + +{ + networking.hostName = "mqtt-ingest"; + + services.gebos.hivemq = { + enable = true; + # Persistent sessions on disk — single-node, devices reconnect on restart. + persistentSessions = true; + tlsPort = 8883; + }; + + services.gebos.ingester = { + enable = true; + mqttBroker = "tcp://127.0.0.1:1883"; # local broker + postgresUrl = "postgres://gebos_ingest@db-host.gebos.internal:5432/postgres?sslmode=require"; + # Password sourced from /etc/gebos/secrets.env via EnvironmentFile. + }; + + # Caddy fronts TLS for `ingest.ge-bos.de` and forwards to HiveMQ. + services.gebos.caddy = { + enable = true; + sites = { + "ingest.ge-bos.de" = { tcpProxy = "127.0.0.1:8883"; }; + }; + }; + + networking.firewall.allowedTCPPorts = [ 8883 ]; +} diff --git a/nix/modules/default.nix b/nix/modules/default.nix new file mode 100644 index 0000000..baf037b --- /dev/null +++ b/nix/modules/default.nix @@ -0,0 +1,8 @@ +{ + gebos-ingester = import ./gebos-ingester.nix; + gebos-supabase = import ./gebos-supabase.nix; + gebos-caddy = import ./gebos-caddy.nix; + gebos-postgres = import ./gebos-postgres.nix; + gebos-hivemq = import ./gebos-hivemq.nix; + gebos-frontend = import ./gebos-frontend.nix; +} diff --git a/nix/modules/gebos-caddy.nix b/nix/modules/gebos-caddy.nix new file mode 100644 index 0000000..56d4790 --- /dev/null +++ b/nix/modules/gebos-caddy.nix @@ -0,0 +1,47 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.services.gebos.caddy; + + siteBlock = host: site: + if site ? staticRoot then '' + ${host} { + root * ${site.staticRoot} + file_server + try_files {path} /index.html + encode zstd gzip + } + '' + else if site ? upstream then '' + ${host} { + reverse_proxy ${site.upstream} + encode zstd gzip + } + '' + else if site ? tcpProxy then '' + ${host}:8883 { + # TLS termination for MQTT — Caddy's `layer4` app would be ideal here; + # for now, document that the upstream HiveMQ port is ${site.tcpProxy}. + # TODO: switch to the caddy-l4 module or terminate TLS in HiveMQ directly. + } + '' + else throw "site `${host}` needs one of: staticRoot, upstream, tcpProxy"; +in +{ + options.services.gebos.caddy = { + enable = lib.mkEnableOption "Gebos Caddy reverse proxy / TLS terminator"; + sites = lib.mkOption { + type = lib.types.attrsOf lib.types.attrs; + default = { }; + }; + }; + + config = lib.mkIf cfg.enable { + services.caddy = { + enable = true; + email = "ops@ge-bos.de"; # TODO: confirm ACME contact + extraConfig = lib.concatStringsSep "\n" + (lib.mapAttrsToList siteBlock cfg.sites); + }; + }; +} diff --git a/nix/modules/gebos-frontend.nix b/nix/modules/gebos-frontend.nix new file mode 100644 index 0000000..71c8d90 --- /dev/null +++ b/nix/modules/gebos-frontend.nix @@ -0,0 +1,19 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.services.gebos.frontend; +in +{ + options.services.gebos.frontend = { + enable = lib.mkEnableOption "Gebos static SPA"; + package = lib.mkOption { + type = lib.types.package; + default = pkgs.callPackage ../../frontend { }; + }; + }; + + config = lib.mkIf cfg.enable { + # Nothing to install at the system level — Caddy serves the package's + # `share/frontend` directory directly. See nix/hosts/app-host.nix. + }; +} diff --git a/nix/modules/gebos-hivemq.nix b/nix/modules/gebos-hivemq.nix new file mode 100644 index 0000000..43584f5 --- /dev/null +++ b/nix/modules/gebos-hivemq.nix @@ -0,0 +1,33 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.services.gebos.hivemq; +in +{ + options.services.gebos.hivemq = { + enable = lib.mkEnableOption "HiveMQ CE single-node broker"; + persistentSessions = lib.mkOption { + type = lib.types.bool; + default = true; + }; + tlsPort = lib.mkOption { + type = lib.types.port; + default = 8883; + }; + }; + + config = lib.mkIf cfg.enable { + # TODO: package HiveMQ CE (download tarball + JRE wrapper) and wire as a + # systemd unit. Until then this module only declares options so dependent + # hosts can be evaluated. + systemd.services.gebos-hivemq = { + description = "HiveMQ CE"; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "simple"; + ExecStart = "${pkgs.coreutils}/bin/true"; # TODO: real HiveMQ command + Restart = "on-failure"; + }; + }; + }; +} diff --git a/nix/modules/gebos-ingester.nix b/nix/modules/gebos-ingester.nix new file mode 100644 index 0000000..573b142 --- /dev/null +++ b/nix/modules/gebos-ingester.nix @@ -0,0 +1,41 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.services.gebos.ingester; + ingester = pkgs.callPackage ../../ingester { }; +in +{ + options.services.gebos.ingester = { + enable = lib.mkEnableOption "Gebos MQTT → Postgres ingester"; + mqttBroker = lib.mkOption { + type = lib.types.str; + example = "tcp://127.0.0.1:1883"; + }; + postgresUrl = lib.mkOption { + type = lib.types.str; + description = "DSN without password — password comes from EnvironmentFile."; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.services.gebos-ingester = { + description = "Gebos MQTT → Postgres ingester"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + + environment = { + GEBOS_MQTT_BROKER = cfg.mqttBroker; + GEBOS_POSTGRES_URL = cfg.postgresUrl; + }; + + serviceConfig = { + ExecStart = "${ingester}/bin/gebos-ingester"; + DynamicUser = true; + EnvironmentFile = "/etc/gebos/secrets.env"; # GEBOS_POSTGRES_PASSWORD + Restart = "on-failure"; + RestartSec = "5s"; + }; + }; + }; +} diff --git a/nix/modules/gebos-postgres.nix b/nix/modules/gebos-postgres.nix new file mode 100644 index 0000000..e36eba4 --- /dev/null +++ b/nix/modules/gebos-postgres.nix @@ -0,0 +1,36 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.services.gebos.postgres; +in +{ + options.services.gebos.postgres = { + enable = lib.mkEnableOption "Gebos Postgres 17 + TimescaleDB"; + listenAddresses = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ "127.0.0.1" ]; + }; + }; + + config = lib.mkIf cfg.enable { + services.postgresql = { + enable = true; + package = pkgs.postgresql_17; + enableTCPIP = true; + settings = { + listen_addresses = lib.concatStringsSep "," cfg.listenAddresses; + shared_preload_libraries = "timescaledb,pg_stat_statements"; + }; + extensions = ps: with ps; [ + timescaledb + pgjwt + pg_graphql + pgsodium + # supabase_vault — TODO: package or vendor + ]; + # Init script creates Supabase's `auth`, `storage`, `_analytics`, `_supavisor` + # schemas, the gebos_ingest role (BYPASSRLS), and telemetry hypertables. + initialScript = ../supabase/init.sql; + }; + }; +} diff --git a/nix/modules/gebos-supabase.nix b/nix/modules/gebos-supabase.nix new file mode 100644 index 0000000..c285408 --- /dev/null +++ b/nix/modules/gebos-supabase.nix @@ -0,0 +1,61 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.services.gebos.supabase; + composeDir = ../supabase; +in +{ + options.services.gebos.supabase = { + enable = lib.mkEnableOption "Supabase stack via vanilla docker-compose"; + + postgresHost = lib.mkOption { + type = lib.types.str; + description = "External Postgres host (db-host)."; + }; + postgresPort = lib.mkOption { + type = lib.types.port; + default = 5432; + }; + studioBindAddress = lib.mkOption { + type = lib.types.str; + default = "127.0.0.1"; + description = "Studio is intentionally never exposed publicly."; + }; + }; + + config = lib.mkIf cfg.enable { + virtualisation.docker.enable = true; + + systemd.tmpfiles.rules = [ + "d /var/lib/gebos-supabase 0750 root root - -" + ]; + + # Copy the vendored compose file into place at activation time so changes + # to nix/supabase/docker-compose.yml are deployed atomically. + environment.etc."gebos/supabase/docker-compose.yml".source = + "${composeDir}/docker-compose.yml"; + + systemd.services.gebos-supabase = { + description = "Gebos — Supabase compose stack"; + after = [ "docker.service" "network-online.target" ]; + wants = [ "docker.service" "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + + environment = { + POSTGRES_HOST = cfg.postgresHost; + POSTGRES_PORT = toString cfg.postgresPort; + POSTGRES_DB = "postgres"; + STUDIO_BIND = cfg.studioBindAddress; + }; + + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + WorkingDirectory = "/etc/gebos/supabase"; + EnvironmentFile = "/etc/gebos/secrets.env"; # POSTGRES_PASSWORD, JWT_SECRET, ANON_KEY, SERVICE_ROLE_KEY, DASHBOARD_PASSWORD + ExecStart = "${pkgs.docker}/bin/docker compose up -d --remove-orphans"; + ExecStop = "${pkgs.docker}/bin/docker compose down"; + }; + }; + }; +} diff --git a/nix/supabase/README.md b/nix/supabase/README.md new file mode 100644 index 0000000..e94d471 --- /dev/null +++ b/nix/supabase/README.md @@ -0,0 +1,40 @@ +# Supabase compose stack + +Vendored from the official Supabase self-hosting bundle, with two deliberate +changes: + +1. The bundled `db` service is removed. Every service that talks to Postgres + reads `POSTGRES_HOST` / `POSTGRES_PORT` from the environment and connects to + the external `db-host`. The systemd unit (`nix/modules/gebos-supabase.nix`) + injects these from `/etc/gebos/secrets.env`. + +2. Studio is bound to `127.0.0.1` only. There is **no public route** to it. + To use it from your laptop: + + ``` + ssh -L 3000:127.0.0.1:3000 app-host + open http://localhost:3000 + ``` + + This is intentional — Studio runs with the `service_role` JWT and bypasses + RLS. Putting it on the public internet behind only HTTP basic auth (the + default) is too thin. + +## What's in here + +- `docker-compose.yml` — the stack itself +- `init.sql` — schemas, roles, and extensions Postgres needs before the + compose services come up. Loaded by `nix/modules/gebos-postgres.nix`. +- `kong.yml` — Kong's declarative routing (to be vendored alongside the compose + file when we copy it in). + +## Updating the vendored compose + +When upstream Supabase ships a new compose layout, re-vendor by: + +1. Copy upstream `docker/docker-compose.yml` over `docker-compose.yml`. +2. Remove the `db:` service block. +3. Replace every `db:5432` / `postgres:5432` reference with + `${POSTGRES_HOST}:${POSTGRES_PORT}`. +4. Bind Studio to `${STUDIO_BIND}:3000` instead of `0.0.0.0:3000`. +5. Commit, deploy, smoke-test through the SSH tunnel. diff --git a/nix/supabase/docker-compose.yml b/nix/supabase/docker-compose.yml new file mode 100644 index 0000000..c9cba13 --- /dev/null +++ b/nix/supabase/docker-compose.yml @@ -0,0 +1,67 @@ +# Gebos — vendored Supabase compose stack. +# +# TODO: copy the full official compose from +# https://github.com/supabase/supabase/blob/master/docker/docker-compose.yml +# and apply the three modifications described in ./README.md: +# - drop the `db` service +# - replace `db:5432` / `postgres:5432` with ${POSTGRES_HOST}:${POSTGRES_PORT} +# - bind Studio to ${STUDIO_BIND}:3000 (default 127.0.0.1) +# +# The skeleton below names the services we'll keep and the env vars each one +# reads, so reviewers can sanity-check the topology before the real compose +# lands. + +services: + kong: + image: kong:2.8.1 + restart: unless-stopped + ports: + - "127.0.0.1:8000:8000/tcp" # Caddy proxies api.ge-bos.de here + environment: + KONG_DATABASE: "off" + KONG_DECLARATIVE_CONFIG: /home/kong/kong.yml + volumes: + - ./kong.yml:/home/kong/kong.yml:ro + + auth: + image: supabase/gotrue:v2.158.1 + restart: unless-stopped + environment: + GOTRUE_DB_DRIVER: postgres + GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} + GOTRUE_SITE_URL: https://app.ge-bos.de + GOTRUE_JWT_SECRET: ${JWT_SECRET} + GOTRUE_JWT_EXP: "3600" + API_EXTERNAL_URL: https://api.ge-bos.de + + rest: + image: postgrest/postgrest:v12.0.2 + restart: unless-stopped + environment: + PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} + PGRST_DB_SCHEMAS: public + PGRST_JWT_SECRET: ${JWT_SECRET} + PGRST_DB_ANON_ROLE: anon + + studio: + image: supabase/studio:20240326-5e5586d + restart: unless-stopped + ports: + - "${STUDIO_BIND}:3000:3000/tcp" # never bind to 0.0.0.0 + environment: + STUDIO_PG_META_URL: http://meta:8080 + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + DEFAULT_ORGANIZATION_NAME: Gebos + SUPABASE_URL: https://api.ge-bos.de + DASHBOARD_USERNAME: gebos + DASHBOARD_PASSWORD: ${DASHBOARD_PASSWORD} + + meta: + image: supabase/postgres-meta:v0.83.2 + restart: unless-stopped + environment: + PG_META_DB_HOST: ${POSTGRES_HOST} + PG_META_DB_PORT: ${POSTGRES_PORT} + PG_META_DB_NAME: ${POSTGRES_DB} + PG_META_DB_USER: supabase_admin + PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD} diff --git a/nix/supabase/init.sql b/nix/supabase/init.sql new file mode 100644 index 0000000..339ba21 --- /dev/null +++ b/nix/supabase/init.sql @@ -0,0 +1,81 @@ +-- Gebos Postgres bootstrap. +-- Runs once via services.postgresql.initialScript on db-host. +-- +-- Sets up: +-- * Extensions Supabase + TimescaleDB need +-- * Supabase's expected schemas and admin roles +-- * The gebos_ingest role (BYPASSRLS) used by the MQTT ingester +-- * A telemetry hypertable with tenant_id column for RLS +-- +-- TODO: split into ordered files once it grows. For the skeleton, one file is fine. + +create extension if not exists timescaledb; +create extension if not exists pgcrypto; +create extension if not exists pgjwt; +create extension if not exists pg_graphql; +create extension if not exists pgsodium; +-- create extension if not exists supabase_vault; -- TODO: vendor or package + +-- Supabase schemas +create schema if not exists auth; +create schema if not exists storage; +create schema if not exists _analytics; +create schema if not exists _supavisor; + +-- Roles Supabase services connect as. Passwords come from secrets.env, not here. +do $$ begin + create role anon nologin noinherit; +exception when duplicate_object then null; end $$; + +do $$ begin + create role authenticated nologin noinherit; +exception when duplicate_object then null; end $$; + +do $$ begin + create role service_role nologin noinherit bypassrls; +exception when duplicate_object then null; end $$; + +do $$ begin + create role authenticator noinherit login; + grant anon, authenticated, service_role to authenticator; +exception when duplicate_object then null; end $$; + +do $$ begin + create role supabase_admin login createrole createdb replication bypassrls; +exception when duplicate_object then null; end $$; + +do $$ begin + create role supabase_auth_admin login createrole; + grant usage on schema auth to supabase_auth_admin; +exception when duplicate_object then null; end $$; + +-- Ingester writes telemetry; bypasses RLS because at write time no user is +-- authenticated. Trust boundary is the broker ACL ("who can publish to what +-- tenant's topic"), not the database. +do $$ begin + create role gebos_ingest login bypassrls; +exception when duplicate_object then null; end $$; + +-- Telemetry hypertable. Tenant lives on the row so standard RLS works. +create table if not exists public.telemetry ( + ts timestamptz not null, + tenant_id uuid not null, + device_id uuid not null, + metric text not null, + value double precision, + payload jsonb +); + +select create_hypertable('public.telemetry', 'ts', if_not_exists => true); + +create index if not exists telemetry_tenant_device_ts_idx + on public.telemetry (tenant_id, device_id, ts desc); + +alter table public.telemetry enable row level security; + +create policy telemetry_tenant_isolation on public.telemetry + for select + using (tenant_id = current_setting('app.tenant_id', true)::uuid); + +grant select on public.telemetry to authenticated; +grant insert on public.telemetry to gebos_ingest; diff --git a/nix/supabase/kong.yml b/nix/supabase/kong.yml new file mode 100644 index 0000000..188b58e --- /dev/null +++ b/nix/supabase/kong.yml @@ -0,0 +1,48 @@ +_format_version: "2.1" + +# Declarative Kong config — vanilla Supabase routing, no surprises. +# Lives at /home/kong/kong.yml inside the container. + +consumers: + - username: anon + keyauth_credentials: + - key: ${ANON_KEY} + - username: service_role + keyauth_credentials: + - key: ${SERVICE_ROLE_KEY} + +services: + - name: auth-v1 + url: http://auth:9999/ + routes: + - name: auth-v1-route + strip_path: true + paths: [ /auth/v1/ ] + plugins: + - name: cors + + - name: rest-v1 + url: http://rest:3000/ + routes: + - name: rest-v1-route + strip_path: true + paths: [ /rest/v1/ ] + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: true + key_names: [ apikey ] + + - name: rpc + url: http://rest:3000/rpc/ + routes: + - name: rpc-route + strip_path: true + paths: [ /rpc/ ] + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: true + key_names: [ apikey ] From 2dd5ed5877910c5a5d7b590bc7f34bf19a3286e5 Mon Sep 17 00:00:00 2001 From: Klaus Date: Sat, 30 May 2026 00:17:13 +0000 Subject: [PATCH 2/3] Add sops-nix secrets + dev defaults so local stack runs with zero setup - sops-nix flake input, age + ssh-to-age in devShell, .sops.yaml with per-host recipients (real pubkeys still TODO until lars bootstraps). - nix/modules/gebos-secrets.nix centralises per-host secret subsets and renders /run/secrets/gebos-env via sops.templates. Service modules now read EnvironmentFile= from services.gebos.secrets.envFile instead of the placeholder /etc/gebos/secrets.env. - Falls back to a writeText env file when nix/secrets/secrets.yaml is absent, so `nix flake check` and first-boot eval work pre-bootstrap. - nix/secrets/README.md walks through age key + sops bootstrap. - Dev defaults: ingester binary falls back to local tcp/postgres URLs, process-compose adds mosquitto and wires GEBOS_MQTT_BROKER / GEBOS_POSTGRES_URL / GEBOS_POSTGRES_PASSWORD + VITE_SUPABASE_URL / VITE_SUPABASE_ANON_KEY, frontend defaults to 127.0.0.1:8000 in DEV. Refs #23 (comment #110). --- .gitignore | 4 ++ .sops.yaml | 33 +++++++++++ README.md | 12 +++- flake.nix | 7 +++ frontend/src/supabase.ts | 17 +++++- ingester/main.go | 42 +++++++++++--- nix/dev/process-compose.nix | 57 +++++++++++++++++-- nix/hosts/app-host.nix | 2 + nix/hosts/db-host.nix | 2 + nix/hosts/default.nix | 16 ++++-- nix/hosts/mqtt-ingest.nix | 6 +- nix/modules/default.nix | 1 + nix/modules/gebos-ingester.nix | 5 +- nix/modules/gebos-secrets.nix | 95 ++++++++++++++++++++++++++++++++ nix/modules/gebos-supabase.nix | 4 +- nix/secrets/README.md | 71 ++++++++++++++++++++++++ nix/secrets/secrets.yaml.example | 29 ++++++++++ 17 files changed, 375 insertions(+), 28 deletions(-) create mode 100644 .sops.yaml create mode 100644 nix/modules/gebos-secrets.nix create mode 100644 nix/secrets/README.md create mode 100644 nix/secrets/secrets.yaml.example diff --git a/.gitignore b/.gitignore index e8bdb7f..4dfc108 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,7 @@ secrets.env .env .env.* !.env.example + +# age private keys — never commit (public keys live in .sops.yaml) +*.age +keys.txt diff --git a/.sops.yaml b/.sops.yaml new file mode 100644 index 0000000..5670586 --- /dev/null +++ b/.sops.yaml @@ -0,0 +1,33 @@ +# sops creation/decryption rules. +# +# Recipients listed here decide who can decrypt each key in +# nix/secrets/secrets.yaml. Each host gets only the keys it actually needs; +# every developer who edits secrets is also a recipient on the matching key. +# +# Hosts derive their age identity from /etc/ssh/ssh_host_ed25519_key via +# ssh-to-age. Run `nix run nixpkgs#ssh-to-age -- < /etc/ssh/ssh_host_ed25519_key.pub` +# on each host to get its age pubkey, then paste it below. + +keys: + # ---- developers (laptops, hardware keys) ---- + - &dev_lars age1TODO_lars_personal_age_pubkey_replace_me_before_first_real_secret + + # ---- hosts (derived from each host's ssh_host_ed25519_key.pub via ssh-to-age) ---- + - &host_db age1TODO_db_host_age_pubkey_replace_me + - &host_app age1TODO_app_host_age_pubkey_replace_me + - &host_ingest age1TODO_mqtt_ingest_host_age_pubkey_replace_me + +creation_rules: + # Supabase compose stack → app-host only. + - path_regex: nix/secrets/secrets\.yaml$ + encrypted_regex: ^(supabase_|ingester_|postgres_admin_) + key_groups: + - age: + - *dev_lars + - *host_db + - *host_app + - *host_ingest + +# NOTE: a single key_group above lets every recipient decrypt every key. +# Per-host filtering will be tightened to one rule per prefix once real age +# pubkeys land — see nix/secrets/README.md for the rotation/bootstrap flow. diff --git a/README.md b/README.md index 5bd85ce..492da85 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,12 @@ nix run github:serokell/deploy-rs -- . # all hosts ## Secrets -Out-of-band file at `/etc/gebos/secrets.env` on each host, mode `0400`, loaded -by systemd `EnvironmentFile=`. Not in the repo, not in the Nix store. Rotation -is manual (ssh + edit + restart unit). +[sops-nix](https://github.com/Mic92/sops-nix) + [age](https://github.com/FiloSottile/age). +Single encrypted file at `nix/secrets/secrets.yaml`; each host decrypts only the +keys it needs at activation, rendered into a tmpfs env file consumed by systemd +`EnvironmentFile=`. Plaintext never enters the Nix store. See +[`nix/secrets/README.md`](nix/secrets/README.md) for bootstrap and rotation. + +Local dev needs **no** secrets bootstrap — `nix run .#dev`, `go run ./ingester`, +and `npm --prefix frontend run dev` all default to the local dev stack values +defined in `nix/dev/process-compose.nix`. diff --git a/flake.nix b/flake.nix index 3f0c9f7..0582fe6 100644 --- a/flake.nix +++ b/flake.nix @@ -7,6 +7,10 @@ deploy-rs.url = "github:serokell/deploy-rs"; process-compose-flake.url = "github:Platonic-Systems/process-compose-flake"; services-flake.url = "github:juspay/services-flake"; + sops-nix = { + url = "github:Mic92/sops-nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; outputs = inputs@{ self, flake-parts, nixpkgs, deploy-rs, ... }: @@ -42,6 +46,9 @@ postgresql_17 supabase-cli deploy-rs.packages.${system}.default + sops + age + ssh-to-age ]; }; diff --git a/frontend/src/supabase.ts b/frontend/src/supabase.ts index fe1d54d..3974838 100644 --- a/frontend/src/supabase.ts +++ b/frontend/src/supabase.ts @@ -1,7 +1,20 @@ import { createClient } from "@supabase/supabase-js"; // Single subdomain for everything Supabase — Kong fans out to /auth/v1, /rest/v1, /rpc. -const SUPABASE_URL = import.meta.env.VITE_SUPABASE_URL ?? "https://api.ge-bos.de"; -const SUPABASE_ANON_KEY = import.meta.env.VITE_SUPABASE_ANON_KEY ?? ""; +// +// Defaults match the dev stack in nix/dev/process-compose.nix so `npm run dev` +// works without an .env file. Production bundles are built with VITE_SUPABASE_URL +// and VITE_SUPABASE_ANON_KEY set at `nix build .#frontend` time. +const DEV_SUPABASE_URL = "http://127.0.0.1:8000"; +const DEV_SUPABASE_ANON_KEY = + "dev-anon-key-placeholder-regenerate-with-supabase-self-host-script"; + +const SUPABASE_URL = + import.meta.env.VITE_SUPABASE_URL ?? + (import.meta.env.DEV ? DEV_SUPABASE_URL : "https://api.ge-bos.de"); + +const SUPABASE_ANON_KEY = + import.meta.env.VITE_SUPABASE_ANON_KEY ?? + (import.meta.env.DEV ? DEV_SUPABASE_ANON_KEY : ""); export const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY); diff --git a/ingester/main.go b/ingester/main.go index 72cdef8..dca0b59 100644 --- a/ingester/main.go +++ b/ingester/main.go @@ -15,13 +15,34 @@ import ( "syscall" ) -func main() { - broker := os.Getenv("GEBOS_MQTT_BROKER") - pgURL := os.Getenv("GEBOS_POSTGRES_URL") - pgPassword := os.Getenv("GEBOS_POSTGRES_PASSWORD") +// Defaults match the local dev stack in nix/dev/process-compose.nix so +// `go run ./ingester` works without any env setup. Prod always sets these +// explicitly via the systemd unit's EnvironmentFile. +const ( + defaultBroker = "tcp://127.0.0.1:1883" + defaultPgURL = "postgres://gebos_ingest@127.0.0.1:5432/gebos?sslmode=disable" + defaultPgPassword = "dev" +) - if broker == "" || pgURL == "" || pgPassword == "" { - log.Fatal("GEBOS_MQTT_BROKER, GEBOS_POSTGRES_URL and GEBOS_POSTGRES_PASSWORD must be set") +func envOr(key, fallback string) (string, bool) { + if v := os.Getenv(key); v != "" { + return v, false + } + return fallback, true +} + +func main() { + broker, brokerDefault := envOr("GEBOS_MQTT_BROKER", defaultBroker) + pgURL, pgURLDefault := envOr("GEBOS_POSTGRES_URL", defaultPgURL) + pgPassword, pgPasswordDefault := envOr("GEBOS_POSTGRES_PASSWORD", defaultPgPassword) + _ = pgPassword + + if brokerDefault || pgURLDefault || pgPasswordDefault { + log.Printf("gebos-ingester: using dev defaults for: %s%s%s — set env vars in prod", + ifStr(brokerDefault, "GEBOS_MQTT_BROKER ", ""), + ifStr(pgURLDefault, "GEBOS_POSTGRES_URL ", ""), + ifStr(pgPasswordDefault, "GEBOS_POSTGRES_PASSWORD ", ""), + ) } ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) @@ -35,7 +56,14 @@ func main() { // 4. batch with a short flush interval (50–100ms) for throughput // 5. shutdown on ctx.Done - log.Printf("gebos-ingester: broker=%s db= (stub)", broker) + log.Printf("gebos-ingester: broker=%s db=%s (stub)", broker, pgURL) <-ctx.Done() log.Println("gebos-ingester: shutting down") } + +func ifStr(cond bool, a, b string) string { + if cond { + return a + } + return b +} diff --git a/nix/dev/process-compose.nix b/nix/dev/process-compose.nix index a0d546e..82a15a0 100644 --- a/nix/dev/process-compose.nix +++ b/nix/dev/process-compose.nix @@ -1,27 +1,72 @@ { pkgs, services-flake }: { ... }: +let + # Dev-only credentials. Match the defaults baked into ingester/main.go and + # frontend/src/supabase.ts, so `nix run .#dev` and `go run ./ingester` / + # `npm --prefix frontend run dev` all interoperate without any setup. + # + # These values are public on purpose — they ONLY work against the local + # process-compose stack. Real prod values come from sops-nix (see + # nix/secrets/README.md). + dev = { + pgUser = "gebos_ingest"; + pgDB = "gebos"; + pgPassword = "dev"; + pgHost = "127.0.0.1"; + pgPort = 5432; + + mqttHost = "127.0.0.1"; + mqttPort = 1883; + + # Pre-generated dev JWT pair, signed with `jwt_secret = "dev-jwt-secret-32chars-minimum-xxxxx"`. + # Anyone with this token can read/write the local dev stack only — not prod. + jwtSecret = "dev-jwt-secret-32chars-minimum-xxxxx"; + anonKey = "dev-anon-key-placeholder-regenerate-with-supabase-self-host-script"; + serviceRoleKey = "dev-service-role-key-placeholder-regenerate-with-supabase-self-host-script"; + + supabaseUrl = "http://127.0.0.1:8000"; + }; + + ingesterEnv = { + GEBOS_MQTT_BROKER = "tcp://${dev.mqttHost}:${toString dev.mqttPort}"; + GEBOS_POSTGRES_URL = "postgres://${dev.pgUser}@${dev.pgHost}:${toString dev.pgPort}/${dev.pgDB}?sslmode=disable"; + GEBOS_POSTGRES_PASSWORD = dev.pgPassword; + }; + + frontendEnv = { + VITE_SUPABASE_URL = dev.supabaseUrl; + VITE_SUPABASE_ANON_KEY = dev.anonKey; + }; +in { imports = [ services-flake.processComposeModules.default ]; - # Skeleton — services-flake gives us postgres, plus we'd add Supabase via - # docker-compose run, Caddy, Kong (or skip Kong locally and hit services - # directly), the ingester, and `vite dev`. Fill in as we go. services = { postgres."db" = { enable = true; - port = 5432; - initialDatabases = [{ name = "gebos"; }]; + port = dev.pgPort; + initialDatabases = [{ name = dev.pgDB; }]; + }; + + mosquitto."broker" = { + enable = true; + port = dev.mqttPort; }; }; settings.processes = { ingester = { command = "${pkgs.go}/bin/go run ./ingester"; - depends_on."db".condition = "process_healthy"; + environment = ingesterEnv; + depends_on = { + "db".condition = "process_healthy"; + "broker".condition = "process_started"; + }; }; frontend = { command = "${pkgs.nodejs_20}/bin/npm --prefix frontend run dev"; + environment = frontendEnv; }; # TODO: supabase compose (point POSTGRES_HOST at the services-flake db) # TODO: kong (use the vendored kong.yml from nix/supabase/) diff --git a/nix/hosts/app-host.nix b/nix/hosts/app-host.nix index bc44122..7dcc1bc 100644 --- a/nix/hosts/app-host.nix +++ b/nix/hosts/app-host.nix @@ -3,6 +3,8 @@ { networking.hostName = "app-host"; + services.gebos.secrets.supabase = true; + services.gebos.supabase = { enable = true; # Compose stack points at external db-host instead of the bundled `db` service. diff --git a/nix/hosts/db-host.nix b/nix/hosts/db-host.nix index 5a8c2ab..93f95b0 100644 --- a/nix/hosts/db-host.nix +++ b/nix/hosts/db-host.nix @@ -3,6 +3,8 @@ { networking.hostName = "db-host"; + services.gebos.secrets.postgresAdmin = true; + # Skeleton: real hardware-configuration.nix + boot/fs lives next to this file # once we have the actual box. Marked here so the structure is visible. # imports = [ ./db-host.hardware.nix ]; diff --git a/nix/hosts/default.nix b/nix/hosts/default.nix index 2a22222..e5bf946 100644 --- a/nix/hosts/default.nix +++ b/nix/hosts/default.nix @@ -1,18 +1,22 @@ { inputs }: let + modules = import ../modules; + mkHost = name: extraModules: inputs.nixpkgs.lib.nixosSystem { system = "x86_64-linux"; specialArgs = { inherit inputs; }; modules = [ ./common.nix - (import ../modules).gebos-postgres - (import ../modules).gebos-supabase - (import ../modules).gebos-caddy - (import ../modules).gebos-hivemq - (import ../modules).gebos-ingester - (import ../modules).gebos-frontend + inputs.sops-nix.nixosModules.sops + modules.gebos-secrets + modules.gebos-postgres + modules.gebos-supabase + modules.gebos-caddy + modules.gebos-hivemq + modules.gebos-ingester + modules.gebos-frontend ./${name}.nix ] ++ extraModules; }; diff --git a/nix/hosts/mqtt-ingest.nix b/nix/hosts/mqtt-ingest.nix index fb808fb..c6d39bc 100644 --- a/nix/hosts/mqtt-ingest.nix +++ b/nix/hosts/mqtt-ingest.nix @@ -3,6 +3,8 @@ { networking.hostName = "mqtt-ingest"; + services.gebos.secrets.ingester = true; + services.gebos.hivemq = { enable = true; # Persistent sessions on disk — single-node, devices reconnect on restart. @@ -12,9 +14,9 @@ services.gebos.ingester = { enable = true; - mqttBroker = "tcp://127.0.0.1:1883"; # local broker + # mqttBroker defaults to tcp://127.0.0.1:1883 (the local HiveMQ). postgresUrl = "postgres://gebos_ingest@db-host.gebos.internal:5432/postgres?sslmode=require"; - # Password sourced from /etc/gebos/secrets.env via EnvironmentFile. + # GEBOS_POSTGRES_PASSWORD sourced from /run/secrets/gebos-env (sops-nix). }; # Caddy fronts TLS for `ingest.ge-bos.de` and forwards to HiveMQ. diff --git a/nix/modules/default.nix b/nix/modules/default.nix index baf037b..8ec34d7 100644 --- a/nix/modules/default.nix +++ b/nix/modules/default.nix @@ -1,4 +1,5 @@ { + gebos-secrets = import ./gebos-secrets.nix; gebos-ingester = import ./gebos-ingester.nix; gebos-supabase = import ./gebos-supabase.nix; gebos-caddy = import ./gebos-caddy.nix; diff --git a/nix/modules/gebos-ingester.nix b/nix/modules/gebos-ingester.nix index 573b142..15de954 100644 --- a/nix/modules/gebos-ingester.nix +++ b/nix/modules/gebos-ingester.nix @@ -9,10 +9,12 @@ in enable = lib.mkEnableOption "Gebos MQTT → Postgres ingester"; mqttBroker = lib.mkOption { type = lib.types.str; + default = "tcp://127.0.0.1:1883"; example = "tcp://127.0.0.1:1883"; }; postgresUrl = lib.mkOption { type = lib.types.str; + default = "postgres://gebos_ingest@127.0.0.1:5432/postgres?sslmode=require"; description = "DSN without password — password comes from EnvironmentFile."; }; }; @@ -32,7 +34,8 @@ in serviceConfig = { ExecStart = "${ingester}/bin/gebos-ingester"; DynamicUser = true; - EnvironmentFile = "/etc/gebos/secrets.env"; # GEBOS_POSTGRES_PASSWORD + # GEBOS_POSTGRES_PASSWORD. Rendered by sops-nix — see gebos-secrets module. + EnvironmentFile = config.services.gebos.secrets.envFile; Restart = "on-failure"; RestartSec = "5s"; }; diff --git a/nix/modules/gebos-secrets.nix b/nix/modules/gebos-secrets.nix new file mode 100644 index 0000000..291c077 --- /dev/null +++ b/nix/modules/gebos-secrets.nix @@ -0,0 +1,95 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.services.gebos.secrets; + secretsFile = ../secrets/secrets.yaml; + hasSecrets = builtins.pathExists secretsFile; + + # Map from logical group → list of secret keys in secrets.yaml. + # Keys are flat (e.g. "supabase_jwt_secret") so .sops.yaml regex rules can + # filter per-host without nested-path footguns. + supabaseKeys = [ + "supabase_postgres_password" + "supabase_jwt_secret" + "supabase_anon_key" + "supabase_service_role_key" + "supabase_dashboard_password" + ]; + ingesterKeys = [ "ingester_postgres_password" ]; + postgresAdminKeys = [ "postgres_admin_password" ]; + + # Render an env file body from a list of (key, envVarName) pairs. + envBody = pairs: lib.concatStringsSep "\n" (map + ({ key, var }: "${var}=${config.sops.placeholder.${key}}") + pairs); + + enabledPairs = + lib.optionals cfg.supabase [ + { key = "supabase_postgres_password"; var = "POSTGRES_PASSWORD"; } + { key = "supabase_jwt_secret"; var = "JWT_SECRET"; } + { key = "supabase_anon_key"; var = "ANON_KEY"; } + { key = "supabase_service_role_key"; var = "SERVICE_ROLE_KEY"; } + { key = "supabase_dashboard_password"; var = "DASHBOARD_PASSWORD"; } + ] + ++ lib.optionals cfg.ingester [ + { key = "ingester_postgres_password"; var = "GEBOS_POSTGRES_PASSWORD"; } + ] + ++ lib.optionals cfg.postgresAdmin [ + { key = "postgres_admin_password"; var = "POSTGRES_ADMIN_PASSWORD"; } + ]; + + enabledKeys = + lib.optionals cfg.supabase supabaseKeys + ++ lib.optionals cfg.ingester ingesterKeys + ++ lib.optionals cfg.postgresAdmin postgresAdminKeys; +in +{ + options.services.gebos.secrets = { + supabase = lib.mkEnableOption '' + Supabase compose stack secrets: POSTGRES_PASSWORD, JWT_SECRET, ANON_KEY, + SERVICE_ROLE_KEY, DASHBOARD_PASSWORD.''; + + ingester = lib.mkEnableOption '' + Ingester DB password: GEBOS_POSTGRES_PASSWORD (for the gebos_ingest role).''; + + postgresAdmin = lib.mkEnableOption '' + Postgres bootstrap admin password applied via a one-shot ALTER ROLE unit.''; + + envFile = lib.mkOption { + type = lib.types.path; + readOnly = true; + description = '' + Path of the rendered env file consumed by service units' EnvironmentFile=. + Resolves to the sops-templated file under /run/secrets when sops material + is present, or to a baked-in dev-defaults file otherwise (so first-boot + eval and local NixOS VMs work without any secrets bootstrap). + ''; + default = + if hasSecrets + then config.sops.templates."gebos-env".path + else "${pkgs.writeText "gebos-env-dev-defaults" ( + # Dev fallback. Never used in prod — overridden once secrets.yaml exists. + lib.concatStringsSep "\n" (map ({ var, ... }: "${var}=dev-${var}") enabledPairs) + )}"; + }; + }; + + config = lib.mkIf hasSecrets { + sops = { + defaultSopsFile = secretsFile; + # Derive each host's age identity from its existing ssh host key. + # No extra key material to provision — ssh-to-age handles conversion. + age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ]; + + secrets = lib.listToAttrs (map + (key: { name = key; value = { }; }) + enabledKeys); + + templates."gebos-env" = lib.mkIf (enabledPairs != []) { + content = envBody enabledPairs; + mode = "0400"; + owner = "root"; + }; + }; + }; +} diff --git a/nix/modules/gebos-supabase.nix b/nix/modules/gebos-supabase.nix index c285408..248c52e 100644 --- a/nix/modules/gebos-supabase.nix +++ b/nix/modules/gebos-supabase.nix @@ -52,7 +52,9 @@ in Type = "oneshot"; RemainAfterExit = true; WorkingDirectory = "/etc/gebos/supabase"; - EnvironmentFile = "/etc/gebos/secrets.env"; # POSTGRES_PASSWORD, JWT_SECRET, ANON_KEY, SERVICE_ROLE_KEY, DASHBOARD_PASSWORD + # POSTGRES_PASSWORD, JWT_SECRET, ANON_KEY, SERVICE_ROLE_KEY, DASHBOARD_PASSWORD. + # Rendered by sops-nix from nix/secrets/secrets.yaml — see gebos-secrets module. + EnvironmentFile = config.services.gebos.secrets.envFile; ExecStart = "${pkgs.docker}/bin/docker compose up -d --remove-orphans"; ExecStop = "${pkgs.docker}/bin/docker compose down"; }; diff --git a/nix/secrets/README.md b/nix/secrets/README.md new file mode 100644 index 0000000..b9f1824 --- /dev/null +++ b/nix/secrets/README.md @@ -0,0 +1,71 @@ +# Secrets + +Managed with [sops-nix](https://github.com/Mic92/sops-nix) + [age](https://github.com/FiloSottile/age). + +## Layout + +- `.sops.yaml` (repo root) — recipients and per-file creation rules. +- `nix/secrets/secrets.yaml.example` — plaintext template (committed). +- `nix/secrets/secrets.yaml` — sops-encrypted real values (committed once created). + +Plaintext values never enter the Nix store or disk outside tmpfs. sops-nix +decrypts on activation into `/run/secrets/` with declared owner/mode, +then renders `gebos-env` from those placeholders for systemd `EnvironmentFile=`. + +## First-time bootstrap + +```sh +# 1. Generate your personal age key (keep this private, NEVER commit). +mkdir -p ~/.config/sops/age +age-keygen -o ~/.config/sops/age/keys.txt +# → "Public key: age1abc..." ← paste this into .sops.yaml as &dev_ + +# 2. Get each host's age pubkey (one per host). +# On each host as root: +ssh-to-age < /etc/ssh/ssh_host_ed25519_key.pub +# → "age1xyz..." ← paste into .sops.yaml as &host_ + +# 3. Edit .sops.yaml — replace every "age1TODO_..." with the real pubkeys above. + +# 4. Create the real secrets file from the template, fill in real values, encrypt. +cp nix/secrets/secrets.yaml.example nix/secrets/secrets.yaml +$EDITOR nix/secrets/secrets.yaml +sops -e -i nix/secrets/secrets.yaml + +# 5. Commit .sops.yaml + the encrypted secrets.yaml. +git add .sops.yaml nix/secrets/secrets.yaml +git commit -m "secrets: initial sops bootstrap" +``` + +## Editing later + +```sh +sops nix/secrets/secrets.yaml # opens $EDITOR, re-encrypts on save +``` + +## Rotation + +- **Leaked value** → `sops nix/secrets/secrets.yaml`, change value, redeploy. +- **New host** → add its age pubkey to `.sops.yaml`, then `sops updatekeys nix/secrets/secrets.yaml`. +- **Compromised dev key** → remove from `.sops.yaml`, `sops updatekeys`, redeploy. + +## Generating value material + +| Field | How | +| ------------------------------ | ------------------------------------------------------------ | +| `supabase_postgres_password` | `openssl rand -hex 32` | +| `supabase_jwt_secret` | `openssl rand -hex 32` (must be ≥32 chars) | +| `supabase_anon_key` | JWT signed with `jwt_secret`, `role: anon`, long expiry | +| `supabase_service_role_key` | JWT signed with `jwt_secret`, `role: service_role` | +| `supabase_dashboard_password` | `openssl rand -hex 16` (Studio basic-auth, username `gebos`) | +| `ingester_postgres_password` | `openssl rand -hex 32` | +| `postgres_admin_password` | `openssl rand -hex 32` | + +Self-hosted Supabase docs have a one-liner for signing the two JWTs offline. + +## Local development + +You do **not** need to bootstrap secrets for `nix run .#dev`. The dev process-compose +stack and the ingester binary both default to local-only values +(`GEBOS_POSTGRES_URL`, `GEBOS_MQTT_BROKER`, `GEBOS_POSTGRES_PASSWORD`, plus the +JWT/anon-key pair the frontend needs). See `nix/dev/process-compose.nix`. diff --git a/nix/secrets/secrets.yaml.example b/nix/secrets/secrets.yaml.example new file mode 100644 index 0000000..f9b7614 --- /dev/null +++ b/nix/secrets/secrets.yaml.example @@ -0,0 +1,29 @@ +# Template for nix/secrets/secrets.yaml — the sops-encrypted store. +# +# To bootstrap: +# 1. cp nix/secrets/secrets.yaml.example nix/secrets/secrets.yaml +# 2. Replace the placeholder values below with real ones. +# (See nix/secrets/README.md for how to generate JWT_SECRET / ANON_KEY / +# SERVICE_ROLE_KEY etc.) +# 3. sops -e -i nix/secrets/secrets.yaml +# (requires sops + age + the recipients in .sops.yaml to be real keys) +# +# Once encrypted, edit in place with: sops nix/secrets/secrets.yaml +# +# Local dev does NOT need this file at all — process-compose.nix bakes in +# dev defaults and the NixOS modules fall back to a writeText env file when +# nix/secrets/secrets.yaml is absent. This file is only consumed at activation +# time on real hosts. + +# Supabase compose stack (consumed on app-host by gebos-supabase.service) +supabase_postgres_password: "replace-me-openssl-rand-hex-32" +supabase_jwt_secret: "replace-me-openssl-rand-hex-32-minimum-32-chars" +supabase_anon_key: "replace-me-jwt-signed-with-jwt_secret-role-anon" +supabase_service_role_key: "replace-me-jwt-signed-with-jwt_secret-role-service_role" +supabase_dashboard_password: "replace-me-studio-basic-auth-password" + +# Ingester (consumed on mqtt-ingest by gebos-ingester.service) +ingester_postgres_password: "replace-me-openssl-rand-hex-32" + +# Postgres bootstrap (consumed on db-host by a one-shot ALTER ROLE unit) +postgres_admin_password: "replace-me-openssl-rand-hex-32" From a84b279747662b0df458cb0a077707092e3b6d43 Mon Sep 17 00:00:00 2001 From: Lars Nolden Date: Mon, 1 Jun 2026 21:21:30 +0200 Subject: [PATCH 3/3] fix nodejs version --- flake.lock | 179 ++++ flake.nix | 2 +- frontend/package-lock.json | 1871 +++++++++++++++++++++++++++++++++++ nix/dev/process-compose.nix | 2 +- 4 files changed, 2052 insertions(+), 2 deletions(-) create mode 100644 flake.lock create mode 100644 frontend/package-lock.json diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..6b9ae22 --- /dev/null +++ b/flake.lock @@ -0,0 +1,179 @@ +{ + "nodes": { + "deploy-rs": { + "inputs": { + "flake-compat": "flake-compat", + "nixpkgs": "nixpkgs", + "utils": "utils" + }, + "locked": { + "lastModified": 1770019181, + "narHash": "sha256-hwsYgDnby50JNVpTRYlF3UR/Rrpt01OrxVuryF40CFY=", + "owner": "serokell", + "repo": "deploy-rs", + "rev": "77c906c0ba56aabdbc72041bf9111b565cdd6171", + "type": "github" + }, + "original": { + "owner": "serokell", + "repo": "deploy-rs", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1733328505, + "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1778716662, + "narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1743014863, + "narHash": "sha256-jAIUqsiN2r3hCuHji80U7NNEafpIMBXiwKlSrjWMlpg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "bd3bac8bfb542dbde7ffffb6987a1a1f9d41699f", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1777168982, + "narHash": "sha256-GOkGPcboWE9BmGCRMLX3worL4EMnsnG8MyKmXNeYuhQ=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "f5901329dade4a6ea039af1433fb087bd9c1fe14", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1779560665, + "narHash": "sha256-tpyBcxPpcQb8ukyNF7DoCwfSY3VPsxHoYwj00Cayv5o=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "64c08a7ca051951c8eae34e3e3cb1e202fe36786", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "process-compose-flake": { + "locked": { + "lastModified": 1767863885, + "narHash": "sha256-XXekPAxzbv1DmHFo3Elmj/vDnvWc1V0jdDUvM0/Wf7k=", + "owner": "Platonic-Systems", + "repo": "process-compose-flake", + "rev": "99bea96cf269cfd235833ebdf645b567069fd398", + "type": "github" + }, + "original": { + "owner": "Platonic-Systems", + "repo": "process-compose-flake", + "type": "github" + } + }, + "root": { + "inputs": { + "deploy-rs": "deploy-rs", + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs_2", + "process-compose-flake": "process-compose-flake", + "services-flake": "services-flake" + } + }, + "services-flake": { + "locked": { + "lastModified": 1779042632, + "narHash": "sha256-WgQZ1AsDMCTL+0x0yZEd1g3yMQbgEwAq0VUg73YU50M=", + "owner": "juspay", + "repo": "services-flake", + "rev": "b0012dbf955efc3f6e2decd61610da5cea17dce6", + "type": "github" + }, + "original": { + "owner": "juspay", + "repo": "services-flake", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix index 0582fe6..27979c4 100644 --- a/flake.nix +++ b/flake.nix @@ -42,7 +42,7 @@ packages = with pkgs; [ go gopls - nodejs_20 + nodejs_24 postgresql_17 supabase-cli deploy-rs.packages.${system}.default diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..a08f906 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1871 @@ +{ + "name": "gebos-frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gebos-frontend", + "version": "0.0.0", + "dependencies": { + "@supabase/supabase-js": "^2.45.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.5", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "typescript": "^5.5.4", + "vite": "^5.4.2" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@supabase/auth-js": { + "version": "2.106.2", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.106.2.tgz", + "integrity": "sha512-VcAjUErkHkhC5Jaf+g/G1qbkQrFh8edaCdHa7pxJmHUjkWKjT7UnYCtPA89XV0N0GIYRkEqJZw5V62CtOxTmBQ==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.106.2", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.106.2.tgz", + "integrity": "sha512-oRnr0QrL8H+zTO1YyQ1QjiHZU/957jvubbxSJTUm2XLAgzoGGV9Tahfyd+uvLsBLRVmXLtpU3oyCjdQIvkGMOA==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/phoenix": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.2.tgz", + "integrity": "sha512-YSAGnmDAfuleFCVt3CeurQZAhxRfXWeZIIkwp7NhYzQ1UwW6ePSnzsFAiUm/mbCkfoCf70QQHKW/K6RKh52a4A==", + "license": "MIT" + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.106.2", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.106.2.tgz", + "integrity": "sha512-tDOzyPgp9pIRMR2x6C9+uDSJrnXSzxLtt3d7nC+Lrsy3jnJDHYfdQC/xcRyhJE/TOBJ0heSqRKR3UmejDjZxsw==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.106.2", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.106.2.tgz", + "integrity": "sha512-LdRGT7DNhyZkPjubUv5bSdAZ0jSEX8wTHvx7htj7+K59TOZRvz4TuQK7tL2RWxyIZVeFMRluL04SzWS61rKnUA==", + "license": "MIT", + "dependencies": { + "@supabase/phoenix": "^0.4.2", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.106.2", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.106.2.tgz", + "integrity": "sha512-xgKCSYuev1YarV+iVqr+zlfgSyremnJtn8T0NCT8L4XmMv1CLtESc0Q6kNp8+mKWdX/8ND0nzm7OMKx08kwNAw==", + "license": "MIT", + "dependencies": { + "iceberg-js": "^0.8.1", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.106.2", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.106.2.tgz", + "integrity": "sha512-2/RZ/1fmJx/MRSEDG2Xk8+J4JVk5clM9V0uSI6kUTrcS32KA89DtqI5RUOC9r6mzY3WBC9qexLjssIHjbLyVJA==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.106.2", + "@supabase/functions-js": "2.106.2", + "@supabase/postgrest-js": "2.106.2", + "@supabase/realtime-js": "2.106.2", + "@supabase/storage-js": "2.106.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.29", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.29.tgz", + "integrity": "sha512-ch0qJdr2JY0r04NXSprbK6TXOgnaJ1Tz23fm5W+z0/CBah6BSBc3n96h7K9GOtwh0HrilNWHIBzE1Ko4Dcw/Wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.32", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz", + "integrity": "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.364", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.364.tgz", + "integrity": "sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "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" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/iceberg-js": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", + "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz", + "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "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 + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/nix/dev/process-compose.nix b/nix/dev/process-compose.nix index 82a15a0..4853788 100644 --- a/nix/dev/process-compose.nix +++ b/nix/dev/process-compose.nix @@ -65,7 +65,7 @@ in }; }; frontend = { - command = "${pkgs.nodejs_20}/bin/npm --prefix frontend run dev"; + command = "${pkgs.nodejs_24}/bin/npm --prefix frontend install && ${pkgs.nodejs_24}/bin/npm --prefix frontend run dev"; environment = frontendEnv; }; # TODO: supabase compose (point POSTGRES_HOST at the services-flake db)