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..4dfc108
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,16 @@
+result
+result-*
+.direnv/
+node_modules/
+dist/
+*.log
+
+# local secrets — never commit
+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 e69de29..492da85 100644
--- a/README.md
+++ b/README.md
@@ -0,0 +1,66 @@
+# 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
+
+[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.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
new file mode 100644
index 0000000..27979c4
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,63 @@
+{
+ 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";
+ sops-nix = {
+ url = "github:Mic92/sops-nix";
+ inputs.nixpkgs.follows = "nixpkgs";
+ };
+ };
+
+ 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_24
+ postgresql_17
+ supabase-cli
+ deploy-rs.packages.${system}.default
+ sops
+ age
+ ssh-to-age
+ ];
+ };
+
+ # `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-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/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..3974838
--- /dev/null
+++ b/frontend/src/supabase.ts
@@ -0,0 +1,20 @@
+import { createClient } from "@supabase/supabase-js";
+
+// Single subdomain for everything Supabase — Kong fans out to /auth/v1, /rest/v1, /rpc.
+//
+// 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/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..dca0b59
--- /dev/null
+++ b/ingester/main.go
@@ -0,0 +1,69 @@
+// 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"
+)
+
+// 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"
+)
+
+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)
+ 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=%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/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..4853788
--- /dev/null
+++ b/nix/dev/process-compose.nix
@@ -0,0 +1,75 @@
+{ 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 ];
+
+ services = {
+ postgres."db" = {
+ enable = true;
+ port = dev.pgPort;
+ initialDatabases = [{ name = dev.pgDB; }];
+ };
+
+ mosquitto."broker" = {
+ enable = true;
+ port = dev.mqttPort;
+ };
+ };
+
+ settings.processes = {
+ ingester = {
+ command = "${pkgs.go}/bin/go run ./ingester";
+ environment = ingesterEnv;
+ depends_on = {
+ "db".condition = "process_healthy";
+ "broker".condition = "process_started";
+ };
+ };
+ frontend = {
+ 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)
+ # 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..7dcc1bc
--- /dev/null
+++ b/nix/hosts/app-host.nix
@@ -0,0 +1,29 @@
+{ config, lib, pkgs, ... }:
+
+{
+ 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.
+ 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..93f95b0
--- /dev/null
+++ b/nix/hosts/db-host.nix
@@ -0,0 +1,23 @@
+{ config, lib, pkgs, ... }:
+
+{
+ 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 ];
+
+ 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..e5bf946
--- /dev/null
+++ b/nix/hosts/default.nix
@@ -0,0 +1,28 @@
+{ inputs }:
+
+let
+ modules = import ../modules;
+
+ mkHost = name: extraModules:
+ inputs.nixpkgs.lib.nixosSystem {
+ system = "x86_64-linux";
+ specialArgs = { inherit inputs; };
+ modules = [
+ ./common.nix
+ 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;
+ };
+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..c6d39bc
--- /dev/null
+++ b/nix/hosts/mqtt-ingest.nix
@@ -0,0 +1,31 @@
+{ config, lib, pkgs, ... }:
+
+{
+ networking.hostName = "mqtt-ingest";
+
+ services.gebos.secrets.ingester = true;
+
+ 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 defaults to tcp://127.0.0.1:1883 (the local HiveMQ).
+ postgresUrl = "postgres://gebos_ingest@db-host.gebos.internal:5432/postgres?sslmode=require";
+ # GEBOS_POSTGRES_PASSWORD sourced from /run/secrets/gebos-env (sops-nix).
+ };
+
+ # 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..8ec34d7
--- /dev/null
+++ b/nix/modules/default.nix
@@ -0,0 +1,9 @@
+{
+ gebos-secrets = import ./gebos-secrets.nix;
+ 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..15de954
--- /dev/null
+++ b/nix/modules/gebos-ingester.nix
@@ -0,0 +1,44 @@
+{ 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;
+ 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.";
+ };
+ };
+
+ 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;
+ # 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-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-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
new file mode 100644
index 0000000..248c52e
--- /dev/null
+++ b/nix/modules/gebos-supabase.nix
@@ -0,0 +1,63 @@
+{ 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";
+ # 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"
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 ]