commit 0f3173d93ee1355f212f524549dc99d3075dae51 Author: Nils Pukropp Date: Tue May 12 19:25:14 2026 +0200 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..af6cd2d --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Per-package ignores live next to their package.json / Cargo.toml. +# This file only covers root-level artefacts. + +# Local secrets / env overrides +deploy/.env +deploy/*.local.yaml + +# Claude / editor / OS noise +.claude/ +.idea/ +.vscode/ +*.swp +.DS_Store +Thumbs.db + +# Generic build artefacts that may accidentally land at root +*.log diff --git a/Justfile b/Justfile new file mode 100644 index 0000000..8e31b62 --- /dev/null +++ b/Justfile @@ -0,0 +1,87 @@ +# SMGW PKI · Monorepo runner. +# Run `just` (no args) to list recipes. + +set shell := ["bash", "-euo", "pipefail", "-c"] + +repo := justfile_directory() +deploy := repo / "deploy" +backend := repo / "backend" +frontend := repo / "frontend" + +# Default target — print recipe list. +default: + @just --list --unsorted + +# ─── Compose lifecycle ─────────────────────────────────────────────────────── + +# Bring stack up (attached). Builds on first run. +up *ARGS: + docker compose --project-directory {{deploy}} -f {{deploy}}/compose.yaml up {{ARGS}} + +# Bring stack up detached. +up-d *ARGS: + docker compose --project-directory {{deploy}} -f {{deploy}}/compose.yaml up -d {{ARGS}} + +# Stop and remove containers + network. Volumes preserved. +down: + docker compose --project-directory {{deploy}} -f {{deploy}}/compose.yaml down + +# Rebuild images. `just rebuild backend` rebuilds one service. +build *ARGS: + docker compose --project-directory {{deploy}} -f {{deploy}}/compose.yaml build {{ARGS}} + +rebuild *ARGS: + docker compose --project-directory {{deploy}} -f {{deploy}}/compose.yaml build --no-cache {{ARGS}} + +# Tail logs (default: both services). Pass a service to filter. +logs *ARGS: + docker compose --project-directory {{deploy}} -f {{deploy}}/compose.yaml logs -f {{ARGS}} + +# Reload nginx config without restarting the container. +nginx-reload: + docker compose --project-directory {{deploy}} -f {{deploy}}/compose.yaml exec frontend nginx -s reload + +# Compose status table. +ps: + docker compose --project-directory {{deploy}} -f {{deploy}}/compose.yaml ps + +# ─── Local dev (without Docker) ────────────────────────────────────────────── + +# Run the Rust backend locally with dev-auth + CORS for Vite. +dev-backend: + cd {{backend}} && DEV_AUTH=1 CORS_ALLOW_ORIGIN=http://localhost:5173 cargo run + +# Run the Vite dev server (expects backend on :8443). +dev-frontend: + cd {{frontend}} && bun run dev + +# ─── OpenAPI contract ──────────────────────────────────────────────────────── + +# Regenerate openapi.json from the backend and the TS client from it. +gen-api: + cd {{backend}} && cargo run --quiet -- --emit-openapi > {{frontend}}/openapi.json + cd {{frontend}} && bun run gen:api + +# Fail if the committed openapi.json drifts from the current Rust source. +openapi-diff: + cd {{backend}} && cargo run --quiet -- --emit-openapi > /tmp/smgw-fresh-openapi.json + diff {{frontend}}/openapi.json /tmp/smgw-fresh-openapi.json \ + && echo "openapi.json in sync" \ + || ( echo "drift — run \`just gen-api\`" >&2 ; exit 1 ) + +# ─── Checks ────────────────────────────────────────────────────────────────── + +# Run both side checks (cargo check + bun typecheck). +check: + cd {{backend}} && cargo check + cd {{frontend}} && bun run typecheck + +# Run both test suites. +test: + cd {{backend}} && cargo test + cd {{frontend}} && bun run typecheck # placeholder — frontend tests TBD + +# Format + lint (best-effort). +fmt: + cd {{backend}} && cargo fmt + cd {{frontend}} && bun x prettier --write 'src/**/*.{ts,tsx,css}' 2>/dev/null || true diff --git a/README.md b/README.md new file mode 100644 index 0000000..7f824eb --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# smgw-pki + +Automatisierung der Smart-Meter-Gateway-PKI-Prozesse + Operator-Konsole. **Test- und Labor-Umgebung** gemäß BSI TR-03129-4, TR-03109-1 und SM-PKI Certificate Policy. + +## Pakete + +| Pfad | Inhalt | +| ----------- | --------------------------------------------------------------- | +| `backend/` | Rust-Service (`smgw-pki-automator`) — Sub-CA-Anbindung, HSM, Scheduler | +| `frontend/` | React-Konsole (`smgw-pki-console`) — Operator-UI auf gleicher API | +| `deploy/` | Docker-Compose, nginx-Config, `.env.example` | +| `docs/` | BSI-Architektur + Compliance-Mapping + Entwicklungs-Setup | + +Kontextdokumente in jedem Paket: `backend/CLAUDE.md`, `frontend/CLAUDE.md` und das Top-Level [`CLAUDE.md`](./CLAUDE.md). + +## Quickstart + +Voraussetzungen: Docker (+ compose), [`just`](https://github.com/casey/just). Für lokale Entwicklung ohne Container zusätzlich Rust ≥ 1.85 und [Bun](https://bun.sh/) ≥ 1.2. + +```bash +just # Rezepte auflisten +cp deploy/.env.example deploy/.env +just up # Stack hochfahren +``` + +Frontend: . Backend läuft intern auf `backend:8443` — siehe `deploy/compose.yaml`. + +Login im Lab: Cert-Subject wird vom Reverse-Proxy via `X-Forwarded-Cert-Subject` gesetzt. Solange `DEV_AUTH=1` (Standard in `.env`), wird zusätzlich ein Dev-Subject im Login-Formular akzeptiert. + +## Häufige Befehle + +```bash +just up # build + run +just down # stoppen +just logs backend # logs eines Services +just rebuild backend # ohne Cache neu bauen + +just dev-backend # cargo run (dev-auth + CORS für Vite) +just dev-frontend # bun run dev → http://localhost:5173 + +just gen-api # Rust → openapi.json → TS-Client +just openapi-diff # Drift-Check (CI-tauglich) + +just check # cargo check + bun typecheck +just test # cargo test +``` + +## API-Vertrag + +Single source of truth ist das Rust-Backend. `utoipa-axum` emittiert OpenAPI, `openapi-typescript` baut daraus den TS-Client. + +```bash +just gen-api # nach jeder API-Änderung im Backend +``` + +`frontend/openapi.json` ist eingecheckt für reproduzierbare Builds. `just openapi-diff` schlägt fehl, wenn der Snapshot von der aktuellen Rust-Quelle abweicht. + +## Sicherheit + +- **Nicht für Produktion.** SoftHSMv2 erfüllt SM-PKI CP Level 1 nur für Entwicklungszwecke. +- mTLS-Termination erfolgt vor dem Frontend-nginx (Caddy/Traefik o. ä.). Der Proxy setzt `X-Forwarded-Cert-Subject`. +- Sitzungen via `HttpOnly; SameSite=Strict; Secure`-Cookie. TTL 8 h. +- Callback-Handler des Backends prüft mTLS-Cert + SOAP-Signatur (Status TODO, siehe `docs/bsi-compliance.md`). + +## Status + +Skeleton. Sub-CA-Adapter, HSM-Anbindung und iconfig-Signatur sind Stubs. Frontend ist vollständig funktional gegen das aktuelle Backend; Aktionen, die noch nicht implementiert sind, geben deterministisch `501 not_implemented` zurück. + +Umsetzungsreihenfolge: `docs/architecture.md#umsetzungsreihenfolge`. diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..5a7bc46 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,7 @@ +target/ +.git/ +.idea/ +.vscode/ +*.swp +Dockerfile +.dockerignore diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..a5a4061 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,27 @@ +# Rust build artefacts +target/ +**/*.rs.bk +*.pdb + +# Local env overrides (real config lives in deploy/.env) +.env +.env.local +.env.*.local + +# SQLx offline cache (regenerate via `cargo sqlx prepare`) +.sqlx-tmp/ + +# Local SQLite databases / data dirs created by `cargo run` +*.db +*.db-journal +*.db-wal +*.db-shm +/data/ + +# Claude / editor / OS noise +.claude/ +.idea/ +.vscode/ +*.swp +.DS_Store +*.log diff --git a/backend/Cargo.lock b/backend/Cargo.lock new file mode 100644 index 0000000..71c0ee3 --- /dev/null +++ b/backend/Cargo.lock @@ -0,0 +1,3399 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "num-traits", + "windows-link", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[package]] +name = "cron" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8c3e73077b4b4a6ab1ea5047c37c57aee77657bc8ecd6f29b0af082d0b0c07" +dependencies = [ + "chrono", + "nom 7.1.3", + "once_cell", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cryptoki" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60d645cc2c5faf466571c0c752d39d8fbc2746773b2f043ac8f9cd73bec55db9" +dependencies = [ + "bitflags 1.3.2", + "cryptoki-sys", + "libloading", + "log", + "paste", + "secrecy", +] + +[[package]] +name = "cryptoki-sys" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "750380200f47d4ff677be725b6e0d78b590e1d0343573dcd4b62147f25dc6efa" +dependencies = [ + "libloading", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "email-encoding" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9298e6504d9b9e780ed3f7dfd43a61be8cd0e09eb07f7706a945b0072b6670b6" +dependencies = [ + "base64 0.22.1", + "memchr", +] + +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls 0.23.40", + "rustls-native-certs", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 1.0.7", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "lettre" +version = "0.11.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dabda5859ee7c06b995b9d1165aa52c39110e079ef609db97178d86aeb051fa7" +dependencies = [ + "async-trait", + "base64 0.22.1", + "email-encoding", + "email_address", + "fastrand", + "futures-io", + "futures-util", + "httpdate", + "idna", + "mime", + "nom 8.0.0", + "percent-encoding", + "quoted_printable", + "rustls 0.23.40", + "socket2", + "tokio", + "tokio-rustls", + "url", + "webpki-roots 1.0.7", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "bitflags 2.11.1", + "libc", + "plain", + "redox_syscall 0.7.5", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.6", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls 0.23.40", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash", + "rustls 0.23.40", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "quoted_printable" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478e0585659a122aa407eb7e3c0e1fa51b1d8a870038bd29f0cf4a8551eea972" + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "redox_syscall" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4666a1a60d8412eab19d94f6d13dcc9cea0a5ef4fdf6a5db306537413c661b1b" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls 0.23.40", + "rustls-native-certs", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 1.0.7", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "ring", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki 0.103.13", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "zeroize", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.1", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smgw-pki-automator" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "axum", + "base64 0.22.1", + "cryptoki", + "lettre", + "quick-xml", + "reqwest", + "serde", + "serde_json", + "sqlx", + "thiserror 1.0.69", + "time", + "tokio", + "tokio-cron-scheduler", + "tower-http", + "tracing", + "tracing-subscriber", + "utoipa", + "utoipa-axum", + "uuid", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlformat" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" +dependencies = [ + "nom 7.1.3", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" +dependencies = [ + "ahash", + "atoi", + "byteorder", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashlink", + "hex", + "indexmap", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "rustls 0.21.12", + "rustls-pemfile", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlformat", + "thiserror 1.0.69", + "time", + "tokio", + "tokio-stream", + "tracing", + "url", + "webpki-roots 0.25.4", +] + +[[package]] +name = "sqlx-macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 1.0.109", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" +dependencies = [ + "dotenvy", + "either", + "heck 0.4.1", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 1.0.109", + "tempfile", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" +dependencies = [ + "atoi", + "base64 0.21.7", + "bitflags 2.11.1", + "byteorder", + "bytes", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.6", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 1.0.69", + "time", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" +dependencies = [ + "atoi", + "base64 0.21.7", + "bitflags 2.11.1", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.6", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 1.0.69", + "time", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "time", + "tracing", + "url", + "urlencoding", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-cron-scheduler" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2594dd7c2abbbafbb1c78d167fd10860dc7bd75f814cb051a1e0d3e796b9702" +dependencies = [ + "chrono", + "cron", + "num-derive", + "num-traits", + "tokio", + "tracing", + "uuid", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls 0.23.40", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51" +dependencies = [ + "bitflags 2.11.1", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "tracing", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utoipa" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bde15df68e80b16c7d16b9616e80770ad158988daa56a27dccd1e55558b0160" +dependencies = [ + "indexmap", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-axum" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "839e89ad0db7f9e8737dace8ff43c1ce0711d5e0d08cc1c9d31cc8454d4643ee" +dependencies = [ + "axum", + "paste", + "tower-layer", + "tower-service", + "utoipa", +] + +[[package]] +name = "utoipa-gen" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba0b99ee52df3028635d93840c797102da61f8a7bb3cf751032455895b52ef8" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 2.0.117", + "uuid", +] + +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/backend/Cargo.toml b/backend/Cargo.toml new file mode 100644 index 0000000..503c6ee --- /dev/null +++ b/backend/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "smgw-pki-automator" +version = "0.1.0" +edition = "2021" + +[dependencies] +tokio = { version = "1", features = ["full"] } +axum = "0.7" +async-trait = "0.1" +thiserror = "1" +anyhow = "1" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +time = { version = "0.3", features = ["serde", "serde-well-known", "macros"] } + +# Storage +sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "sqlite", "time", "macros", "migrate"] } + +# HSM (PKCS#11) +cryptoki = "0.7" + +# HTTP / SOAP +reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "rustls-tls-native-roots"] } +quick-xml = { version = "0.36", features = ["serialize"] } +base64 = "0.22" + +# Mail +lettre = { version = "0.11", default-features = false, features = ["smtp-transport", "tokio1-rustls-tls", "builder"] } + +# Cron +tokio-cron-scheduler = "0.11" + +# OpenAPI / HTTP middleware +utoipa = { version = "5", features = ["axum_extras", "time", "uuid"] } +utoipa-axum = "0.1" +tower-http = { version = "0.6", features = ["cors", "trace"] } +uuid = { version = "1", features = ["v4", "serde"] } + +[dev-dependencies] +tokio = { version = "1", features = ["full", "test-util"] } diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..7c94e8e --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,37 @@ +# syntax=docker/dockerfile:1.7 +# Build stage — cached deps, then app sources. +FROM rust:1-bookworm AS builder +WORKDIR /app + +ENV CARGO_TERM_COLOR=always +ENV CARGO_INCREMENTAL=0 + +COPY Cargo.toml Cargo.lock ./ +# Touch a stub main to allow dependency-only build for caching. +RUN mkdir -p src && echo 'fn main() {}' > src/main.rs \ + && cargo build --release \ + && rm -rf src target/release/deps/smgw_pki_automator* + +COPY src ./src +COPY migrations ./migrations +RUN cargo build --release + +# Runtime stage — minimal Debian with TLS + xmlsec1 (for InitialConfigBuilder), +# softhsm2 libs available at runtime when needed. +FROM debian:bookworm-slim AS runtime +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + libssl3 \ + xmlsec1 \ + libsofthsm2 \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /opt/smgw +COPY --from=builder /app/target/release/smgw-pki-automator /usr/local/bin/smgw-pki-automator + +ENV RUST_LOG=info +ENV BIND_ADDR=0.0.0.0:8443 +EXPOSE 8443 +ENTRYPOINT ["/usr/local/bin/smgw-pki-automator"] diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..7695b5e --- /dev/null +++ b/backend/README.md @@ -0,0 +1,64 @@ +# smgw-pki-automator + +Automatisierungs-Tool für die Smart-Meter-Gateway (SMGW) PKI-Prozesse in einer +Test-/Labor-Umgebung. Erzeugt Schlüsselmaterial in einem HSM, beantragt +Zertifikate bei einer Sub-CA gemäß **BSI TR-03129-4**, erzeugt signierte +Initial-Konfigurationen gemäß **BSI TR-03109-1** und überwacht +Zertifikatslaufzeiten gemäß den Vorgaben der SM-PKI Certificate Policy. + +## Ziel + +- Asynchrone `RequestCertificate`-Aufrufe an die Test-Sub-CA (mTLS, SOAP). +- Asynchroner Callback-Endpunkt zur Annahme fertiger Zertifikate. +- Generierung signierter `iconfig.xml` + `iconfig.sig`, verpackt in + `iconfig.tar`. +- Periodische Prüfung auf ablaufende Zertifikate (Standard: 30 Tage vor Ablauf) + und automatische Erneuerung. +- Alerting per SMTP bei Fehlern. + +## Architektur + +Hexagonale Architektur (Ports & Adapters). Die fachliche Kernlogik +(`src/domain/`) kennt keine Infrastruktur. Sie spricht ausschließlich gegen +Ports (`src/ports/`). Konkrete Implementierungen liegen in `src/adapters/`. + +Details: [`../docs/architecture.md`](../docs/architecture.md). + +## Projektstruktur + +``` +src/ +├── domain/ Geschäftslogik, Entities (Certificate, Gateway) +├── ports/ Traits (Inbound/Outbound) — Schnittstellen +├── adapters/ Konkrete Impl.: HSM, Sub-CA, SQLite, SMTP, Clock +├── builders/ Builder für SOAP-Requests und iconfig.xml +├── app.rs Composition Root (Dependency Injection) +└── main.rs Tokio-Runtime, Tracing, Boot +``` + +## Dokumentation + +- [`../docs/architecture.md`](../docs/architecture.md) — Hexagonale Architektur, Ports & Adapters, Datenflüsse. +- [`../docs/bsi-compliance.md`](../docs/bsi-compliance.md) — Mapping BSI-Vorgaben → Code (TR-03129-4, TR-03109-1, SM-PKI CP). +- [`../docs/development.md`](../docs/development.md) — Lokales Setup (SoftHSM2-Container, mTLS-Testzertifikate, Build & Run). + +## Quickstart + +```bash +cargo check +cargo run +``` + +Für die volle Lab-Umgebung siehe [`../docs/development.md`](../docs/development.md). + +## Status + +Skeleton. Ports und Domäne stehen. Adapter sind Stubs, die `not implemented` +zurückgeben. Reihenfolge der Umsetzung siehe +[`../docs/architecture.md`](../docs/architecture.md#umsetzungsreihenfolge). + +## Sicherheitshinweis + +Dieses Tool ist **ausschließlich** für Test- und Labor-Umgebungen gedacht. Der +SoftHSMv2 erfüllt die "Security Level 1"-Anforderung der SM-PKI CP nur für +Entwicklungszwecke; für Produktion ist ein zertifiziertes HSM zwingend. diff --git a/backend/migrations/0001_init.sql b/backend/migrations/0001_init.sql new file mode 100644 index 0000000..976df4b --- /dev/null +++ b/backend/migrations/0001_init.sql @@ -0,0 +1,36 @@ +-- Smart Meter Gateways under PKI management. +CREATE TABLE IF NOT EXISTS gateways ( + id TEXT PRIMARY KEY, + serial_number TEXT NOT NULL, + admin_key_label TEXT NOT NULL, + created_at TEXT NOT NULL +); + +-- Issued end-entity certificates per gateway and usage. +-- not_before/not_after stored as RFC3339 UTC text (sorts lexicographically). +CREATE TABLE IF NOT EXISTS certificates ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + gateway_id TEXT NOT NULL REFERENCES gateways(id) ON DELETE CASCADE, + serial TEXT NOT NULL, + usage TEXT NOT NULL CHECK (usage IN ('tls', 'signature', 'encryption')), + pem TEXT NOT NULL, + not_before TEXT NOT NULL, + not_after TEXT NOT NULL, + updated_at TEXT NOT NULL, + UNIQUE (gateway_id, usage) +); + +CREATE INDEX IF NOT EXISTS idx_certificates_not_after + ON certificates (not_after); + +-- TR-03129-4 messageID -> gateway_id lookup for asynchronous CA callbacks. +CREATE TABLE IF NOT EXISTS pending_requests ( + message_id TEXT PRIMARY KEY, + gateway_id TEXT NOT NULL REFERENCES gateways(id) ON DELETE CASCADE, + created_at TEXT NOT NULL, + resolved_at TEXT +); + +CREATE INDEX IF NOT EXISTS idx_pending_unresolved + ON pending_requests (resolved_at) + WHERE resolved_at IS NULL; diff --git a/backend/src/adapters/clock.rs b/backend/src/adapters/clock.rs new file mode 100644 index 0000000..1997565 --- /dev/null +++ b/backend/src/adapters/clock.rs @@ -0,0 +1,11 @@ +use time::OffsetDateTime; + +use crate::ports::outbound::ClockPort; + +pub struct SystemClock; + +impl ClockPort for SystemClock { + fn now(&self) -> OffsetDateTime { + OffsetDateTime::now_utc() + } +} diff --git a/backend/src/adapters/db.rs b/backend/src/adapters/db.rs new file mode 100644 index 0000000..6426571 --- /dev/null +++ b/backend/src/adapters/db.rs @@ -0,0 +1,283 @@ +use std::str::FromStr; + +use async_trait::async_trait; +use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; +use sqlx::{Row, SqlitePool}; +use time::format_description::well_known::Rfc3339; +use time::OffsetDateTime; + +use crate::domain::certificate::{Certificate, CertificateUsage}; +use crate::domain::gateway::Gateway; +use crate::ports::outbound::{StorageError, StoragePort}; + +static MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!("./migrations"); + +pub struct SqliteAdapter { + pool: SqlitePool, +} + +impl SqliteAdapter { + pub async fn new(url: &str) -> Result { + let opts = SqliteConnectOptions::from_str(url) + .map_err(|e| StorageError::Backend(format!("parse url: {e}")))? + .create_if_missing(true); + + let pool = SqlitePoolOptions::new() + .max_connections(5) + .connect_with(opts) + .await + .map_err(|e| StorageError::Backend(e.to_string()))?; + + MIGRATOR + .run(&pool) + .await + .map_err(|e| StorageError::Backend(format!("migrate: {e}")))?; + + Ok(Self { pool }) + } + + pub async fn new_in_memory() -> Result { + Self::new("sqlite::memory:").await + } + + pub fn pool(&self) -> &SqlitePool { + &self.pool + } +} + +#[async_trait] +impl StoragePort for SqliteAdapter { + async fn get_expiring_certificates( + &self, + now: OffsetDateTime, + days_left: u32, + ) -> Result, StorageError> { + let now_s = now.format(&Rfc3339).map_err(fmt_err)?; + let cutoff = (now + time::Duration::days(days_left as i64)) + .format(&Rfc3339) + .map_err(fmt_err)?; + + let rows = sqlx::query( + "SELECT gateway_id, serial, usage, pem, not_before, not_after \ + FROM certificates \ + WHERE not_after >= ?1 AND not_after <= ?2 \ + ORDER BY not_after ASC", + ) + .bind(&now_s) + .bind(&cutoff) + .fetch_all(&self.pool) + .await + .map_err(backend_err)?; + + rows.into_iter().map(row_to_certificate).collect() + } + + async fn list_certificates(&self) -> Result, StorageError> { + let rows = sqlx::query( + "SELECT gateway_id, serial, usage, pem, not_before, not_after \ + FROM certificates \ + ORDER BY not_after ASC", + ) + .fetch_all(&self.pool) + .await + .map_err(backend_err)?; + + rows.into_iter().map(row_to_certificate).collect() + } + + async fn list_gateways(&self) -> Result, StorageError> { + let rows = sqlx::query( + "SELECT id, serial_number, admin_key_label FROM gateways ORDER BY id ASC", + ) + .fetch_all(&self.pool) + .await + .map_err(backend_err)?; + + rows.into_iter() + .map(|row| { + Ok(Gateway { + id: row.try_get("id").map_err(backend_err)?, + serial_number: row.try_get("serial_number").map_err(backend_err)?, + admin_key_label: row.try_get("admin_key_label").map_err(backend_err)?, + }) + }) + .collect() + } + + async fn save_pending_request( + &self, + message_id: &str, + gateway_id: &str, + ) -> Result<(), StorageError> { + let created_at = OffsetDateTime::now_utc().format(&Rfc3339).map_err(fmt_err)?; + sqlx::query( + "INSERT INTO pending_requests (message_id, gateway_id, created_at) \ + VALUES (?1, ?2, ?3)", + ) + .bind(message_id) + .bind(gateway_id) + .bind(created_at) + .execute(&self.pool) + .await + .map_err(backend_err)?; + Ok(()) + } + + async fn update_certificate( + &self, + gateway_id: &str, + new_cert_pem: &str, + ) -> Result<(), StorageError> { + // NOTE: parsing serial/not_before/not_after from PEM is the caller's + // responsibility today. The port signature should grow a structured + // Certificate argument; until then we only refresh PEM + updated_at. + let updated_at = OffsetDateTime::now_utc().format(&Rfc3339).map_err(fmt_err)?; + let result = sqlx::query( + "UPDATE certificates SET pem = ?1, updated_at = ?2 WHERE gateway_id = ?3", + ) + .bind(new_cert_pem) + .bind(updated_at) + .bind(gateway_id) + .execute(&self.pool) + .await + .map_err(backend_err)?; + + if result.rows_affected() == 0 { + return Err(StorageError::NotFound); + } + Ok(()) + } +} + +fn row_to_certificate(row: sqlx::sqlite::SqliteRow) -> Result { + let gateway_id: String = row.try_get("gateway_id").map_err(backend_err)?; + let serial: String = row.try_get("serial").map_err(backend_err)?; + let usage_s: String = row.try_get("usage").map_err(backend_err)?; + let pem: String = row.try_get("pem").map_err(backend_err)?; + let not_before_s: String = row.try_get("not_before").map_err(backend_err)?; + let not_after_s: String = row.try_get("not_after").map_err(backend_err)?; + + Ok(Certificate { + gateway_id, + serial, + usage: parse_usage(&usage_s)?, + pem, + not_before: OffsetDateTime::parse(¬_before_s, &Rfc3339).map_err(parse_err)?, + not_after: OffsetDateTime::parse(¬_after_s, &Rfc3339).map_err(parse_err)?, + }) +} + +fn parse_usage(s: &str) -> Result { + match s { + "tls" => Ok(CertificateUsage::Tls), + "signature" => Ok(CertificateUsage::Signature), + "encryption" => Ok(CertificateUsage::Encryption), + other => Err(StorageError::Backend(format!("unknown usage: {other}"))), + } +} + +pub fn usage_str(u: &CertificateUsage) -> &'static str { + match u { + CertificateUsage::Tls => "tls", + CertificateUsage::Signature => "signature", + CertificateUsage::Encryption => "encryption", + } +} + +fn backend_err(e: E) -> StorageError { + StorageError::Backend(e.to_string()) +} + +fn fmt_err(e: E) -> StorageError { + StorageError::Backend(format!("format: {e}")) +} + +fn parse_err(e: E) -> StorageError { + StorageError::Backend(format!("parse: {e}")) +} + +#[cfg(test)] +mod tests { + use super::*; + + async fn seed_gateway(pool: &SqlitePool, id: &str) { + let now = OffsetDateTime::now_utc().format(&Rfc3339).unwrap(); + sqlx::query( + "INSERT INTO gateways (id, serial_number, admin_key_label, created_at) \ + VALUES (?1, ?2, ?3, ?4)", + ) + .bind(id) + .bind("SN-1") + .bind("ADMIN-LABEL") + .bind(now) + .execute(pool) + .await + .unwrap(); + } + + async fn seed_cert( + pool: &SqlitePool, + gateway_id: &str, + usage: CertificateUsage, + not_after: OffsetDateTime, + ) { + let nb = (not_after - time::Duration::days(365)).format(&Rfc3339).unwrap(); + let na = not_after.format(&Rfc3339).unwrap(); + let updated = OffsetDateTime::now_utc().format(&Rfc3339).unwrap(); + sqlx::query( + "INSERT INTO certificates (gateway_id, serial, usage, pem, not_before, not_after, updated_at) \ + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", + ) + .bind(gateway_id) + .bind("SERIAL") + .bind(usage_str(&usage)) + .bind("-----BEGIN CERTIFICATE-----\nold\n-----END CERTIFICATE-----") + .bind(nb) + .bind(na) + .bind(updated) + .execute(pool) + .await + .unwrap(); + } + + #[tokio::test] + async fn get_expiring_returns_only_in_window() { + let db = SqliteAdapter::new_in_memory().await.unwrap(); + seed_gateway(db.pool(), "gw-1").await; + let now = OffsetDateTime::now_utc(); + seed_cert(db.pool(), "gw-1", CertificateUsage::Tls, now + time::Duration::days(10)).await; + seed_cert(db.pool(), "gw-1", CertificateUsage::Signature, now + time::Duration::days(90)).await; + + let result = db.get_expiring_certificates(now, 30).await.unwrap(); + + assert_eq!(result.len(), 1); + assert_eq!(result[0].usage, CertificateUsage::Tls); + } + + #[tokio::test] + async fn save_pending_then_update_certificate_roundtrip() { + let db = SqliteAdapter::new_in_memory().await.unwrap(); + seed_gateway(db.pool(), "gw-1").await; + let now = OffsetDateTime::now_utc(); + seed_cert(db.pool(), "gw-1", CertificateUsage::Tls, now + time::Duration::days(5)).await; + + db.save_pending_request("msg-abc", "gw-1").await.unwrap(); + db.update_certificate("gw-1", "-----BEGIN CERTIFICATE-----\nnew\n-----END CERTIFICATE-----") + .await + .unwrap(); + + let row = sqlx::query("SELECT pem FROM certificates WHERE gateway_id = 'gw-1'") + .fetch_one(db.pool()) + .await + .unwrap(); + let pem: String = row.try_get("pem").unwrap(); + assert!(pem.contains("new")); + } + + #[tokio::test] + async fn update_certificate_unknown_gateway_is_not_found() { + let db = SqliteAdapter::new_in_memory().await.unwrap(); + let err = db.update_certificate("nope", "pem").await.unwrap_err(); + assert!(matches!(err, StorageError::NotFound)); + } +} diff --git a/backend/src/adapters/hsm.rs b/backend/src/adapters/hsm.rs new file mode 100644 index 0000000..f58f046 --- /dev/null +++ b/backend/src/adapters/hsm.rs @@ -0,0 +1,33 @@ +use crate::ports::outbound::{HsmError, HsmPort}; + +pub struct SoftHsmAdapter { + _module_path: String, + _pin: String, +} + +impl SoftHsmAdapter { + pub fn new(module_path: impl Into, pin: impl Into) -> Self { + Self { + _module_path: module_path.into(), + _pin: pin.into(), + } + } + + pub fn new_stub() -> Self { + Self::new("/usr/lib/softhsm/libsofthsm2.so", "1234") + } +} + +impl HsmPort for SoftHsmAdapter { + fn generate_key_pair(&self, _label: &str) -> Result { + Err(HsmError::Other("not implemented".into())) + } + + fn sign_csr(&self, _key_id: &str, _payload: &[u8]) -> Result, HsmError> { + Err(HsmError::Other("not implemented".into())) + } + + fn sign_xml(&self, _key_id: &str, _xml_data: &str) -> Result { + Err(HsmError::Other("not implemented".into())) + } +} diff --git a/backend/src/adapters/mail.rs b/backend/src/adapters/mail.rs new file mode 100644 index 0000000..6161b82 --- /dev/null +++ b/backend/src/adapters/mail.rs @@ -0,0 +1,29 @@ +use async_trait::async_trait; + +use crate::ports::outbound::{NotificationError, NotificationPort}; + +pub struct SmtpAdapter { + _host: String, + _port: u16, +} + +impl SmtpAdapter { + pub fn new(host: impl Into, port: u16) -> Self { + Self { + _host: host.into(), + _port: port, + } + } + + pub fn new_stub() -> Self { + Self::new("smtp.local", 587) + } +} + +#[async_trait] +impl NotificationPort for SmtpAdapter { + async fn send_alert(&self, subject: &str, body: &str) -> Result<(), NotificationError> { + tracing::info!(subject, body, "alert (stub)"); + Ok(()) + } +} diff --git a/backend/src/adapters/mod.rs b/backend/src/adapters/mod.rs new file mode 100644 index 0000000..746a414 --- /dev/null +++ b/backend/src/adapters/mod.rs @@ -0,0 +1,5 @@ +pub mod clock; +pub mod db; +pub mod hsm; +pub mod mail; +pub mod sub_ca; diff --git a/backend/src/adapters/sub_ca.rs b/backend/src/adapters/sub_ca.rs new file mode 100644 index 0000000..7be5c5c --- /dev/null +++ b/backend/src/adapters/sub_ca.rs @@ -0,0 +1,27 @@ +use async_trait::async_trait; + +use crate::domain::certificate::CertificateRequest; +use crate::ports::outbound::{CaError, CertificateCaPort}; + +pub struct SubCaSoapAdapter { + _endpoint: String, +} + +impl SubCaSoapAdapter { + pub fn new(endpoint: impl Into) -> Self { + Self { + _endpoint: endpoint.into(), + } + } + + pub fn new_stub() -> Self { + Self::new("https://test-ca.local/soap") + } +} + +#[async_trait] +impl CertificateCaPort for SubCaSoapAdapter { + async fn request_certificate(&self, _csr: CertificateRequest) -> Result { + Err(CaError::Transport("not implemented".into())) + } +} diff --git a/backend/src/app.rs b/backend/src/app.rs new file mode 100644 index 0000000..28e3754 --- /dev/null +++ b/backend/src/app.rs @@ -0,0 +1,111 @@ +use std::sync::Arc; + +use anyhow::{Context, Result}; +use tokio::sync::{RwLock, Semaphore}; + +use crate::adapters; +use crate::http::{self, HttpState}; +use crate::ports::inbound::{HandleCaCallback, RenewExpiringCertificates}; +use crate::ports::outbound::{CertificateCaPort, ClockPort, HsmPort, NotificationPort, StoragePort}; +use crate::scheduler; +use crate::state::{ + RuntimeConfig, SchedulerState, SessionStore, SharedAlerts, SharedScheduler, SharedSessions, +}; +use crate::usecases; + +#[derive(Clone)] +pub struct AppState { + pub hsm: Arc, + pub ca: Arc, + pub storage: Arc, + pub mail: Arc, + pub clock: Arc, +} + +pub async fn run() -> Result<()> { + let state = build_state().await?; + let config = Arc::new(RwLock::new(RuntimeConfig::from_env())); + let cfg_snapshot = config.read().await.clone(); + + let scheduler_state: SharedScheduler = Arc::new(RwLock::new(SchedulerState { + cron_schedule: cfg_snapshot.cron_schedule.clone(), + days_window: cfg_snapshot.days_window, + ..Default::default() + })); + let alerts: SharedAlerts = Arc::new(RwLock::new(Vec::new())); + let sessions: SharedSessions = Arc::new(RwLock::new(SessionStore::default())); + let run_lock = Arc::new(Semaphore::new(1)); + + let renew: Arc = Arc::new(usecases::renew::RenewService { + storage: state.storage.clone(), + ca: state.ca.clone(), + hsm: state.hsm.clone(), + clock: state.clock.clone(), + notifier: state.mail.clone(), + }); + let callback: Arc = Arc::new(usecases::callback::CallbackService { + storage: state.storage.clone(), + }); + + let sched_handle = scheduler::start( + &cfg_snapshot.cron_schedule, + cfg_snapshot.days_window, + renew.clone(), + scheduler_state.clone(), + run_lock.clone(), + ) + .await?; + + let cors_origin = std::env::var("CORS_ALLOW_ORIGIN").ok(); + let dev_auth = std::env::var("DEV_AUTH").map(|v| v == "1").unwrap_or(false); + + let http_state = HttpState { + callback, + renew, + storage: state.storage.clone(), + mail: state.mail.clone(), + clock: state.clock.clone(), + config: config.clone(), + scheduler: scheduler_state.clone(), + alerts, + sessions, + run_lock, + dev_auth, + }; + + let router = http::router(http_state, cors_origin); + let bind = cfg_snapshot.bind_addr.clone(); + let listener = tokio::net::TcpListener::bind(&bind) + .await + .with_context(|| format!("bind {bind}"))?; + tracing::info!(%bind, "http server up"); + + axum::serve(listener, router) + .with_graceful_shutdown(shutdown_signal()) + .await + .context("axum serve")?; + + sched_handle.shutdown().await?; + Ok(()) +} + +async fn build_state() -> Result { + let hsm = Arc::new(adapters::hsm::SoftHsmAdapter::new_stub()); + let ca = Arc::new(adapters::sub_ca::SubCaSoapAdapter::new_stub()); + let storage = Arc::new(adapters::db::SqliteAdapter::new_in_memory().await?); + let mail = Arc::new(adapters::mail::SmtpAdapter::new_stub()); + let clock = Arc::new(adapters::clock::SystemClock); + + Ok(AppState { + hsm, + ca, + storage, + mail, + clock, + }) +} + +async fn shutdown_signal() { + let _ = tokio::signal::ctrl_c().await; + tracing::info!("shutdown signal received"); +} diff --git a/backend/src/builders/iconfig.rs b/backend/src/builders/iconfig.rs new file mode 100644 index 0000000..4fd1f2b --- /dev/null +++ b/backend/src/builders/iconfig.rs @@ -0,0 +1,33 @@ +use std::sync::Arc; + +use crate::ports::outbound::HsmPort; + +pub struct InitialConfigBuilder { + hsm: Arc, + admin_key_id: String, + gateway_id: Option, +} + +impl InitialConfigBuilder { + pub fn new(hsm: Arc, admin_key_id: impl Into) -> Self { + Self { + hsm, + admin_key_id: admin_key_id.into(), + gateway_id: None, + } + } + + pub fn gateway_id(mut self, id: impl Into) -> Self { + self.gateway_id = Some(id.into()); + self + } + + pub fn build_signed(self) -> Result { + let gw = self.gateway_id.ok_or("gateway_id required")?; + // TODO: build iconfig.xml via quick-xml, C14N, sign via HSM + let raw_xml = format!("", gw); + self.hsm + .sign_xml(&self.admin_key_id, &raw_xml) + .map_err(|e| e.to_string()) + } +} diff --git a/backend/src/builders/mod.rs b/backend/src/builders/mod.rs new file mode 100644 index 0000000..f0da69d --- /dev/null +++ b/backend/src/builders/mod.rs @@ -0,0 +1,2 @@ +pub mod iconfig; +pub mod soap_req; diff --git a/backend/src/builders/soap_req.rs b/backend/src/builders/soap_req.rs new file mode 100644 index 0000000..95159c7 --- /dev/null +++ b/backend/src/builders/soap_req.rs @@ -0,0 +1,38 @@ +use crate::domain::certificate::CertificateRequest; + +pub struct SoapRequestBuilder<'a> { + csr: Option<&'a CertificateRequest>, + message_id: Option, +} + +impl<'a> SoapRequestBuilder<'a> { + pub fn new() -> Self { + Self { + csr: None, + message_id: None, + } + } + + pub fn csr(mut self, csr: &'a CertificateRequest) -> Self { + self.csr = Some(csr); + self + } + + pub fn message_id(mut self, id: impl Into) -> Self { + self.message_id = Some(id.into()); + self + } + + pub fn build_request_certificate(self) -> Result { + let _csr = self.csr.ok_or("csr required")?; + let _mid = self.message_id.ok_or("message_id required")?; + // TODO: TR-03129-4 RequestCertificate envelope, base64(csr_der) + Err("not implemented") + } +} + +impl<'a> Default for SoapRequestBuilder<'a> { + fn default() -> Self { + Self::new() + } +} diff --git a/backend/src/domain/certificate.rs b/backend/src/domain/certificate.rs new file mode 100644 index 0000000..0556552 --- /dev/null +++ b/backend/src/domain/certificate.rs @@ -0,0 +1,36 @@ +use time::OffsetDateTime; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CertificateUsage { + Tls, + Signature, + Encryption, +} + +#[derive(Debug, Clone)] +pub struct Certificate { + pub gateway_id: String, + pub serial: String, + pub usage: CertificateUsage, + pub pem: String, + pub not_before: OffsetDateTime, + pub not_after: OffsetDateTime, +} + +impl Certificate { + pub fn days_until_expiry(&self, now: OffsetDateTime) -> i64 { + (self.not_after - now).whole_days() + } + + pub fn is_expiring_within(&self, now: OffsetDateTime, days: u32) -> bool { + let d = self.days_until_expiry(now); + d >= 0 && d <= days as i64 + } +} + +#[derive(Debug, Clone)] +pub struct CertificateRequest { + pub gateway_id: String, + pub usage: CertificateUsage, + pub csr_der: Vec, +} diff --git a/backend/src/domain/gateway.rs b/backend/src/domain/gateway.rs new file mode 100644 index 0000000..58c36d4 --- /dev/null +++ b/backend/src/domain/gateway.rs @@ -0,0 +1,6 @@ +#[derive(Debug, Clone)] +pub struct Gateway { + pub id: String, + pub serial_number: String, + pub admin_key_label: String, +} diff --git a/backend/src/domain/mod.rs b/backend/src/domain/mod.rs new file mode 100644 index 0000000..cce9b9b --- /dev/null +++ b/backend/src/domain/mod.rs @@ -0,0 +1,2 @@ +pub mod certificate; +pub mod gateway; diff --git a/backend/src/http/api/alerts.rs b/backend/src/http/api/alerts.rs new file mode 100644 index 0000000..80fda8d --- /dev/null +++ b/backend/src/http/api/alerts.rs @@ -0,0 +1,81 @@ +use axum::extract::State; +use axum::Json; +use serde::Deserialize; +use time::OffsetDateTime; +use utoipa::ToSchema; +use utoipa_axum::router::OpenApiRouter; +use utoipa_axum::routes; + +use super::error::ApiResult; +use crate::http::HttpState; +use crate::state::{AlertEntry, AlertSeverity}; + +pub fn router() -> OpenApiRouter { + OpenApiRouter::new() + .routes(routes!(list_alerts)) + .routes(routes!(send_test_alert)) +} + +#[derive(Debug, serde::Serialize, ToSchema)] +pub struct AlertListResponse { + pub items: Vec, +} + +#[derive(Debug, Deserialize, ToSchema)] +pub struct TestAlertRequest { + pub subject: Option, + pub body: Option, +} + +#[utoipa::path( + get, + path = "", + tag = "alerts", + responses( + (status = 200, description = "Recent alerts", body = AlertListResponse), + ) +)] +pub async fn list_alerts(State(state): State) -> Json { + let items = state.alerts.read().await.clone(); + Json(AlertListResponse { items }) +} + +#[utoipa::path( + post, + path = "/test", + tag = "alerts", + request_body = TestAlertRequest, + responses( + (status = 200, description = "Sent (or stub-logged)"), + ) +)] +pub async fn send_test_alert( + State(state): State, + Json(req): Json, +) -> ApiResult<()> { + let subject = req + .subject + .unwrap_or_else(|| "smgw-pki-automator: test alert".into()); + let body = req + .body + .unwrap_or_else(|| "If you see this, SMTP wiring works.".into()); + + let send_result = state.mail.send_alert(&subject, &body).await; + let mut alerts = state.alerts.write().await; + alerts.push(AlertEntry { + at: OffsetDateTime::now_utc(), + severity: if send_result.is_ok() { + AlertSeverity::Info + } else { + AlertSeverity::Error + }, + subject, + body, + }); + // Trim ringbuffer. + let len = alerts.len(); + if len > 200 { + alerts.drain(0..(len - 200)); + } + Ok(()) +} diff --git a/backend/src/http/api/auth.rs b/backend/src/http/api/auth.rs new file mode 100644 index 0000000..8d1fba9 --- /dev/null +++ b/backend/src/http/api/auth.rs @@ -0,0 +1,160 @@ +use axum::extract::State; +use axum::http::header::{HeaderMap, SET_COOKIE}; +use axum::http::StatusCode; +use axum::response::IntoResponse; +use axum::Json; +use serde::{Deserialize, Serialize}; +use time::{Duration, OffsetDateTime}; +use utoipa::ToSchema; +use utoipa_axum::router::OpenApiRouter; +use utoipa_axum::routes; +use uuid::Uuid; + +use super::error::{ApiResponseError, ApiResult}; +use crate::http::HttpState; +use crate::state::Session; + +pub const SESSION_COOKIE: &str = "smgw_session"; +pub const CERT_SUBJECT_HEADER: &str = "x-forwarded-cert-subject"; +pub const SESSION_TTL_MINUTES: i64 = 60 * 8; + +pub fn router() -> OpenApiRouter { + OpenApiRouter::new() + .routes(routes!(create_session)) + .routes(routes!(end_session)) + .routes(routes!(whoami)) +} + +#[derive(Debug, Serialize, ToSchema)] +pub struct SessionResponse { + pub subject: String, + #[serde(with = "time::serde::rfc3339")] + pub expires_at: OffsetDateTime, +} + +#[derive(Debug, Deserialize, ToSchema)] +pub struct LoginRequest { + /// Optional fallback subject for dev mode when no mTLS header is present. + pub dev_subject: Option, +} + +/// Exchange mTLS client cert (passed via `X-Forwarded-Cert-Subject`) for a +/// server-issued session cookie. The reverse proxy terminating mTLS is the +/// trust anchor. +#[utoipa::path( + post, + path = "/session", + tag = "auth", + request_body = LoginRequest, + responses( + (status = 200, description = "Session issued", body = SessionResponse), + (status = 403, description = "No client cert subject", body = super::error::ApiError), + ) +)] +pub async fn create_session( + State(state): State, + headers: HeaderMap, + Json(body): Json, +) -> ApiResult { + let subject = headers + .get(CERT_SUBJECT_HEADER) + .and_then(|v| v.to_str().ok()) + .map(|s| s.to_string()) + .or_else(|| { + if state.dev_auth { + body.dev_subject + } else { + None + } + }) + .ok_or_else(ApiResponseError::forbidden)?; + + let now = OffsetDateTime::now_utc(); + let expires_at = now + Duration::minutes(SESSION_TTL_MINUTES); + let session = Session { + id: Uuid::new_v4().to_string(), + subject: subject.clone(), + issued_at: now, + expires_at, + }; + + { + let mut store = state.sessions.write().await; + store.gc(now); + store.insert(session.clone()); + } + + let cookie = format!( + "{}={}; HttpOnly; SameSite=Strict; Path=/; Max-Age={}{}", + SESSION_COOKIE, + session.id, + SESSION_TTL_MINUTES * 60, + if state.dev_auth { "" } else { "; Secure" }, + ); + + Ok(( + StatusCode::OK, + [(SET_COOKIE, cookie)], + Json(SessionResponse { + subject, + expires_at, + }), + )) +} + +/// Revoke the current session and clear cookie. +#[utoipa::path( + delete, + path = "/session", + tag = "auth", + responses( + (status = 204, description = "Session ended"), + ) +)] +pub async fn end_session( + State(state): State, + headers: HeaderMap, +) -> impl IntoResponse { + if let Some(id) = session_id_from_cookie(&headers) { + state.sessions.write().await.remove(&id); + } + let clear = format!( + "{}=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0", + SESSION_COOKIE + ); + (StatusCode::NO_CONTENT, [(SET_COOKIE, clear)]) +} + +/// Inspect the current session. +#[utoipa::path( + get, + path = "/me", + tag = "auth", + responses( + (status = 200, description = "Current session", body = SessionResponse), + (status = 401, description = "No active session", body = super::error::ApiError), + ) +)] +pub async fn whoami( + State(state): State, + headers: HeaderMap, +) -> ApiResult> { + let id = session_id_from_cookie(&headers).ok_or_else(ApiResponseError::unauthorized)?; + let store = state.sessions.read().await; + let session = store.get(&id).ok_or_else(ApiResponseError::unauthorized)?; + if session.expires_at <= OffsetDateTime::now_utc() { + return Err(ApiResponseError::unauthorized()); + } + Ok(Json(SessionResponse { + subject: session.subject.clone(), + expires_at: session.expires_at, + })) +} + +pub fn session_id_from_cookie(headers: &HeaderMap) -> Option { + let raw = headers.get(axum::http::header::COOKIE)?.to_str().ok()?; + raw.split(';').find_map(|kv| { + let (k, v) = kv.split_once('=')?; + (k.trim() == SESSION_COOKIE).then(|| v.trim().to_string()) + }) +} diff --git a/backend/src/http/api/certs.rs b/backend/src/http/api/certs.rs new file mode 100644 index 0000000..264a609 --- /dev/null +++ b/backend/src/http/api/certs.rs @@ -0,0 +1,148 @@ +use axum::extract::{Path, State}; +use axum::Json; +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; +use utoipa::ToSchema; +use utoipa_axum::router::OpenApiRouter; +use utoipa_axum::routes; + +use super::error::{ApiResponseError, ApiResult}; +use crate::domain::certificate::{Certificate, CertificateUsage}; +use crate::http::HttpState; + +pub fn router() -> OpenApiRouter { + OpenApiRouter::new() + .routes(routes!(list_certificates)) + .routes(routes!(renew_certificate)) +} + +#[derive(Debug, Serialize, ToSchema)] +pub struct CertificateDto { + pub gateway_id: String, + pub serial: String, + pub usage: CertificateUsageDto, + #[serde(with = "time::serde::rfc3339")] + pub not_before: OffsetDateTime, + #[serde(with = "time::serde::rfc3339")] + pub not_after: OffsetDateTime, + pub days_to_expiry: i64, + pub state: CertState, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "lowercase")] +pub enum CertificateUsageDto { + Tls, + Signature, + Encryption, +} + +impl From<&CertificateUsage> for CertificateUsageDto { + fn from(u: &CertificateUsage) -> Self { + match u { + CertificateUsage::Tls => Self::Tls, + CertificateUsage::Signature => Self::Signature, + CertificateUsage::Encryption => Self::Encryption, + } + } +} + +impl From for CertificateUsage { + fn from(u: CertificateUsageDto) -> Self { + match u { + CertificateUsageDto::Tls => Self::Tls, + CertificateUsageDto::Signature => Self::Signature, + CertificateUsageDto::Encryption => Self::Encryption, + } + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "lowercase")] +pub enum CertState { + Valid, + Expiring, + Expired, +} + +#[derive(Debug, Serialize, ToSchema)] +pub struct CertListResponse { + pub items: Vec, +} + +#[derive(Debug, Serialize, ToSchema)] +pub struct RenewAccepted { + pub message_id: String, +} + +fn to_dto(c: &Certificate, now: OffsetDateTime, days_window: u32) -> CertificateDto { + let d = c.days_until_expiry(now); + let state = if d < 0 { + CertState::Expired + } else if d <= days_window as i64 { + CertState::Expiring + } else { + CertState::Valid + }; + CertificateDto { + gateway_id: c.gateway_id.clone(), + serial: c.serial.clone(), + usage: (&c.usage).into(), + not_before: c.not_before, + not_after: c.not_after, + days_to_expiry: d, + state, + } +} + +/// List all known end-entity certificates with derived state. +#[utoipa::path( + get, + path = "", + tag = "certs", + responses( + (status = 200, description = "Certificates", body = CertListResponse), + ) +)] +pub async fn list_certificates( + State(state): State, +) -> ApiResult> { + let certs = state + .storage + .list_certificates() + .await + .map_err(|e| ApiResponseError::internal(e.to_string()))?; + let now = state.clock.now(); + let cfg = state.config.read().await.clone(); + let items = certs + .iter() + .map(|c| to_dto(c, now, cfg.days_window)) + .collect(); + Ok(Json(CertListResponse { items })) +} + +/// Trigger an out-of-band renewal for a specific (gateway, usage) pair. +/// Returns the SOAP `messageID` so the caller can correlate the async callback. +#[utoipa::path( + post, + path = "/{gateway_id}/{usage}/renew", + tag = "certs", + params( + ("gateway_id" = String, Path, description = "Gateway identifier"), + ("usage" = CertificateUsageDto, Path, description = "Certificate usage"), + ), + responses( + (status = 202, description = "Renewal accepted", body = RenewAccepted), + (status = 501, description = "Sub-CA adapter not implemented yet", body = super::error::ApiError), + ) +)] +pub async fn renew_certificate( + State(_state): State, + Path((gateway_id, usage)): Path<(String, CertificateUsageDto)>, +) -> ApiResult> { + // TODO wire to RenewService once SubCaSoapAdapter is real. + tracing::info!(%gateway_id, ?usage, "manual renewal requested"); + Err(ApiResponseError::not_implemented( + "manual renewal pending SubCaSoapAdapter", + )) +} diff --git a/backend/src/http/api/config.rs b/backend/src/http/api/config.rs new file mode 100644 index 0000000..871d604 --- /dev/null +++ b/backend/src/http/api/config.rs @@ -0,0 +1,88 @@ +use axum::extract::State; +use axum::Json; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use utoipa_axum::router::OpenApiRouter; +use utoipa_axum::routes; + +use super::error::ApiResult; +use crate::http::HttpState; +use crate::state::{HsmConfig, RuntimeConfig, SmtpConfig, SubCaConfig}; + +pub fn router() -> OpenApiRouter { + OpenApiRouter::new() + .routes(routes!(get_config)) + .routes(routes!(update_config)) +} + +#[derive(Debug, Serialize, ToSchema)] +pub struct ConfigView { + pub config: RuntimeConfig, + pub restart_required_fields: Vec<&'static str>, +} + +#[derive(Debug, Deserialize, ToSchema)] +pub struct ConfigUpdate { + pub cron_schedule: Option, + pub days_window: Option, + pub sub_ca: Option, + pub smtp: Option, + pub hsm: Option, +} + +#[utoipa::path( + get, + path = "", + tag = "config", + responses( + (status = 200, description = "Current runtime config", body = ConfigView), + ) +)] +pub async fn get_config(State(state): State) -> ApiResult> { + let cfg = state.config.read().await.clone(); + Ok(Json(ConfigView { + config: cfg, + restart_required_fields: vec!["bind_addr", "database_url"], + })) +} + +#[utoipa::path( + put, + path = "", + tag = "config", + request_body = ConfigUpdate, + responses( + (status = 200, description = "Updated runtime config", body = ConfigView), + ) +)] +pub async fn update_config( + State(state): State, + Json(patch): Json, +) -> ApiResult> { + let mut cfg = state.config.write().await; + if let Some(v) = patch.cron_schedule { + cfg.cron_schedule = v; + } + if let Some(v) = patch.days_window { + cfg.days_window = v; + } + if let Some(v) = patch.sub_ca { + cfg.sub_ca = v; + } + if let Some(v) = patch.smtp { + cfg.smtp = v; + } + if let Some(v) = patch.hsm { + cfg.hsm = v; + } + // Mirror scheduler-affecting fields. + { + let mut sch = state.scheduler.write().await; + sch.cron_schedule = cfg.cron_schedule.clone(); + sch.days_window = cfg.days_window; + } + Ok(Json(ConfigView { + config: cfg.clone(), + restart_required_fields: vec!["bind_addr", "database_url"], + })) +} diff --git a/backend/src/http/api/error.rs b/backend/src/http/api/error.rs new file mode 100644 index 0000000..242ec6a --- /dev/null +++ b/backend/src/http/api/error.rs @@ -0,0 +1,77 @@ +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use axum::Json; +use serde::Serialize; +use utoipa::ToSchema; + +#[derive(Debug, Serialize, ToSchema)] +pub struct ApiError { + pub code: &'static str, + pub message: String, +} + +impl ApiError { + pub fn new(code: &'static str, message: impl Into) -> Self { + Self { + code, + message: message.into(), + } + } +} + +pub struct ApiResponseError { + pub status: StatusCode, + pub body: ApiError, +} + +impl ApiResponseError { + pub fn bad_request(msg: impl Into) -> Self { + Self { + status: StatusCode::BAD_REQUEST, + body: ApiError::new("bad_request", msg), + } + } + + pub fn unauthorized() -> Self { + Self { + status: StatusCode::UNAUTHORIZED, + body: ApiError::new("unauthorized", "session missing or expired"), + } + } + + pub fn forbidden() -> Self { + Self { + status: StatusCode::FORBIDDEN, + body: ApiError::new("forbidden", "client certificate not accepted"), + } + } + + pub fn not_found() -> Self { + Self { + status: StatusCode::NOT_FOUND, + body: ApiError::new("not_found", "resource not found"), + } + } + + pub fn not_implemented(msg: impl Into) -> Self { + Self { + status: StatusCode::NOT_IMPLEMENTED, + body: ApiError::new("not_implemented", msg), + } + } + + pub fn internal(msg: impl Into) -> Self { + Self { + status: StatusCode::INTERNAL_SERVER_ERROR, + body: ApiError::new("internal", msg), + } + } +} + +impl IntoResponse for ApiResponseError { + fn into_response(self) -> Response { + (self.status, Json(self.body)).into_response() + } +} + +pub type ApiResult = Result; diff --git a/backend/src/http/api/gateways.rs b/backend/src/http/api/gateways.rs new file mode 100644 index 0000000..66c5aa5 --- /dev/null +++ b/backend/src/http/api/gateways.rs @@ -0,0 +1,52 @@ +use axum::extract::State; +use axum::Json; +use serde::Serialize; +use utoipa::ToSchema; +use utoipa_axum::router::OpenApiRouter; +use utoipa_axum::routes; + +use super::error::{ApiResponseError, ApiResult}; +use crate::http::HttpState; + +pub fn router() -> OpenApiRouter { + OpenApiRouter::new().routes(routes!(list_gateways)) +} + +#[derive(Debug, Serialize, ToSchema)] +pub struct GatewayDto { + pub id: String, + pub serial_number: String, + pub admin_key_label: String, +} + +#[derive(Debug, Serialize, ToSchema)] +pub struct GatewayListResponse { + pub items: Vec, +} + +#[utoipa::path( + get, + path = "", + tag = "gateways", + responses( + (status = 200, description = "Gateways", body = GatewayListResponse), + ) +)] +pub async fn list_gateways( + State(state): State, +) -> ApiResult> { + let rows = state + .storage + .list_gateways() + .await + .map_err(|e| ApiResponseError::internal(e.to_string()))?; + let items = rows + .into_iter() + .map(|g| GatewayDto { + id: g.id, + serial_number: g.serial_number, + admin_key_label: g.admin_key_label, + }) + .collect(); + Ok(Json(GatewayListResponse { items })) +} diff --git a/backend/src/http/api/iconfig.rs b/backend/src/http/api/iconfig.rs new file mode 100644 index 0000000..164048c --- /dev/null +++ b/backend/src/http/api/iconfig.rs @@ -0,0 +1,70 @@ +use axum::extract::State; +use axum::Json; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use utoipa_axum::router::OpenApiRouter; +use utoipa_axum::routes; + +use super::error::{ApiResponseError, ApiResult}; +use crate::http::HttpState; + +pub fn router() -> OpenApiRouter { + OpenApiRouter::new() + .routes(routes!(preview_iconfig)) + .routes(routes!(build_iconfig)) +} + +#[derive(Debug, Deserialize, ToSchema)] +pub struct IconfigRequest { + pub gateway_id: String, + pub admin_key_label: String, + pub profile: String, + pub extras: Option, +} + +#[derive(Debug, Serialize, ToSchema)] +pub struct IconfigPreview { + pub xml: String, +} + +/// Render the unsigned `iconfig.xml` for review. Does not touch the HSM. +#[utoipa::path( + post, + path = "/preview", + tag = "iconfig", + request_body = IconfigRequest, + responses( + (status = 200, description = "Preview XML", body = IconfigPreview), + ) +)] +pub async fn preview_iconfig( + State(_state): State, + Json(req): Json, +) -> ApiResult> { + // TODO replace with InitialConfigBuilder once it produces canonical XML. + let xml = format!( + "\n\n {}\n\n", + req.gateway_id, req.profile, req.admin_key_label + ); + Ok(Json(IconfigPreview { xml })) +} + +/// Build, sign via HSM, and stream back `iconfig.tar`. +#[utoipa::path( + post, + path = "/build", + tag = "iconfig", + request_body = IconfigRequest, + responses( + (status = 200, description = "iconfig.tar", content_type = "application/x-tar"), + (status = 501, description = "HSM signature not implemented", body = super::error::ApiError), + ) +)] +pub async fn build_iconfig( + State(_state): State, + Json(_req): Json, +) -> ApiResult<()> { + Err(ApiResponseError::not_implemented( + "iconfig build pending InitialConfigBuilder + HsmPort::sign_xml", + )) +} diff --git a/backend/src/http/api/mod.rs b/backend/src/http/api/mod.rs new file mode 100644 index 0000000..8e6a371 --- /dev/null +++ b/backend/src/http/api/mod.rs @@ -0,0 +1,104 @@ +pub mod alerts; +pub mod auth; +pub mod certs; +pub mod config; +pub mod error; +pub mod gateways; +pub mod iconfig; +pub mod scheduler; + +use std::sync::Arc; + +use axum::Router; +use tower_http::cors::{AllowOrigin, CorsLayer}; +use utoipa::OpenApi; +use utoipa_axum::router::OpenApiRouter; + +use crate::http::HttpState; + +#[derive(OpenApi)] +#[openapi( + info( + title = "smgw-pki-automator", + version = env!("CARGO_PKG_VERSION"), + description = "Control + observation surface for the SMGW PKI automation tool. Test/lab use only.", + ), + tags( + (name = "auth", description = "mTLS-bridged session management"), + (name = "certs", description = "Certificate lifecycle"), + (name = "gateways", description = "Smart Meter Gateways"), + (name = "config", description = "Runtime configuration"), + (name = "scheduler", description = "Renewal scheduler"), + (name = "iconfig", description = "BSI TR-03109-1 initial config"), + (name = "alerts", description = "Operator alerts"), + ), +)] +pub struct ApiDoc; + +fn build() -> OpenApiRouter { + OpenApiRouter::with_openapi(ApiDoc::openapi()) + .nest("/auth", auth::router()) + .nest("/certs", certs::router()) + .nest("/gateways", gateways::router()) + .nest("/config", config::router()) + .nest("/scheduler", scheduler::router()) + .nest("/iconfig", iconfig::router()) + .nest("/alerts", alerts::router()) +} + +/// Build the OpenAPI document without instantiating runtime state. Used for +/// static spec emission feeding the frontend client generator. +pub fn openapi_spec() -> utoipa::openapi::OpenApi { + let (_, api) = build().split_for_parts(); + api +} + +/// Mount `/api/*` and emit OpenAPI at `/api/openapi.json`. +pub fn router(state: HttpState) -> Router { + let (router, api) = build().split_for_parts(); + let router = router.with_state(state); + let openapi_json = serde_json::to_string(&api).expect("serialize openapi"); + let openapi_arc = Arc::new(openapi_json); + + router.route( + "/openapi.json", + axum::routing::get({ + let doc = openapi_arc.clone(); + move || { + let doc = doc.clone(); + async move { + ( + [(axum::http::header::CONTENT_TYPE, "application/json")], + doc.as_str().to_string(), + ) + } + } + }), + ) +} + +pub fn cors_layer(allowed_origin: Option) -> CorsLayer { + let base = CorsLayer::new() + .allow_credentials(true) + .allow_methods([ + axum::http::Method::GET, + axum::http::Method::POST, + axum::http::Method::PUT, + axum::http::Method::DELETE, + axum::http::Method::OPTIONS, + ]) + .allow_headers([ + axum::http::header::CONTENT_TYPE, + axum::http::header::AUTHORIZATION, + axum::http::HeaderName::from_static("x-forwarded-cert-subject"), + ]); + match allowed_origin { + Some(origin) => base.allow_origin( + origin + .parse::() + .map(AllowOrigin::exact) + .unwrap_or_else(|_| AllowOrigin::any()), + ), + None => base.allow_origin(AllowOrigin::any()), + } +} diff --git a/backend/src/http/api/scheduler.rs b/backend/src/http/api/scheduler.rs new file mode 100644 index 0000000..7d3e667 --- /dev/null +++ b/backend/src/http/api/scheduler.rs @@ -0,0 +1,96 @@ +use axum::extract::State; +use axum::Json; +use serde::Deserialize; +use utoipa::ToSchema; +use utoipa_axum::router::OpenApiRouter; +use utoipa_axum::routes; + +use super::error::{ApiResponseError, ApiResult}; +use crate::http::HttpState; +use crate::state::SchedulerState; + +pub fn router() -> OpenApiRouter { + OpenApiRouter::new() + .routes(routes!(get_status)) + .routes(routes!(trigger_run)) + .routes(routes!(set_paused)) +} + +#[derive(Debug, Deserialize, ToSchema)] +pub struct PauseRequest { + pub paused: bool, +} + +#[utoipa::path( + get, + path = "", + tag = "scheduler", + responses( + (status = 200, description = "Scheduler state", body = SchedulerState), + ) +)] +pub async fn get_status(State(state): State) -> Json { + Json(state.scheduler.read().await.clone()) +} + +/// Run renewal once, out of band. Honours the same overlap-lock as the cron job. +#[utoipa::path( + post, + path = "/trigger", + tag = "scheduler", + responses( + (status = 202, description = "Run accepted"), + (status = 409, description = "Run already in progress", body = super::error::ApiError), + ) +)] +pub async fn trigger_run(State(state): State) -> ApiResult<()> { + let days = state.scheduler.read().await.days_window; + let renew = state.renew.clone(); + let sched = state.scheduler.clone(); + + if state.run_lock.clone().try_acquire_owned().is_err() { + return Err(ApiResponseError { + status: axum::http::StatusCode::CONFLICT, + body: super::error::ApiError::new("run_in_progress", "previous run still active"), + }); + } + + tokio::spawn(async move { + let permit = state.run_lock.clone().acquire_owned().await; + let started = time::OffsetDateTime::now_utc(); + let outcome = renew.run(days).await; + let mut s = sched.write().await; + s.last_run_at = Some(started); + match outcome { + Ok(n) => { + s.last_run_ok = Some(true); + s.last_handled = Some(n); + s.last_error = None; + } + Err(e) => { + s.last_run_ok = Some(false); + s.last_error = Some(e.to_string()); + } + } + drop(permit); + }); + Ok(()) +} + +#[utoipa::path( + post, + path = "/pause", + tag = "scheduler", + request_body = PauseRequest, + responses( + (status = 200, description = "Pause state updated", body = SchedulerState), + ) +)] +pub async fn set_paused( + State(state): State, + Json(body): Json, +) -> Json { + let mut s = state.scheduler.write().await; + s.paused = body.paused; + Json(s.clone()) +} diff --git a/backend/src/http/callback.rs b/backend/src/http/callback.rs new file mode 100644 index 0000000..16e2380 --- /dev/null +++ b/backend/src/http/callback.rs @@ -0,0 +1,45 @@ +use axum::extract::State; +use axum::http::StatusCode; +use axum::response::IntoResponse; + +use super::HttpState; + +/// TR-03129-4 Callback-Endpunkt. +/// +/// SECURITY: Vor dem Aufruf der Domäne MÜSSEN geprüft werden: +/// 1. mTLS-Client-Cert der CA (Server-seitig terminiert oder per +/// Connection-Info). +/// 2. XML-Signatur des SOAP-Envelopes. +/// Beides ist hier noch nicht implementiert. Siehe docs/bsi-compliance.md §1.3. +pub async fn handler(State(state): State, body: String) -> impl IntoResponse { + let parsed = parse_callback(&body); + let (message_id, cert) = match parsed { + Ok(v) => v, + Err(e) => { + tracing::warn!(error = %e, "callback parse failed"); + return (StatusCode::BAD_REQUEST, "bad request"); + } + }; + + match state.callback.handle(&message_id, &cert).await { + Ok(()) => (StatusCode::OK, "ok"), + Err(e) => { + tracing::error!(error = %e, "callback handler failed"); + (StatusCode::INTERNAL_SERVER_ERROR, "error") + } + } +} + +/// Naive Extraktion. Echte Impl. nutzt quick-xml und prüft den SOAP-Envelope. +fn parse_callback(body: &str) -> Result<(String, String), &'static str> { + let message_id = between(body, "", "").ok_or("missing messageID")?; + let cert = between(body, "", "") + .ok_or("missing certificateSeq")?; + Ok((message_id.to_string(), cert.to_string())) +} + +fn between<'a>(s: &'a str, start: &str, end: &str) -> Option<&'a str> { + let i = s.find(start)? + start.len(); + let j = s[i..].find(end)? + i; + Some(&s[i..j]) +} diff --git a/backend/src/http/health.rs b/backend/src/http/health.rs new file mode 100644 index 0000000..148c6ba --- /dev/null +++ b/backend/src/http/health.rs @@ -0,0 +1,5 @@ +use axum::http::StatusCode; + +pub async fn handler() -> (StatusCode, &'static str) { + (StatusCode::OK, "ok") +} diff --git a/backend/src/http/mod.rs b/backend/src/http/mod.rs new file mode 100644 index 0000000..76cc1f3 --- /dev/null +++ b/backend/src/http/mod.rs @@ -0,0 +1,41 @@ +pub mod api; +pub mod callback; +pub mod health; + +use std::sync::Arc; + +use axum::routing::{get, post}; +use axum::Router; +use tokio::sync::Semaphore; + +use crate::ports::inbound::{HandleCaCallback, RenewExpiringCertificates}; +use crate::ports::outbound::{ClockPort, NotificationPort, StoragePort}; +use crate::state::{SharedAlerts, SharedConfig, SharedScheduler, SharedSessions}; + +/// State shared with every HTTP handler. Cheap to clone (Arc inside). +#[derive(Clone)] +pub struct HttpState { + pub callback: Arc, + pub renew: Arc, + pub storage: Arc, + pub mail: Arc, + pub clock: Arc, + pub config: SharedConfig, + pub scheduler: SharedScheduler, + pub alerts: SharedAlerts, + pub sessions: SharedSessions, + /// Single-flight guard reused by cron scheduler and manual trigger. + pub run_lock: Arc, + /// In dev (no reverse proxy) accept a `dev_subject` field in /api/auth/session. + pub dev_auth: bool, +} + +pub fn router(state: HttpState, cors_origin: Option) -> Router { + let api_router = api::router(state.clone()).layer(api::cors_layer(cors_origin)); + + Router::new() + .route("/health", get(health::handler)) + .route("/pki/callback", post(callback::handler)) + .with_state(state) + .nest("/api", api_router) +} diff --git a/backend/src/main.rs b/backend/src/main.rs new file mode 100644 index 0000000..187c12b --- /dev/null +++ b/backend/src/main.rs @@ -0,0 +1,29 @@ +mod adapters; +mod app; +mod builders; +mod domain; +mod http; +mod ports; +mod scheduler; +mod state; +mod usecases; + +use anyhow::Result; +use tracing_subscriber::EnvFilter; + +#[tokio::main] +async fn main() -> Result<()> { + let args: Vec = std::env::args().collect(); + if args.iter().any(|a| a == "--emit-openapi") { + let spec = http::api::openapi_spec(); + let json = serde_json::to_string_pretty(&spec)?; + println!("{json}"); + return Ok(()); + } + + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"))) + .init(); + + app::run().await +} diff --git a/backend/src/ports/inbound.rs b/backend/src/ports/inbound.rs new file mode 100644 index 0000000..e5f5a16 --- /dev/null +++ b/backend/src/ports/inbound.rs @@ -0,0 +1,20 @@ +use async_trait::async_trait; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum UseCaseError { + #[error("dependency failed: {0}")] + Dependency(String), + #[error("invariant violated: {0}")] + Invariant(String), +} + +#[async_trait] +pub trait RenewExpiringCertificates: Send + Sync { + async fn run(&self, days_window: u32) -> Result; +} + +#[async_trait] +pub trait HandleCaCallback: Send + Sync { + async fn handle(&self, message_id: &str, cert_pem: &str) -> Result<(), UseCaseError>; +} diff --git a/backend/src/ports/mod.rs b/backend/src/ports/mod.rs new file mode 100644 index 0000000..5756441 --- /dev/null +++ b/backend/src/ports/mod.rs @@ -0,0 +1,2 @@ +pub mod inbound; +pub mod outbound; diff --git a/backend/src/ports/outbound.rs b/backend/src/ports/outbound.rs new file mode 100644 index 0000000..e8f2f3f --- /dev/null +++ b/backend/src/ports/outbound.rs @@ -0,0 +1,83 @@ +use async_trait::async_trait; +use thiserror::Error; +use time::OffsetDateTime; + +use crate::domain::certificate::{Certificate, CertificateRequest}; +use crate::domain::gateway::Gateway; + +#[derive(Debug, Error)] +pub enum HsmError { + #[error("pkcs11 session error: {0}")] + Session(String), + #[error("key not found: {0}")] + KeyNotFound(String), + #[error("sign failed: {0}")] + Sign(String), + #[error("other: {0}")] + Other(String), +} + +#[derive(Debug, Error)] +pub enum CaError { + #[error("transport error: {0}")] + Transport(String), + #[error("soap fault: {0}")] + SoapFault(String), + #[error("malformed response: {0}")] + Malformed(String), +} + +#[derive(Debug, Error)] +pub enum StorageError { + #[error("not found")] + NotFound, + #[error("backend error: {0}")] + Backend(String), +} + +#[derive(Debug, Error)] +pub enum NotificationError { + #[error("send failed: {0}")] + Send(String), +} + +pub trait HsmPort: Send + Sync { + fn generate_key_pair(&self, label: &str) -> Result; + fn sign_csr(&self, key_id: &str, payload: &[u8]) -> Result, HsmError>; + fn sign_xml(&self, key_id: &str, xml_data: &str) -> Result; +} + +#[async_trait] +pub trait CertificateCaPort: Send + Sync { + async fn request_certificate(&self, csr: CertificateRequest) -> Result; +} + +#[async_trait] +pub trait StoragePort: Send + Sync { + async fn get_expiring_certificates( + &self, + now: OffsetDateTime, + days_left: u32, + ) -> Result, StorageError>; + async fn list_certificates(&self) -> Result, StorageError>; + async fn list_gateways(&self) -> Result, StorageError>; + async fn save_pending_request( + &self, + message_id: &str, + gateway_id: &str, + ) -> Result<(), StorageError>; + async fn update_certificate( + &self, + gateway_id: &str, + new_cert_pem: &str, + ) -> Result<(), StorageError>; +} + +#[async_trait] +pub trait NotificationPort: Send + Sync { + async fn send_alert(&self, subject: &str, body: &str) -> Result<(), NotificationError>; +} + +pub trait ClockPort: Send + Sync { + fn now(&self) -> OffsetDateTime; +} diff --git a/backend/src/scheduler/mod.rs b/backend/src/scheduler/mod.rs new file mode 100644 index 0000000..2fcff3d --- /dev/null +++ b/backend/src/scheduler/mod.rs @@ -0,0 +1,76 @@ +use std::sync::Arc; + +use anyhow::{Context, Result}; +use time::OffsetDateTime; +use tokio::sync::Semaphore; +use tokio_cron_scheduler::{Job, JobScheduler}; + +use crate::ports::inbound::RenewExpiringCertificates; +use crate::state::SharedScheduler; + +pub struct SchedulerHandle { + sched: JobScheduler, +} + +impl SchedulerHandle { + pub async fn shutdown(mut self) -> Result<()> { + self.sched.shutdown().await.context("scheduler shutdown")?; + Ok(()) + } +} + +pub async fn start( + cron_expr: &str, + days_window: u32, + renew: Arc, + state: SharedScheduler, + run_lock: Arc, +) -> Result { + let sched = JobScheduler::new().await.context("scheduler new")?; + + let renew_for_job = renew.clone(); + let lock_for_job = run_lock.clone(); + let state_for_job = state.clone(); + let job = Job::new_async(cron_expr, move |_uuid, _l| { + let renew = renew_for_job.clone(); + let lock = lock_for_job.clone(); + let state = state_for_job.clone(); + Box::pin(async move { + if state.read().await.paused { + tracing::info!("renew job paused — skipping tick"); + return; + } + let _permit = match lock.try_acquire() { + Ok(p) => p, + Err(_) => { + tracing::warn!("renew job overlap — previous run still active, skipping"); + return; + } + }; + let started = OffsetDateTime::now_utc(); + let outcome = renew.run(days_window).await; + let mut s = state.write().await; + s.last_run_at = Some(started); + match outcome { + Ok(n) => { + tracing::info!(handled = n, "renew run finished"); + s.last_run_ok = Some(true); + s.last_handled = Some(n); + s.last_error = None; + } + Err(e) => { + tracing::error!(error = %e, "renew run failed"); + s.last_run_ok = Some(false); + s.last_error = Some(e.to_string()); + } + } + }) + }) + .context("build renew job")?; + + sched.add(job).await.context("scheduler add")?; + sched.start().await.context("scheduler start")?; + + tracing::info!(cron = cron_expr, days_window, "scheduler up"); + Ok(SchedulerHandle { sched }) +} diff --git a/backend/src/state.rs b/backend/src/state.rs new file mode 100644 index 0000000..ae7a2c9 --- /dev/null +++ b/backend/src/state.rs @@ -0,0 +1,152 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; +use tokio::sync::RwLock; +use utoipa::ToSchema; + +/// Mutable runtime config. Seeded from env on boot; UI may override at runtime +/// for fields flagged `hot_reload`. Restart-only fields (BIND_ADDR) are read +/// but cannot be applied without restart. +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct RuntimeConfig { + pub bind_addr: String, + pub cron_schedule: String, + pub days_window: u32, + pub database_url: String, + pub sub_ca: SubCaConfig, + pub smtp: SmtpConfig, + pub hsm: HsmConfig, +} + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct SubCaConfig { + pub endpoint: String, + pub client_cert_path: Option, + pub client_key_path: Option, + pub ca_bundle_path: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct SmtpConfig { + pub host: String, + pub port: u16, + pub from: String, + pub to: String, + pub starttls: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct HsmConfig { + pub module_path: String, + pub slot: Option, + pub pin_env_var: String, +} + +impl RuntimeConfig { + pub fn from_env() -> Self { + Self { + bind_addr: std::env::var("BIND_ADDR").unwrap_or_else(|_| "0.0.0.0:8443".into()), + cron_schedule: std::env::var("CRON_SCHEDULE").unwrap_or_else(|_| "0 0 3 * * *".into()), + days_window: std::env::var("DAYS_WINDOW") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(30), + database_url: std::env::var("DATABASE_URL") + .unwrap_or_else(|_| "sqlite::memory:".into()), + sub_ca: SubCaConfig { + endpoint: std::env::var("SUB_CA_ENDPOINT") + .unwrap_or_else(|_| "https://test-ca.local/soap".into()), + client_cert_path: std::env::var("SUB_CA_CLIENT_CERT").ok(), + client_key_path: std::env::var("SUB_CA_CLIENT_KEY").ok(), + ca_bundle_path: std::env::var("SUB_CA_BUNDLE").ok(), + }, + smtp: SmtpConfig { + host: std::env::var("SMTP_HOST").unwrap_or_else(|_| "smtp.local".into()), + port: std::env::var("SMTP_PORT") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(587), + from: std::env::var("SMTP_FROM").unwrap_or_else(|_| "pki-bot@local".into()), + to: std::env::var("SMTP_TO").unwrap_or_else(|_| "ops@local".into()), + starttls: std::env::var("SMTP_STARTTLS") + .map(|v| v != "0") + .unwrap_or(true), + }, + hsm: HsmConfig { + module_path: std::env::var("HSM_MODULE") + .unwrap_or_else(|_| "/usr/lib/softhsm/libsofthsm2.so".into()), + slot: std::env::var("HSM_SLOT").ok().and_then(|s| s.parse().ok()), + pin_env_var: std::env::var("HSM_PIN_ENV").unwrap_or_else(|_| "HSM_PIN".into()), + }, + } + } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema)] +pub struct SchedulerState { + pub cron_schedule: String, + pub days_window: u32, + pub paused: bool, + #[serde(with = "time::serde::rfc3339::option")] + pub last_run_at: Option, + pub last_run_ok: Option, + pub last_error: Option, + pub last_handled: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct AlertEntry { + #[serde(with = "time::serde::rfc3339")] + pub at: OffsetDateTime, + pub severity: AlertSeverity, + pub subject: String, + pub body: String, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum AlertSeverity { + Info, + Warning, + Error, +} + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct Session { + pub id: String, + pub subject: String, + #[serde(with = "time::serde::rfc3339")] + pub issued_at: OffsetDateTime, + #[serde(with = "time::serde::rfc3339")] + pub expires_at: OffsetDateTime, +} + +#[derive(Default)] +pub struct SessionStore { + inner: HashMap, +} + +impl SessionStore { + pub fn insert(&mut self, s: Session) { + self.inner.insert(s.id.clone(), s); + } + + pub fn get(&self, id: &str) -> Option<&Session> { + self.inner.get(id) + } + + pub fn remove(&mut self, id: &str) { + self.inner.remove(id); + } + + pub fn gc(&mut self, now: OffsetDateTime) { + self.inner.retain(|_, s| s.expires_at > now); + } +} + +pub type SharedConfig = Arc>; +pub type SharedScheduler = Arc>; +pub type SharedAlerts = Arc>>; +pub type SharedSessions = Arc>; diff --git a/backend/src/usecases/callback.rs b/backend/src/usecases/callback.rs new file mode 100644 index 0000000..1f9c073 --- /dev/null +++ b/backend/src/usecases/callback.rs @@ -0,0 +1,20 @@ +use std::sync::Arc; + +use async_trait::async_trait; + +use crate::ports::inbound::{HandleCaCallback, UseCaseError}; +use crate::ports::outbound::StoragePort; + +pub struct CallbackService { + pub storage: Arc, +} + +#[async_trait] +impl HandleCaCallback for CallbackService { + async fn handle(&self, message_id: &str, cert_pem: &str) -> Result<(), UseCaseError> { + tracing::info!(message_id, len = cert_pem.len(), "callback received"); + // TODO: pending lookup über message_id, dann update_certificate + let _ = self.storage.as_ref(); + Ok(()) + } +} diff --git a/backend/src/usecases/mod.rs b/backend/src/usecases/mod.rs new file mode 100644 index 0000000..33cc085 --- /dev/null +++ b/backend/src/usecases/mod.rs @@ -0,0 +1,2 @@ +pub mod callback; +pub mod renew; diff --git a/backend/src/usecases/renew.rs b/backend/src/usecases/renew.rs new file mode 100644 index 0000000..ebbdfb2 --- /dev/null +++ b/backend/src/usecases/renew.rs @@ -0,0 +1,32 @@ +use std::sync::Arc; + +use async_trait::async_trait; + +use crate::ports::inbound::{RenewExpiringCertificates, UseCaseError}; +use crate::ports::outbound::{ + CertificateCaPort, ClockPort, HsmPort, NotificationPort, StoragePort, +}; + +pub struct RenewService { + pub storage: Arc, + pub ca: Arc, + pub hsm: Arc, + pub clock: Arc, + pub notifier: Arc, +} + +#[async_trait] +impl RenewExpiringCertificates for RenewService { + async fn run(&self, days_window: u32) -> Result { + let now = self.clock.now(); + let expiring = self + .storage + .get_expiring_certificates(now, days_window) + .await + .map_err(|e| UseCaseError::Dependency(e.to_string()))?; + + tracing::info!(count = expiring.len(), days_window, "renew scan"); + // TODO: für jedes Cert: keypair -> CSR -> sign -> SOAP RequestCertificate + Ok(expiring.len()) + } +} diff --git a/deploy/.env.example b/deploy/.env.example new file mode 100644 index 0000000..cd6340b --- /dev/null +++ b/deploy/.env.example @@ -0,0 +1,29 @@ +# Copy to .env (compose picks it up automatically). All values are overridable +# at deploy time; defaults in compose.yaml are sensible for lab use. + +# ─── Web exposure ──────────────────────────────────────────────────────────── +WEB_PORT=8080 +CORS_ALLOW_ORIGIN=http://localhost:8080 + +# ─── Auth (LAB ONLY) ───────────────────────────────────────────────────────── +# When DEV_AUTH=1 the backend accepts a `dev_subject` in the login body +# instead of requiring the reverse-proxy header. Switch to 0 once mTLS +# termination is wired in front of nginx. +DEV_AUTH=1 + +# ─── Scheduler ─────────────────────────────────────────────────────────────── +# 6-field cron: sec min hour day month weekday +CRON_SCHEDULE=0 0 3 * * * +DAYS_WINDOW=30 + +# ─── Storage ───────────────────────────────────────────────────────────────── +DATABASE_URL=sqlite:///data/smgw.db?mode=rwc + +# ─── Sub-CA (TR-03129-4) ───────────────────────────────────────────────────── +SUB_CA_ENDPOINT=https://test-ca.local/soap + +# ─── HSM (SoftHSMv2 inside container) ──────────────────────────────────────── +HSM_MODULE=/usr/lib/softhsm/libsofthsm2.so + +# ─── Logging ───────────────────────────────────────────────────────────────── +RUST_LOG=info,smgw_pki_automator=debug diff --git a/deploy/compose.yaml b/deploy/compose.yaml new file mode 100644 index 0000000..0fc2539 --- /dev/null +++ b/deploy/compose.yaml @@ -0,0 +1,50 @@ +name: smgw-pki + +services: + backend: + image: smgw-pki-automator:dev + build: + context: ../backend + dockerfile: Dockerfile + container_name: smgw-pki-automator + environment: + RUST_LOG: ${RUST_LOG:-info,smgw_pki_automator=debug} + BIND_ADDR: 0.0.0.0:8443 + CRON_SCHEDULE: ${CRON_SCHEDULE:-0 0 3 * * *} + DAYS_WINDOW: ${DAYS_WINDOW:-30} + DATABASE_URL: ${DATABASE_URL:-sqlite:///data/smgw.db?mode=rwc} + CORS_ALLOW_ORIGIN: ${CORS_ALLOW_ORIGIN:-http://localhost:8080} + DEV_AUTH: ${DEV_AUTH:-1} + SUB_CA_ENDPOINT: ${SUB_CA_ENDPOINT:-https://test-ca.local/soap} + HSM_MODULE: ${HSM_MODULE:-/usr/lib/softhsm/libsofthsm2.so} + volumes: + - backend_data:/data + - softhsm_tokens:/var/lib/softhsm/tokens + expose: + - "8443" + healthcheck: + test: ["CMD", "curl", "-fsS", "http://localhost:8443/health"] + interval: 5s + timeout: 3s + retries: 10 + start_period: 10s + + frontend: + image: smgw-pki-console:dev + build: + context: ../frontend + dockerfile: Dockerfile + container_name: smgw-pki-console + depends_on: + backend: + condition: service_healthy + ports: + - "${WEB_PORT:-8080}:80" + volumes: + # nginx.conf lives next to compose.yaml so ops can iterate without + # rebuilding the frontend image. Reload via `just nginx-reload`. + - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro + +volumes: + backend_data: + softhsm_tokens: diff --git a/deploy/nginx.conf b/deploy/nginx.conf new file mode 100644 index 0000000..bcc49af --- /dev/null +++ b/deploy/nginx.conf @@ -0,0 +1,44 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # SPA: route everything that isn't a real file back to index.html so + # TanStack Router handles deep links. + location / { + try_files $uri $uri/ /index.html; + } + + # API → Rust backend. The compose network name `backend` resolves to the + # smgw-pki-automator service. + location /api/ { + proxy_pass http://backend:8443/api/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # mTLS terminating proxy in front of nginx is expected to set this + # header. In dev (DEV_AUTH=1) the backend accepts a body field instead. + proxy_set_header X-Forwarded-Cert-Subject $http_x_forwarded_cert_subject; + + proxy_buffering off; + proxy_read_timeout 300s; + } + + # PKI callback path (TR-03129-4). Kept separate so it can be moved behind + # a different listener that requires a Sub-CA client certificate. + location /pki/ { + proxy_pass http://backend:8443/pki/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } + + # Long-cache hashed assets. + location ~* \.(js|css|woff2?|svg|png|webp|avif)$ { + expires 7d; + access_log off; + try_files $uri =404; + } +} diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..1c40363 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,198 @@ +# Architektur + +## Leitprinzip — Hexagonale Architektur + +Die Anwendung folgt dem Muster **Ports & Adapters** (Alistair Cockburn). Ziel +ist die strikte Trennung der fachlichen Logik (Domain) von technischer +Infrastruktur (HSM, CA, DB, SMTP, HTTP). Konsequenzen: + +- Die Domäne ist frei von asynchroner Laufzeit, ORM-Typen, HTTP-Clients und + XML-Bibliotheken. Sie enthält reine Daten und reine Funktionen. +- Use-Cases ("Anwendungsdienste") orchestrieren die Domäne, indem sie Ports + konsumieren. +- Adapter sind austauschbar. Für Tests gibt es In-Memory-Adapter; für die + Lab-Umgebung gibt es SoftHSM-/SQLite-/SMTP-/SOAP-Adapter. + +``` + ┌──────────────────────────────────────┐ + │ Domain │ + │ Certificate, Gateway, Policies │ + └───────────────▲──────────────────────┘ + │ + ┌────────────────────┴──────────────────────┐ + │ Ports │ + │ Inbound (UseCases) Outbound (Traits) │ + └─────▲─────────────────────────▲───────────┘ + │ │ + ┌────────────────┴─────┐ ┌─────────┴─────────────────┐ + │ Inbound │ │ Outbound │ + │ (treibt die App) │ │ (wird von der App │ + │ │ │ getrieben) │ + │ • Axum HTTP-Server │ │ • SoftHsmAdapter │ + │ • Tokio-Cron │ │ • SubCaSoapAdapter │ + │ │ │ • SqliteAdapter │ + │ │ │ • SmtpAdapter │ + │ │ │ • SystemClock │ + └──────────────────────┘ └───────────────────────────┘ +``` + +## Schichten + +### Domain (`src/domain/`) + +Keine externen Abhängigkeiten außer `time` (Datum/Zeit-Typen). + +- `certificate.rs` — `Certificate`, `CertificateRequest`, `CertificateUsage`, + reine Methoden wie `days_until_expiry`, `is_expiring_within`. +- `gateway.rs` — `Gateway`. + +### Ports (`src/ports/`) + +Traits, die die Domäne nach außen exponiert bzw. von außen erwartet. + +**Outbound-Ports** (`outbound.rs`) — was die Domäne von der Außenwelt braucht: + +| Port | Zweck | +| ------------------- | ------------------------------------------- | +| `HsmPort` | Keypair-Generierung, CSR- und XML-Signatur. | +| `CertificateCaPort` | Asynchrone Zertifikatsanfrage an Sub-CA. | +| `StoragePort` | Persistenz von Zertifikaten + Pending Reqs. | +| `NotificationPort` | Versand von Alert-Mails. | +| `ClockPort` | Testbare Zeitquelle für Ablauflogik. | + +Jeder Port hat seinen eigenen Fehlertyp (`HsmError`, `CaError`, +`StorageError`, `NotificationError`) via `thiserror`. Auf Use-Case-Ebene wird +auf `UseCaseError` aggregiert; auf App-Ebene auf `anyhow::Result`. + +**Inbound-Ports** (`inbound.rs`) — was die Domäne anbietet: + +- `RenewExpiringCertificates` — wird vom Cron getrieben. +- `HandleCaCallback` — wird vom HTTP-Webhook getrieben. + +### Adapters (`src/adapters/`) + +Konkrete Implementierungen der Outbound-Ports: + +| Adapter | Crates | Anmerkung | +| -------------------- | ---------------------------- | ------------------------------------------------------ | +| `SoftHsmAdapter` | `cryptoki` | PKCS#11. Blocking-Aufrufe via `spawn_blocking`. | +| `SubCaSoapAdapter` | `reqwest` (rustls), `quick-xml` | mTLS-Client. SOAP per Hand (kleine Surface). | +| `SqliteAdapter` | `sqlx` | Migrationen unter `migrations/`. | +| `SmtpAdapter` | `lettre` | SMTP via Tokio + rustls. | +| `SystemClock` | `time` | Triviale `now()`-Implementierung. | + +### Builders (`src/builders/`) + +Kapseln das Erzeugen komplexer Payloads: + +- `SoapRequestBuilder` — TR-03129-4 `RequestCertificate`-Envelope inkl. + `callbackIndicator=callback_possible`, eindeutiger `messageID`, + Base64-kodierter CSR im Feld `certReq`. +- `InitialConfigBuilder` — Erzeugt `iconfig.xml` (TR-03109-1) und ruft den + `HsmPort` zur Signatur (`iconfig.sig`) auf. Wichtig: Signatur erfolgt auf + den kanonischen (C14N) Bytes, **nicht** auf pretty-printed XML. + +### Composition Root (`src/app.rs`) + +`AppState` hält `Arc` für alle Outbound-Ports. `app::run()` baut die +Adapter, wired sie in den AppState, startet Axum-Router und Tokio-Cron. + +## Datenflüsse + +### Zertifikatserneuerung (Cron-getrieben) + +``` +Cron ──► RenewExpiringCertificates::run(days=30) + │ + ├─► StoragePort::get_expiring_certificates(now, 30) + │ + └─► für jedes Cert: + ├─► HsmPort::generate_key_pair("gw--") + ├─► CSR bauen, HsmPort::sign_csr(...) + ├─► SoapRequestBuilder::build_request_certificate(...) + ├─► CertificateCaPort::request_certificate(csr) + │ └─ liefert messageID + └─► StoragePort::save_pending_request(messageID, gateway_id) +``` + +Die CA antwortet synchron nur mit `returnCode=ok_syntax`. Das eigentliche +Zertifikat kommt asynchron über den Callback. + +### Callback-Annahme (HTTP-getrieben) + +``` +CA ──► POST /pki/callback (mTLS, SOAP) + │ + └─► Axum Handler + ├─► mTLS-Client-Cert prüfen + ├─► SOAP-Signatur prüfen + ├─► messageID + certificateSeq extrahieren + └─► HandleCaCallback::handle(messageID, certificateSeq) + ├─► StoragePort: pending → resolved + ├─► StoragePort::update_certificate(...) + └─► NotificationPort::send_alert(...) bei Fehler +``` + +**Polling ist laut BSI verboten.** Der Callback ist der einzige Weg. + +### Initial-Konfiguration + +``` +CLI/HTTP-Trigger ──► InitialConfigBuilder + ├─► XML bauen (quick-xml) + ├─► C14N + ├─► HsmPort::sign_xml(GWADM_SIG_PRV-Label, c14n) + │ └─ liefert iconfig.sig (XML-DSig) + └─► tar(iconfig.xml, iconfig.sig) → iconfig.tar +``` + +## Querschnittsthemen + +### Fehlerbehandlung + +- Adapter werfen ihren eigenen `thiserror`-Typ. +- Use-Cases mappen auf `UseCaseError`. +- `main.rs` / `app::run` arbeitet mit `anyhow::Result`. +- Keine `Result<_, String>` in Ports — strukturierte Fehler sind testbar und + matchbar. + +### Async-Modell + +- `HsmPort` ist **synchron**, weil PKCS#11 nativ blockierend ist. Adapter + ruft `tokio::task::spawn_blocking` an den richtigen Stellen. +- Alle anderen Ports sind `async_trait`. +- Eine Tokio-Runtime trägt Axum und Cron. `tokio-cron-scheduler` läuft im + selben Runtime. + +### Nebenläufigkeit der Cron-Jobs + +`tokio-cron-scheduler` startet einen Job auch dann, wenn der vorherige Lauf +noch läuft. Erneuerungs-Job daher mit `Semaphore(1)` schützen, um doppelte +`RequestCertificate`-Aufrufe für dasselbe Gateway zu verhindern. + +### Testbarkeit + +- Domäne wird ohne Adapter getestet. +- Pro Port existiert ein In-Memory-Adapter unter `#[cfg(test)]`. +- Integrationstests gegen SoftHSM2 laufen in Docker (siehe + [`development.md`](development.md)). + +## Umsetzungsreihenfolge + +1. **Domain + Ports** stehen — Code kompiliert. +2. **In-Memory-Adapter** für jeden Port → erste Use-Case-Tests grün. +3. **`SoftHsmAdapter`** — riskantestes Stück, früh validieren. +4. **`SoapRequestBuilder` + Mock-CA** (Wiremock o.ä.). +5. **`SqliteAdapter`** + Migrationen. +6. **Axum** Router (Callback + GUI) + Cron-Wiring. +7. **`SmtpAdapter`** zuletzt. + +## Bewusste Nicht-Entscheidungen + +- **Kein WSDL-Codegen.** Die `RequestCertificate`-Surface ist klein genug, + um per Hand mit `quick-xml` zu bauen. Eingespart: ein zerbrechlicher + Codegen-Schritt. +- **Kein DI-Framework.** `Arc` im `AppState` reicht. +- **XML-DSig nicht in reinem Rust.** Wir wrappen das `xmlsec1`-CLI im + `SoftHsmAdapter` (über Pipes), bis ein reifer Rust-Wrapper für `libxmlsec1` + verfügbar ist. Dokumentiert in [`bsi-compliance.md`](bsi-compliance.md). diff --git a/docs/bsi-compliance.md b/docs/bsi-compliance.md new file mode 100644 index 0000000..3b2dff8 --- /dev/null +++ b/docs/bsi-compliance.md @@ -0,0 +1,141 @@ +# BSI-Konformität + +Mapping der Vorgaben aus den BSI-Richtlinien auf die Code-Stellen in diesem +Projekt. Quelle aller Anforderungen: **BSI TR-03129-4**, **BSI TR-03109-1** +und **SM-PKI Certificate Policy**. + +## 1. TR-03129-4 — Schnittstelle zur Zertifizierungsstelle + +### 1.1 Transportsicherheit (mTLS) + +| Anforderung | Code-Stelle | +| ------------------------------------------------------------------ | -------------------------------------- | +| SOAP-Nachrichten enthalten **keine** Auth-Daten. | `builders/soap_req.rs` | +| Autorisierung + Verschlüsselung **ausschließlich** via mTLS. | `adapters/sub_ca.rs` | +| Client-Cert (EMT-Testzertifikat) konfiguriert in `reqwest`. | `SubCaSoapAdapter::new(...)` | +| Server-Cert der CA gegen vertrauten CA-Trust-Store verifiziert. | `SubCaSoapAdapter::new(...)` | + +Konkret in `reqwest`: + +```rust +reqwest::ClientBuilder::new() + .use_rustls_tls() + .identity(reqwest::Identity::from_pem(client_pem_bundle)?) + .add_root_certificate(reqwest::Certificate::from_pem(ca_pem)?) + .build()? +``` + +### 1.2 WSDL-Konformität + +| Anforderung | Code-Stelle | +| -------------------------------------------------------- | ------------------------- | +| Envelope folgt der offiziellen BSI-WSDL. | `builders/soap_req.rs` | +| Operation `RequestCertificate` mit korrektem Namespace. | `SoapRequestBuilder::build_request_certificate` | +| CSR wird Base64-kodiert im Feld `certReq` übertragen. | dito | + +Bewusste Entscheidung: **kein WSDL-Codegen**. Die Surface ist klein, +`quick-xml` reicht; Codegen-Crates für Rust sind unausgereift und brittle. + +### 1.3 Asynchrone Callbacks (Polling-Verbot) + +| Anforderung | Code-Stelle | +| ------------------------------------------------------ | -------------------------------------------------- | +| `callbackIndicator = callback_possible` im Request. | `SoapRequestBuilder::build_request_certificate` | +| Eindeutige `messageID` pro Anfrage. | `SoapRequestBuilder::message_id` | +| Synchroner Response enthält i.d.R. nur `returnCode`. | `SubCaSoapAdapter::request_certificate` | +| Webhook nimmt das fertige Zertifikat entgegen. | Axum-Handler (folgt; `POST /pki/callback`) | +| Zuordnung Callback → Request über `messageID`. | `StoragePort::save_pending_request` / Lookup | +| Zertifikatskette aus `certificateSeq` extrahieren. | Axum-Handler / `HandleCaCallback` | + +**Sicherheitspunkt:** `messageID` allein ist **kein** Trust-Boundary. Der +Callback-Handler muss zusätzlich: + +1. mTLS-Client-Cert der CA prüfen. +2. SOAP-Signatur der Nachricht prüfen. + +Erst dann darf in der DB nachgeschlagen werden. + +### 1.4 Datenfelder + +| Feld | Inhalt | Quelle | +| ----------------- | ------------------------------------------ | ----------------------------------- | +| `certReq` | Base64(CSR-DER) — vom HSM erzeugt. | `HsmPort::sign_csr` | +| `messageID` | UUIDv4, persistiert in `pending_requests`. | `SoapRequestBuilder::message_id` | +| `callbackIndicator` | `callback_possible` | `SoapRequestBuilder` | +| `certificateSeq` | Antwort der CA via Callback. | Axum-Handler | + +## 2. TR-03109-1 — Initial-Konfiguration + +### 2.1 Dateiformat `iconfig.tar` + +| Pflicht-Inhalt | Code-Stelle | +| ----------------------------------------------- | ------------------------------------ | +| `iconfig.xml` — Konfiguration. | `builders/iconfig.rs` | +| `iconfig.sig` — XML-DSig über `iconfig.xml`. | `builders/iconfig.rs` + `HsmPort` | +| Beide Dateien in einem Tar-Archiv verpackt. | `builders/iconfig.rs` (Tar-Stufe) | + +### 2.2 Inhalt der `iconfig.xml` + +Pflichtbestandteile: + +- Admin-Zertifikate (öffentliche Teile). +- CA-Zertifikatskette. +- Netzwerkprofil (IP-Access für WAN). +- Kommunikationsprofil (TLS-Admin für WAN-Schnittstelle). + +Generierung im `InitialConfigBuilder` via `quick-xml`. Eingangsdaten aus der +Domäne (`Gateway`) bzw. aus Konfig-Files. + +### 2.3 Kryptografische Signatur `iconfig.sig` + +| Anforderung | Code-Stelle | +| -------------------------------------------------------------------- | ------------------------------------ | +| Signatur mit `GWADM_SIG_PRV` (privater Signaturschlüssel des GWA). | `HsmPort::sign_xml` | +| Schlüssel liegt im HSM; Zugriff via PKCS#11. | `adapters/hsm.rs` (`cryptoki`) | +| Signatur über **kanonische** Bytes (XML-C14N). | `InitialConfigBuilder` | + +**Implementierungs-Hinweis (XML-DSig):** + +Reines Rust für XML-DSig ist nicht reif. Optionen: + +1. `xmlsec1` CLI über `std::process::Command` aus dem Adapter aufrufen + (pragmatischer Start). +2. `libxmlsec1` über FFI binden. +3. C14N + Signatur manuell via `cryptoki` zusammensetzen (fehleranfällig + wegen exakter Canonicalization). + +Wir starten mit Option 1 und kapseln das vollständig hinter `HsmPort::sign_xml`, +damit ein späterer Wechsel keinen Domänen-Code anfasst. + +## 3. SM-PKI Certificate Policy + +### 3.1 Schlüsselspeicherung + +| Anforderung | Code-Stelle | +| ------------------------------------------------------------ | ------------------------- | +| Alle Teilnehmer-Schlüssel in HSMs ≥ "Security Level 1". | `adapters/hsm.rs` | +| SoftHSMv2 erfüllt SL1 **nur** für Entwicklung. | siehe Sicherheitshinweis | + +In Produktion: zertifiziertes HSM (CC EAL4+) zwingend. Der `HsmPort` bleibt +identisch — nur das Backend tauscht. + +### 3.2 Zertifikatslaufzeiten und Rotation + +| Anforderung | Code-Stelle | +| -------------------------------------------------------- | ------------------------------------------ | +| Endentitäten/Sub-CAs in der Regel alle 2 Jahre. | Datenmodell `Certificate.not_after` | +| Erneuerung rechtzeitig vor Ablauf (Standard: 30 Tage). | `RenewExpiringCertificates::run(days=30)` | +| Asynchroner `RequestCertificate`-Flow. | siehe §1.3 | +| Testbare Zeitquelle. | `ClockPort` / `SystemClock` | + +Das 30-Tage-Fenster ist Default, kein Gesetz — über Config einstellbar. + +## 4. Offene Punkte + +- Konkrete Werte der BSI-Namespace-URIs und Operationsnamen aus der aktuellen + WSDL-Version müssen in `builders/soap_req.rs` als Konstanten gepflegt + werden, sobald die WSDL aus dem BSI-Repo gezogen ist. +- Schema-Validierung der `iconfig.xml` gegen das BSI-XSD ist offen; sinnvoll + als zusätzlicher Adapter-Check vor dem Signieren. +- mTLS-Server (Axum für Callback) muss Client-Cert-Pinning auf die CA-Cert + durchsetzen — Konfiguration unter [`development.md`](development.md). diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..e1720e4 --- /dev/null +++ b/docs/development.md @@ -0,0 +1,153 @@ +# Development Setup + +Lokales Setup für die Lab-Umgebung. Ziel: Tool gegen einen SoftHSM2-Container +und eine Test-Sub-CA betreiben, ohne echte SMGW-Hardware. + +## Voraussetzungen + +- Rust ≥ 1.80 (`rustup`) +- Docker / Podman +- `xmlsec1` CLI (für die XML-DSig in `iconfig.sig`) +- OpenSSL CLI (für Testzertifikate) + +Auf Debian/Ubuntu: + +```bash +sudo apt install xmlsec1 libxmlsec1-dev openssl pkg-config +``` + +## Build & Run + +```bash +cargo check +cargo run +``` + +Logs via `RUST_LOG`: + +```bash +RUST_LOG=smgw_pki_automator=debug,info cargo run +``` + +## SoftHSMv2 als Container + +`SoftHSMv2` per Docker laufen lassen. Beispiel (Pseudo-Compose, anpassen an +euer Setup): + +```yaml +services: + softhsm: + image: ghcr.io/example/softhsm2:latest + volumes: + - ./tokens:/var/lib/softhsm/tokens + environment: + SOFTHSM2_CONF: /etc/softhsm2.conf +``` + +Token einmalig initialisieren: + +```bash +softhsm2-util --init-token --slot 0 \ + --label "smgw-lab" \ + --pin 1234 --so-pin 5678 +``` + +Im `app.rs`-Boot-Pfad wird der Adapter konstruiert mit: + +```rust +SoftHsmAdapter::new("/usr/lib/softhsm/libsofthsm2.so", "1234") +``` + +Pfad zur PKCS#11-Lib hängt vom Container/OS ab. + +## mTLS-Testzertifikate + +Für die Kommunikation mit der Test-Sub-CA brauchen wir: + +- **Client-Cert (EMT)** — wird im `reqwest`-Client als `Identity` geladen. +- **Server-Cert der CA** — wird als Trust-Root im Client geladen. +- **Eigenes Server-Cert** für den Axum-Callback-Endpunkt (mTLS-Server). +- **CA-Trust** für eingehende CA-Calls, damit das Client-Cert der CA gegen + diesen Trust verifiziert werden kann. + +Test-PKI mit OpenSSL aufsetzen (vereinfacht): + +```bash +mkdir -p certs && cd certs + +# Test-Root +openssl req -x509 -newkey rsa:4096 -days 3650 -nodes \ + -keyout test-root.key -out test-root.crt \ + -subj "/CN=smgw-lab-root" + +# Client (EMT) +openssl req -newkey rsa:4096 -nodes -keyout emt.key -out emt.csr \ + -subj "/CN=emt-client" +openssl x509 -req -in emt.csr -CA test-root.crt -CAkey test-root.key \ + -CAcreateserial -out emt.crt -days 825 + +# Server (CA-Mock + Axum-Callback) +openssl req -newkey rsa:4096 -nodes -keyout server.key -out server.csr \ + -subj "/CN=ca.test.local" +openssl x509 -req -in server.csr -CA test-root.crt -CAkey test-root.key \ + -CAcreateserial -out server.crt -days 825 +``` + +PEM-Bundle für `reqwest::Identity::from_pem`: + +```bash +cat emt.crt emt.key > emt-bundle.pem +``` + +## Datenbank + +SQLite, default `sqlite::memory:` im Stub-Boot. Für Persistenz Pfad setzen: + +```bash +DATABASE_URL=sqlite://./data/smgw.db cargo run +``` + +Migrationen (sobald angelegt) via `sqlx`: + +```bash +cargo install sqlx-cli --no-default-features --features sqlite,rustls +sqlx migrate run +``` + +## SMTP + +Für lokales Testing `MailHog` oder `Mailpit`: + +```bash +docker run -d -p 1025:1025 -p 8025:8025 axllent/mailpit +``` + +`SmtpAdapter::new("localhost", 1025)`. + +## Test-Sub-CA + +Optionen: + +1. **Mock-Server** mit `wiremock` für Integrationstests — antwortet auf + `RequestCertificate` mit `ok_syntax` und triggert anschließend einen + HTTP-Call gegen den eigenen Callback-Endpunkt. Kein BSI-Compliance-Ersatz, + aber gut für CI. +2. **Echte Test-Sub-CA**-Instanz (sofern verfügbar) — Adresse + Test-EMT- + Zertifikate aus eurem Lab-Bestand einsetzen. + +## Tests + +```bash +cargo test # Unit + In-Memory-Integration +cargo test --features it # Integration mit SoftHSM-Container (folgt) +``` + +Konvention: Tests liegen neben dem Code (`#[cfg(test)] mod tests`). Adapter- +Tests, die externe Dienste brauchen, hinter Cargo-Feature `it` gaten. + +## Code-Style + +- `cargo fmt` vor jedem Commit. +- `cargo clippy --all-targets -- -D warnings` muss grün sein. +- Keine `Result<_, String>` in Produktionscode. Strukturierte Fehler via + `thiserror`. diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..57542c1 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,10 @@ +node_modules/ +dist/ +.vite/ +.git/ +.idea/ +.vscode/ +*.log +pnpm-lock.yaml +package-lock.json +yarn.lock diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..9766070 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,4 @@ +# Used by Vite during `pnpm dev`. +# Where to proxy /api/* during development. Default: the Rust binary running +# locally on the default BIND_ADDR. +VITE_API_PROXY_TARGET=http://localhost:8443 diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..2e455b2 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,25 @@ +node_modules/ +dist/ +.vite/ +.tanstack/ +.tsbuildinfo +.env +.env.local +.env.*.local +*.log + +# Claude / editor / OS noise +.claude/ +.idea/ +.vscode/ +*.swp +.DS_Store + +# Lockfiles from other package managers — bun.lock is the source of truth +pnpm-lock.yaml +package-lock.json +yarn.lock + +# Generated client schema — leave the latest in repo for reproducible builds, +# regenerate via `pnpm gen:api`. +# src/api/schema.d.ts diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..b194954 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,19 @@ +# syntax=docker/dockerfile:1.7 +FROM oven/bun:1-alpine AS builder +WORKDIR /app + +COPY package.json bun.lock* bun.lockb* ./ +RUN bun install --frozen-lockfile + +COPY tsconfig*.json vite.config.ts index.html ./ +COPY openapi.json ./ +COPY public ./public +COPY src ./src + +RUN bun run gen:api && bun run build + +# Runtime image is intentionally generic — the nginx config is volume-mounted +# from ../deploy/nginx.conf by compose so ops can iterate without rebuilds. +FROM nginx:1.27-alpine AS runtime +COPY --from=builder /app/dist /usr/share/nginx/html +EXPOSE 80 diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..45c0881 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,88 @@ +# smgw-pki-console + +Web-Konsole für den `smgw-pki-automator`. React + TypeScript + Vite. Typisierter +API-Client wird aus der utoipa-generierten OpenAPI-Spezifikation gebaut. + +## Stack + +- React 19 + TypeScript (strict) + Vite 6 +- TanStack Router (file-based) + TanStack Query +- Tailwind v4 + shadcn-Stil Primitives auf Radix +- `openapi-typescript` + `openapi-fetch` für typisierten Backend-Client +- Sonner für Toasts, Lucide-Icons + +## Layout + +``` +src/ +├── api/schema.d.ts Generiert via `just gen-api` (oder `bun run gen:api`) +├── components/ +│ ├── ui/ Button, Card, Badge, Table, Dialog … +│ ├── layout/ Sidebar, Topbar, App-Shell +│ └── state-badge.tsx +├── lib/ +│ ├── api.ts openapi-fetch Client +│ ├── format.ts Datum/Serial-Formatter +│ └── utils.ts cn() +├── routes/ TanStack Router (file-based) +│ ├── __root.tsx +│ ├── login.tsx +│ ├── _app.tsx Auth-Guard + Shell +│ ├── _app.index.tsx Dashboard +│ ├── _app.certificates.tsx +│ ├── _app.configuration.tsx +│ └── _app.iconfig.tsx +├── index.css Tailwind Theme (light, BSI-Ton) +└── main.tsx +``` + +## Lokale Entwicklung + +Bun ist die Standard-Toolchain (ein Binary, kein Approval-Tanz für Build-Skripte). Andere Manager (pnpm, npm) funktionieren grundsätzlich auch. + +```bash +bun install +bun run gen:api # liest ./openapi.json +bun run dev # http://localhost:5173, /api wird zu localhost:8443 gepro­xied +``` + +Backend separat aus `../backend`: + +```bash +DEV_AUTH=1 CORS_ALLOW_ORIGIN=http://localhost:5173 cargo run +``` + +## OpenAPI-Client + +```bash +bun run gen:api # statisch aus ./openapi.json +bun run gen:api:live # gegen laufendes Backend (localhost:8443) +``` + +OpenAPI selbst stammt aus dem Rust-Code via `utoipa-axum`. Frischen Snapshot in +diesem Verzeichnis ablegen: + +```bash +cd ../backend && cargo run -- --emit-openapi > ../frontend/openapi.json +``` + +## Docker (über Repo-Root) + +```bash +just up # beide Container bauen + starten +just down +just logs frontend +``` + +- Frontend: +- Backend (intern): `backend:8443` +- `deploy/nginx.conf` wird zur Laufzeit in den Frontend-Container gemountet — Edit + `just nginx-reload` ohne Image-Rebuild. +- mTLS-Termination findet **vor** nginx statt; der Proxy setzt `X-Forwarded-Cert-Subject`. Im Lab läuft das Backend mit `DEV_AUTH=1` und akzeptiert ein `dev_subject` im Login-Body. + +## Was noch fehlt (TODOs) + +- PEM-Anzeige im Certificate-Detail-Drawer (Backend liefert PEM noch nicht + separat). +- SSE-Stream für Live-Scheduler-Logs (Endpoint `/api/scheduler/stream`). +- Vollständige iconfig-Profil-Felder, sobald `InitialConfigBuilder` real ist. +- Audit-Log-Seite. diff --git a/frontend/bun.lock b/frontend/bun.lock new file mode 100644 index 0000000..28e80d8 --- /dev/null +++ b/frontend/bun.lock @@ -0,0 +1,745 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "smgw-pki-console", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.4", + "@radix-ui/react-dropdown-menu": "^2.1.4", + "@radix-ui/react-label": "^2.1.1", + "@radix-ui/react-popover": "^1.1.4", + "@radix-ui/react-separator": "^1.1.1", + "@radix-ui/react-slot": "^1.1.1", + "@radix-ui/react-switch": "^1.1.2", + "@radix-ui/react-tabs": "^1.1.2", + "@radix-ui/react-tooltip": "^1.1.5", + "@tanstack/react-query": "^5.62.7", + "@tanstack/react-router": "^1.93.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "geist": "^1.3.1", + "lucide-react": "^0.468.0", + "openapi-fetch": "^0.13.4", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "sonner": "^1.7.1", + "tailwind-merge": "^2.5.5", + "zod": "^3.24.1", + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "@tanstack/router-cli": "^1.166.43", + "@tanstack/router-plugin": "^1.93.0", + "@types/node": "^22.10.2", + "@types/react": "^19.0.2", + "@types/react-dom": "^19.0.2", + "@vitejs/plugin-react": "^4.3.4", + "openapi-typescript": "^7.4.4", + "tailwindcss": "^4.0.0", + "typescript": "^5.7.2", + "vite": "^6.0.5", + }, + }, + }, + "packages": { + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "@babel/compat-data": ["@babel/compat-data@7.29.3", "", {}, "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg=="], + + "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], + + "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + + "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="], + + "@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="], + + "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="], + + "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A=="], + + "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], + + "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], + + "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + + "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + + "@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="], + + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.8", "", { "dependencies": { "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], + + "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], + + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], + + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], + + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], + + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], + + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], + + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], + + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], + + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], + + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], + + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], + + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], + + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], + + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], + + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@next/env": ["@next/env@16.2.6", "", {}, "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw=="], + + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg=="], + + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ=="], + + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w=="], + + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA=="], + + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw=="], + + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g=="], + + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg=="], + + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.6", "", { "os": "win32", "cpu": "x64" }, "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA=="], + + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], + + "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], + + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], + + "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], + + "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + + "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="], + + "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], + + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], + + "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw=="], + + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], + + "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], + + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], + + "@radix-ui/react-label": ["@radix-ui/react-label@2.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A=="], + + "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="], + + "@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="], + + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="], + + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], + + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], + + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="], + + "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g=="], + + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], + + "@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="], + + "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="], + + "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="], + + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], + + "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], + + "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], + + "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], + + "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + + "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="], + + "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="], + + "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="], + + "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="], + + "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], + + "@redocly/ajv": ["@redocly/ajv@8.11.2", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js-replace": "^1.0.1" } }, "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg=="], + + "@redocly/config": ["@redocly/config@0.22.0", "", {}, "sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ=="], + + "@redocly/openapi-core": ["@redocly/openapi-core@1.34.14", "", { "dependencies": { "@redocly/ajv": "8.11.2", "@redocly/config": "0.22.0", "colorette": "1.4.0", "https-proxy-agent": "7.0.6", "js-levenshtein": "1.1.6", "js-yaml": "4.1.1", "minimatch": "5.1.9", "pluralize": "8.0.0", "yaml-ast-parser": "0.0.43" } }, "sha512-y+xFx+Zz54Xhr8jUdnLENYnt7Y7GEDL6Q03ga7rTtX8DVwefX9H+hQEPgJp1nda7vdH+wJ9/HBVvyfBuW9x6rA=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.3", "", { "os": "android", "cpu": "arm" }, "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.3", "", { "os": "android", "cpu": "arm64" }, "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.3", "", { "os": "linux", "cpu": "arm" }, "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.3", "", { "os": "linux", "cpu": "arm" }, "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.3", "", { "os": "linux", "cpu": "none" }, "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.3", "", { "os": "linux", "cpu": "none" }, "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.3", "", { "os": "linux", "cpu": "none" }, "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.3", "", { "os": "linux", "cpu": "none" }, "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.3", "", { "os": "linux", "cpu": "x64" }, "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.3", "", { "os": "linux", "cpu": "x64" }, "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.3", "", { "os": "none", "cpu": "arm64" }, "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.3", "", { "os": "win32", "cpu": "x64" }, "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.3", "", { "os": "win32", "cpu": "x64" }, "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA=="], + + "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], + + "@tailwindcss/node": ["@tailwindcss/node@4.3.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.21.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.0" } }, "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g=="], + + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.0", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.0", "@tailwindcss/oxide-darwin-arm64": "4.3.0", "@tailwindcss/oxide-darwin-x64": "4.3.0", "@tailwindcss/oxide-freebsd-x64": "4.3.0", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", "@tailwindcss/oxide-linux-x64-musl": "4.3.0", "@tailwindcss/oxide-wasm32-wasi": "4.3.0", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" } }, "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg=="], + + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.0", "", { "os": "android", "cpu": "arm64" }, "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng=="], + + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ=="], + + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA=="], + + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ=="], + + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0", "", { "os": "linux", "cpu": "arm" }, "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA=="], + + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg=="], + + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ=="], + + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ=="], + + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg=="], + + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.0", "", { "dependencies": { "@emnapi/core": "^1.10.0", "@emnapi/runtime": "^1.10.0", "@emnapi/wasi-threads": "^1.2.1", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA=="], + + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ=="], + + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA=="], + + "@tailwindcss/vite": ["@tailwindcss/vite@4.3.0", "", { "dependencies": { "@tailwindcss/node": "4.3.0", "@tailwindcss/oxide": "4.3.0", "tailwindcss": "4.3.0" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw=="], + + "@tanstack/history": ["@tanstack/history@1.161.6", "", {}, "sha512-NaOGLRrddszbQj9upGat6HG/4TKvXLvu+osAIgfxPYA+eIvYKv8GKDJOrY2D3/U9MRnKfMWD7bU4jeD4xmqyIg=="], + + "@tanstack/query-core": ["@tanstack/query-core@5.100.10", "", {}, "sha512-8UR0yJR+GiQ40m3lPhUr0xbfAupe6GSQiksSBSa9SM2NjezFyxXCIA69/lz8cSoNKZLrw1/PktIyQBJcVeMi3w=="], + + "@tanstack/react-query": ["@tanstack/react-query@5.100.10", "", { "dependencies": { "@tanstack/query-core": "5.100.10" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-FLaZf2RCrA/Zgp4aiu5tG3TyasTRO7aZ99skxQpr3Hg/zXOhu6yq5FZCYQ/tRaJtM9ylnoK8tFK7PolXQadv6Q=="], + + "@tanstack/react-router": ["@tanstack/react-router@1.169.2", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.169.2", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-OJM7Kguc7ERnweaNRWsyWgIKcl3z23rD1B4jaxjzd9RGdnzpt2HfrWa9rggbT0Hfzhfo4D2ZmsfoTme035tniQ=="], + + "@tanstack/react-store": ["@tanstack/react-store@0.9.3", "", { "dependencies": { "@tanstack/store": "0.9.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg=="], + + "@tanstack/router-cli": ["@tanstack/router-cli@1.166.43", "", { "dependencies": { "@tanstack/router-generator": "1.166.42", "chokidar": "^3.6.0", "yargs": "^17.7.2" }, "bin": { "tsr": "bin/tsr.cjs" } }, "sha512-XvKFA47F5KjL0R8PzUdkBQrVDPjSzE7FgWeKnYLVRGytDTlZOJhgVzoznITdiAsNe9KPe93xHE1z5h80hhOGWg=="], + + "@tanstack/router-core": ["@tanstack/router-core@1.169.2", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.4", "seroval-plugins": "^1.5.4" } }, "sha512-5sm0DJF1A7Mz+9gy4Gz/lLovNailK3yot4vYvz9MkBUPw26uLnhQiR8hSCYxucjE0wD6Mdlc5l+Z0/XTlZ7xHw=="], + + "@tanstack/router-generator": ["@tanstack/router-generator@1.166.42", "", { "dependencies": { "@babel/types": "^7.28.5", "@tanstack/router-core": "1.169.2", "@tanstack/router-utils": "1.161.8", "@tanstack/virtual-file-routes": "1.161.7", "jiti": "^2.7.0", "magic-string": "^0.30.21", "prettier": "^3.5.0", "zod": "^3.24.2" } }, "sha512-2qBWC0t78r6b3vI+AbnvCZcFAvbYBDlLuWZrTjQbcjUmwG3qyeQp983tJyDuj9wb5//adG1tgAGXZkJ3aDwdBg=="], + + "@tanstack/router-plugin": ["@tanstack/router-plugin@1.167.35", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.169.2", "@tanstack/router-generator": "1.166.42", "@tanstack/router-utils": "1.161.8", "@tanstack/virtual-file-routes": "1.161.7", "chokidar": "^3.6.0", "unplugin": "^3.0.0", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2 || ^2.0.0", "@tanstack/react-router": "^1.169.2", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0", "vite-plugin-solid": "^2.11.10 || ^3.0.0-0", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-UAScU5VAzLYVY4FML/Cbc5S5TucT4I8Ata05yozGOe4ZfepTKRffA5xWLtD2N+ov5svdv0KTX/kqlZnYPe28mA=="], + + "@tanstack/router-utils": ["@tanstack/router-utils@1.161.8", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "ansis": "^4.1.0", "babel-dead-code-elimination": "^1.0.12", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-xyiLWEKjfBAVhauDSSjXxyf7s8elU6SM+V050sbkofvGmIIvkwPFtDsX7Gvwh14kBd6iCwAT+RiPvXTxAptY0Q=="], + + "@tanstack/store": ["@tanstack/store@0.9.3", "", {}, "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw=="], + + "@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.161.7", "", { "bin": { "intent": "bin/intent.js" } }, "sha512-olW33+Cn+bsCsZKPwEGhlkqS6w3M2slFv11JIobdnCFKMLG97oAI2kWKdx5/zsywTL8flpnoIgaZZPlQTFYhdQ=="], + + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], + + "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], + + "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], + + "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/node": ["@types/node@22.19.19", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew=="], + + "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], + + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + + "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "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" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], + + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "ansis": ["ansis@4.3.0", "", {}, "sha512-44mvgtPvohuU/70DdY5Oz2AIrLJ9k6/5x4KmoSvPwO+5Moijo0+N9D0fKbbYZQWP1hNm5CpOf+E01jhxG/r8xg=="], + + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], + + "babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.29", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ=="], + + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + + "brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "browserslist": ["browserslist@4.28.2", "", { "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" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001792", "", {}, "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw=="], + + "change-case": ["change-case@5.4.4", "", {}, "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w=="], + + "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + + "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], + + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "colorette": ["colorette@1.4.0", "", {}, "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + + "diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.353", "", {}, "sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w=="], + + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "enhanced-resolve": ["enhanced-resolve@5.21.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-QyL119InA+XXEkNLNTPCXPugSvOfhwv0JOlGNzvxs0hZaiHLNvXSpudUWsOlsXGWJh8G6ckCScEkVHfX3kw/2Q=="], + + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "geist": ["geist@1.7.0", "", { "peerDependencies": { "next": ">=13.2.0" } }, "sha512-ZaoiZwkSf0DwwB1ncdLKp+ggAldqxl5L1+SXaNIBGkPAqcu+xjVJLxlf3/S8vLt9UHx1xu5fz3lbzKCj5iOVdQ=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], + + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + + "index-to-position": ["index-to-position@1.2.0", "", {}, "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw=="], + + "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "isbot": ["isbot@5.1.40", "", {}, "sha512-yNeeynhhtIVRBk12tBV4eHNxwB42HzR4Q3Ea7vCOiJhImGaAIdIMrbJtacQlBizGLjUPw+akkFI5Dn9T70XoVQ=="], + + "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], + + "js-levenshtein": ["js-levenshtein@1.1.6", "", {}, "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "lucide-react": ["lucide-react@0.468.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" } }, "sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], + + "next": ["next@16.2.6", "", { "dependencies": { "@next/env": "16.2.6", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.6", "@next/swc-darwin-x64": "16.2.6", "@next/swc-linux-arm64-gnu": "16.2.6", "@next/swc-linux-arm64-musl": "16.2.6", "@next/swc-linux-x64-gnu": "16.2.6", "@next/swc-linux-x64-musl": "16.2.6", "@next/swc-win32-arm64-msvc": "16.2.6", "@next/swc-win32-x64-msvc": "16.2.6", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw=="], + + "node-releases": ["node-releases@2.0.44", "", {}, "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ=="], + + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + + "openapi-fetch": ["openapi-fetch@0.13.8", "", { "dependencies": { "openapi-typescript-helpers": "^0.0.15" } }, "sha512-yJ4QKRyNxE44baQ9mY5+r/kAzZ8yXMemtNAOFwOzRXJscdjSxxzWSNlyBAr+o5JjkUw9Lc3W7OIoca0cY3PYnQ=="], + + "openapi-typescript": ["openapi-typescript@7.13.0", "", { "dependencies": { "@redocly/openapi-core": "^1.34.6", "ansi-colors": "^4.1.3", "change-case": "^5.4.4", "parse-json": "^8.3.0", "supports-color": "^10.2.2", "yargs-parser": "^21.1.1" }, "peerDependencies": { "typescript": "^5.x" }, "bin": { "openapi-typescript": "bin/cli.js" } }, "sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ=="], + + "openapi-typescript-helpers": ["openapi-typescript-helpers@0.0.15", "", {}, "sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw=="], + + "parse-json": ["parse-json@8.3.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "index-to-position": "^1.1.0", "type-fest": "^4.39.1" } }, "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + + "pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="], + + "postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="], + + "prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="], + + "react": ["react@19.2.6", "", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="], + + "react-dom": ["react-dom@19.2.6", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="], + + "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], + + "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], + + "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], + + "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], + + "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "rollup": ["rollup@4.60.3", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.3", "@rollup/rollup-android-arm64": "4.60.3", "@rollup/rollup-darwin-arm64": "4.60.3", "@rollup/rollup-darwin-x64": "4.60.3", "@rollup/rollup-freebsd-arm64": "4.60.3", "@rollup/rollup-freebsd-x64": "4.60.3", "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", "@rollup/rollup-linux-arm-musleabihf": "4.60.3", "@rollup/rollup-linux-arm64-gnu": "4.60.3", "@rollup/rollup-linux-arm64-musl": "4.60.3", "@rollup/rollup-linux-loong64-gnu": "4.60.3", "@rollup/rollup-linux-loong64-musl": "4.60.3", "@rollup/rollup-linux-ppc64-gnu": "4.60.3", "@rollup/rollup-linux-ppc64-musl": "4.60.3", "@rollup/rollup-linux-riscv64-gnu": "4.60.3", "@rollup/rollup-linux-riscv64-musl": "4.60.3", "@rollup/rollup-linux-s390x-gnu": "4.60.3", "@rollup/rollup-linux-x64-gnu": "4.60.3", "@rollup/rollup-linux-x64-musl": "4.60.3", "@rollup/rollup-openbsd-x64": "4.60.3", "@rollup/rollup-openharmony-arm64": "4.60.3", "@rollup/rollup-win32-arm64-msvc": "4.60.3", "@rollup/rollup-win32-ia32-msvc": "4.60.3", "@rollup/rollup-win32-x64-gnu": "4.60.3", "@rollup/rollup-win32-x64-msvc": "4.60.3", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "seroval": ["seroval@1.5.4", "", {}, "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw=="], + + "seroval-plugins": ["seroval-plugins@1.5.4", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw=="], + + "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], + + "sonner": ["sonner@1.7.4", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], + + "supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], + + "tailwind-merge": ["tailwind-merge@2.6.1", "", {}, "sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ=="], + + "tailwindcss": ["tailwindcss@4.3.0", "", {}, "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q=="], + + "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], + + "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "unplugin": ["unplugin@3.0.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg=="], + + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + + "uri-js-replace": ["uri-js-replace@1.0.1", "", {}, "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g=="], + + "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], + + "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + + "vite": ["vite@6.4.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ=="], + + "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], + + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "yaml-ast-parser": ["yaml-ast-parser@0.0.43", "", {}, "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A=="], + + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-label/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], + + "@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-popover/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-separator/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], + + "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], + + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], + + "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], + + "readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "sharp/semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], + } +} diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..ce3242e --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + + SMGW PKI · Console + + +
+ + + diff --git a/frontend/openapi.json b/frontend/openapi.json new file mode 100644 index 0000000..c38173f --- /dev/null +++ b/frontend/openapi.json @@ -0,0 +1,949 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "smgw-pki-automator", + "description": "Control + observation surface for the SMGW PKI automation tool. Test/lab use only.", + "license": { + "name": "" + }, + "version": "0.1.0" + }, + "paths": { + "/alerts": { + "get": { + "tags": [ + "alerts" + ], + "operationId": "list_alerts", + "responses": { + "200": { + "description": "Recent alerts", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AlertListResponse" + } + } + } + } + } + } + }, + "/alerts/test": { + "post": { + "tags": [ + "alerts" + ], + "operationId": "send_test_alert", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TestAlertRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Sent (or stub-logged)" + } + } + } + }, + "/auth/me": { + "get": { + "tags": [ + "auth" + ], + "summary": "Inspect the current session.", + "operationId": "whoami", + "responses": { + "200": { + "description": "Current session", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionResponse" + } + } + } + }, + "401": { + "description": "No active session", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/auth/session": { + "post": { + "tags": [ + "auth" + ], + "summary": "Exchange mTLS client cert (passed via `X-Forwarded-Cert-Subject`) for a\nserver-issued session cookie. The reverse proxy terminating mTLS is the\ntrust anchor.", + "operationId": "create_session", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoginRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Session issued", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionResponse" + } + } + } + }, + "403": { + "description": "No client cert subject", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + }, + "delete": { + "tags": [ + "auth" + ], + "summary": "Revoke the current session and clear cookie.", + "operationId": "end_session", + "responses": { + "204": { + "description": "Session ended" + } + } + } + }, + "/certs": { + "get": { + "tags": [ + "certs" + ], + "summary": "List all known end-entity certificates with derived state.", + "operationId": "list_certificates", + "responses": { + "200": { + "description": "Certificates", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CertListResponse" + } + } + } + } + } + } + }, + "/certs/{gateway_id}/{usage}/renew": { + "post": { + "tags": [ + "certs" + ], + "summary": "Trigger an out-of-band renewal for a specific (gateway, usage) pair.\nReturns the SOAP `messageID` so the caller can correlate the async callback.", + "operationId": "renew_certificate", + "parameters": [ + { + "name": "gateway_id", + "in": "path", + "description": "Gateway identifier", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "usage", + "in": "path", + "description": "Certificate usage", + "required": true, + "schema": { + "$ref": "#/components/schemas/CertificateUsageDto" + } + } + ], + "responses": { + "202": { + "description": "Renewal accepted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RenewAccepted" + } + } + } + }, + "501": { + "description": "Sub-CA adapter not implemented yet", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/config": { + "get": { + "tags": [ + "config" + ], + "operationId": "get_config", + "responses": { + "200": { + "description": "Current runtime config", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConfigView" + } + } + } + } + } + }, + "put": { + "tags": [ + "config" + ], + "operationId": "update_config", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConfigUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Updated runtime config", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConfigView" + } + } + } + } + } + } + }, + "/gateways": { + "get": { + "tags": [ + "gateways" + ], + "operationId": "list_gateways", + "responses": { + "200": { + "description": "Gateways", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GatewayListResponse" + } + } + } + } + } + } + }, + "/iconfig/build": { + "post": { + "tags": [ + "iconfig" + ], + "summary": "Build, sign via HSM, and stream back `iconfig.tar`.", + "operationId": "build_iconfig", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IconfigRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "iconfig.tar", + "content": { + "application/x-tar": {} + } + }, + "501": { + "description": "HSM signature not implemented", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + }, + "/iconfig/preview": { + "post": { + "tags": [ + "iconfig" + ], + "summary": "Render the unsigned `iconfig.xml` for review. Does not touch the HSM.", + "operationId": "preview_iconfig", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IconfigRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Preview XML", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IconfigPreview" + } + } + } + } + } + } + }, + "/scheduler": { + "get": { + "tags": [ + "scheduler" + ], + "operationId": "get_status", + "responses": { + "200": { + "description": "Scheduler state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SchedulerState" + } + } + } + } + } + } + }, + "/scheduler/pause": { + "post": { + "tags": [ + "scheduler" + ], + "operationId": "set_paused", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PauseRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Pause state updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SchedulerState" + } + } + } + } + } + } + }, + "/scheduler/trigger": { + "post": { + "tags": [ + "scheduler" + ], + "summary": "Run renewal once, out of band. Honours the same overlap-lock as the cron job.", + "operationId": "trigger_run", + "responses": { + "202": { + "description": "Run accepted" + }, + "409": { + "description": "Run already in progress", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "AlertEntry": { + "type": "object", + "required": [ + "at", + "severity", + "subject", + "body" + ], + "properties": { + "at": { + "type": "string", + "format": "date-time" + }, + "body": { + "type": "string" + }, + "severity": { + "$ref": "#/components/schemas/AlertSeverity" + }, + "subject": { + "type": "string" + } + } + }, + "AlertListResponse": { + "type": "object", + "required": [ + "items" + ], + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AlertEntry" + } + } + } + }, + "AlertSeverity": { + "type": "string", + "enum": [ + "info", + "warning", + "error" + ] + }, + "ApiError": { + "type": "object", + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "CertListResponse": { + "type": "object", + "required": [ + "items" + ], + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CertificateDto" + } + } + } + }, + "CertState": { + "type": "string", + "enum": [ + "valid", + "expiring", + "expired" + ] + }, + "CertificateDto": { + "type": "object", + "required": [ + "gateway_id", + "serial", + "usage", + "not_before", + "not_after", + "days_to_expiry", + "state" + ], + "properties": { + "days_to_expiry": { + "type": "integer", + "format": "int64" + }, + "gateway_id": { + "type": "string" + }, + "not_after": { + "type": "string", + "format": "date-time" + }, + "not_before": { + "type": "string", + "format": "date-time" + }, + "serial": { + "type": "string" + }, + "state": { + "$ref": "#/components/schemas/CertState" + }, + "usage": { + "$ref": "#/components/schemas/CertificateUsageDto" + } + } + }, + "CertificateUsageDto": { + "type": "string", + "enum": [ + "tls", + "signature", + "encryption" + ] + }, + "ConfigUpdate": { + "type": "object", + "properties": { + "cron_schedule": { + "type": [ + "string", + "null" + ] + }, + "days_window": { + "type": [ + "integer", + "null" + ], + "format": "int32", + "minimum": 0 + }, + "hsm": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/HsmConfig" + } + ] + }, + "smtp": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SmtpConfig" + } + ] + }, + "sub_ca": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SubCaConfig" + } + ] + } + } + }, + "ConfigView": { + "type": "object", + "required": [ + "config", + "restart_required_fields" + ], + "properties": { + "config": { + "$ref": "#/components/schemas/RuntimeConfig" + }, + "restart_required_fields": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "GatewayDto": { + "type": "object", + "required": [ + "id", + "serial_number", + "admin_key_label" + ], + "properties": { + "admin_key_label": { + "type": "string" + }, + "id": { + "type": "string" + }, + "serial_number": { + "type": "string" + } + } + }, + "GatewayListResponse": { + "type": "object", + "required": [ + "items" + ], + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GatewayDto" + } + } + } + }, + "HsmConfig": { + "type": "object", + "required": [ + "module_path", + "pin_env_var" + ], + "properties": { + "module_path": { + "type": "string" + }, + "pin_env_var": { + "type": "string" + }, + "slot": { + "type": [ + "integer", + "null" + ], + "format": "int64", + "minimum": 0 + } + } + }, + "IconfigPreview": { + "type": "object", + "required": [ + "xml" + ], + "properties": { + "xml": { + "type": "string" + } + } + }, + "IconfigRequest": { + "type": "object", + "required": [ + "gateway_id", + "admin_key_label", + "profile" + ], + "properties": { + "admin_key_label": { + "type": "string" + }, + "extras": {}, + "gateway_id": { + "type": "string" + }, + "profile": { + "type": "string" + } + } + }, + "LoginRequest": { + "type": "object", + "properties": { + "dev_subject": { + "type": [ + "string", + "null" + ], + "description": "Optional fallback subject for dev mode when no mTLS header is present." + } + } + }, + "PauseRequest": { + "type": "object", + "required": [ + "paused" + ], + "properties": { + "paused": { + "type": "boolean" + } + } + }, + "RenewAccepted": { + "type": "object", + "required": [ + "message_id" + ], + "properties": { + "message_id": { + "type": "string" + } + } + }, + "RuntimeConfig": { + "type": "object", + "description": "Mutable runtime config. Seeded from env on boot; UI may override at runtime\nfor fields flagged `hot_reload`. Restart-only fields (BIND_ADDR) are read\nbut cannot be applied without restart.", + "required": [ + "bind_addr", + "cron_schedule", + "days_window", + "database_url", + "sub_ca", + "smtp", + "hsm" + ], + "properties": { + "bind_addr": { + "type": "string" + }, + "cron_schedule": { + "type": "string" + }, + "database_url": { + "type": "string" + }, + "days_window": { + "type": "integer", + "format": "int32", + "minimum": 0 + }, + "hsm": { + "$ref": "#/components/schemas/HsmConfig" + }, + "smtp": { + "$ref": "#/components/schemas/SmtpConfig" + }, + "sub_ca": { + "$ref": "#/components/schemas/SubCaConfig" + } + } + }, + "SchedulerState": { + "type": "object", + "required": [ + "cron_schedule", + "days_window", + "paused" + ], + "properties": { + "cron_schedule": { + "type": "string" + }, + "days_window": { + "type": "integer", + "format": "int32", + "minimum": 0 + }, + "last_error": { + "type": [ + "string", + "null" + ] + }, + "last_handled": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "last_run_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "last_run_ok": { + "type": [ + "boolean", + "null" + ] + }, + "paused": { + "type": "boolean" + } + } + }, + "SessionResponse": { + "type": "object", + "required": [ + "subject", + "expires_at" + ], + "properties": { + "expires_at": { + "type": "string", + "format": "date-time" + }, + "subject": { + "type": "string" + } + } + }, + "SmtpConfig": { + "type": "object", + "required": [ + "host", + "port", + "from", + "to", + "starttls" + ], + "properties": { + "from": { + "type": "string" + }, + "host": { + "type": "string" + }, + "port": { + "type": "integer", + "format": "int32", + "minimum": 0 + }, + "starttls": { + "type": "boolean" + }, + "to": { + "type": "string" + } + } + }, + "SubCaConfig": { + "type": "object", + "required": [ + "endpoint" + ], + "properties": { + "ca_bundle_path": { + "type": [ + "string", + "null" + ] + }, + "client_cert_path": { + "type": [ + "string", + "null" + ] + }, + "client_key_path": { + "type": [ + "string", + "null" + ] + }, + "endpoint": { + "type": "string" + } + } + }, + "TestAlertRequest": { + "type": "object", + "properties": { + "body": { + "type": [ + "string", + "null" + ] + }, + "subject": { + "type": [ + "string", + "null" + ] + } + } + } + } + }, + "tags": [ + { + "name": "auth", + "description": "mTLS-bridged session management" + }, + { + "name": "certs", + "description": "Certificate lifecycle" + }, + { + "name": "gateways", + "description": "Smart Meter Gateways" + }, + { + "name": "config", + "description": "Runtime configuration" + }, + { + "name": "scheduler", + "description": "Renewal scheduler" + }, + { + "name": "iconfig", + "description": "BSI TR-03109-1 initial config" + }, + { + "name": "alerts", + "description": "Operator alerts" + } + ] +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..83e0943 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,52 @@ +{ + "name": "smgw-pki-console", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview --host", + "typecheck": "tsc -b --noEmit", + "lint": "eslint .", + "gen:api": "openapi-typescript ./openapi.json -o ./src/api/schema.d.ts", + "gen:api:live": "openapi-typescript http://localhost:8443/api/openapi.json -o ./src/api/schema.d.ts" + }, + "dependencies": { + "@radix-ui/react-dialog": "^1.1.4", + "@radix-ui/react-dropdown-menu": "^2.1.4", + "@radix-ui/react-label": "^2.1.1", + "@radix-ui/react-popover": "^1.1.4", + "@radix-ui/react-separator": "^1.1.1", + "@radix-ui/react-slot": "^1.1.1", + "@radix-ui/react-switch": "^1.1.2", + "@radix-ui/react-tabs": "^1.1.2", + "@radix-ui/react-tooltip": "^1.1.5", + "@tanstack/react-query": "^5.62.7", + "@tanstack/react-router": "^1.93.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "geist": "^1.3.1", + "lucide-react": "^0.468.0", + "openapi-fetch": "^0.13.4", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "sonner": "^1.7.1", + "tailwind-merge": "^2.5.5", + "zod": "^3.24.1" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "@tanstack/router-cli": "^1.166.43", + "@tanstack/router-plugin": "^1.93.0", + "@types/node": "^22.10.2", + "@types/react": "^19.0.2", + "@types/react-dom": "^19.0.2", + "@vitejs/plugin-react": "^4.3.4", + "openapi-typescript": "^7.4.4", + "tailwindcss": "^4.0.0", + "typescript": "^5.7.2", + "vite": "^6.0.5" + } +} diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..2c4c763 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/frontend/src/api/schema.d.ts b/frontend/src/api/schema.d.ts new file mode 100644 index 0000000..c22bd4a --- /dev/null +++ b/frontend/src/api/schema.d.ts @@ -0,0 +1,740 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/alerts": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["list_alerts"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/alerts/test": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["send_test_alert"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/me": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Inspect the current session. */ + get: operations["whoami"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/session": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Exchange mTLS client cert (passed via `X-Forwarded-Cert-Subject`) for a + * server-issued session cookie. The reverse proxy terminating mTLS is the + * trust anchor. + */ + post: operations["create_session"]; + /** Revoke the current session and clear cookie. */ + delete: operations["end_session"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/certs": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List all known end-entity certificates with derived state. */ + get: operations["list_certificates"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/certs/{gateway_id}/{usage}/renew": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Trigger an out-of-band renewal for a specific (gateway, usage) pair. + * Returns the SOAP `messageID` so the caller can correlate the async callback. + */ + post: operations["renew_certificate"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/config": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get_config"]; + put: operations["update_config"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/gateways": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["list_gateways"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/iconfig/build": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Build, sign via HSM, and stream back `iconfig.tar`. */ + post: operations["build_iconfig"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/iconfig/preview": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Render the unsigned `iconfig.xml` for review. Does not touch the HSM. */ + post: operations["preview_iconfig"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/scheduler": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get_status"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/scheduler/pause": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["set_paused"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/scheduler/trigger": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Run renewal once, out of band. Honours the same overlap-lock as the cron job. */ + post: operations["trigger_run"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + AlertEntry: { + /** Format: date-time */ + at: string; + body: string; + severity: components["schemas"]["AlertSeverity"]; + subject: string; + }; + AlertListResponse: { + items: components["schemas"]["AlertEntry"][]; + }; + /** @enum {string} */ + AlertSeverity: "info" | "warning" | "error"; + ApiError: { + code: string; + message: string; + }; + CertListResponse: { + items: components["schemas"]["CertificateDto"][]; + }; + /** @enum {string} */ + CertState: "valid" | "expiring" | "expired"; + CertificateDto: { + /** Format: int64 */ + days_to_expiry: number; + gateway_id: string; + /** Format: date-time */ + not_after: string; + /** Format: date-time */ + not_before: string; + serial: string; + state: components["schemas"]["CertState"]; + usage: components["schemas"]["CertificateUsageDto"]; + }; + /** @enum {string} */ + CertificateUsageDto: "tls" | "signature" | "encryption"; + ConfigUpdate: { + cron_schedule?: string | null; + /** Format: int32 */ + days_window?: number | null; + hsm?: null | components["schemas"]["HsmConfig"]; + smtp?: null | components["schemas"]["SmtpConfig"]; + sub_ca?: null | components["schemas"]["SubCaConfig"]; + }; + ConfigView: { + config: components["schemas"]["RuntimeConfig"]; + restart_required_fields: string[]; + }; + GatewayDto: { + admin_key_label: string; + id: string; + serial_number: string; + }; + GatewayListResponse: { + items: components["schemas"]["GatewayDto"][]; + }; + HsmConfig: { + module_path: string; + pin_env_var: string; + /** Format: int64 */ + slot?: number | null; + }; + IconfigPreview: { + xml: string; + }; + IconfigRequest: { + admin_key_label: string; + extras?: unknown; + gateway_id: string; + profile: string; + }; + LoginRequest: { + /** @description Optional fallback subject for dev mode when no mTLS header is present. */ + dev_subject?: string | null; + }; + PauseRequest: { + paused: boolean; + }; + RenewAccepted: { + message_id: string; + }; + /** + * @description Mutable runtime config. Seeded from env on boot; UI may override at runtime + * for fields flagged `hot_reload`. Restart-only fields (BIND_ADDR) are read + * but cannot be applied without restart. + */ + RuntimeConfig: { + bind_addr: string; + cron_schedule: string; + database_url: string; + /** Format: int32 */ + days_window: number; + hsm: components["schemas"]["HsmConfig"]; + smtp: components["schemas"]["SmtpConfig"]; + sub_ca: components["schemas"]["SubCaConfig"]; + }; + SchedulerState: { + cron_schedule: string; + /** Format: int32 */ + days_window: number; + last_error?: string | null; + last_handled?: number | null; + /** Format: date-time */ + last_run_at?: string | null; + last_run_ok?: boolean | null; + paused: boolean; + }; + SessionResponse: { + /** Format: date-time */ + expires_at: string; + subject: string; + }; + SmtpConfig: { + from: string; + host: string; + /** Format: int32 */ + port: number; + starttls: boolean; + to: string; + }; + SubCaConfig: { + ca_bundle_path?: string | null; + client_cert_path?: string | null; + client_key_path?: string | null; + endpoint: string; + }; + TestAlertRequest: { + body?: string | null; + subject?: string | null; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + list_alerts: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Recent alerts */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AlertListResponse"]; + }; + }; + }; + }; + send_test_alert: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TestAlertRequest"]; + }; + }; + responses: { + /** @description Sent (or stub-logged) */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + whoami: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Current session */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionResponse"]; + }; + }; + /** @description No active session */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiError"]; + }; + }; + }; + }; + create_session: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["LoginRequest"]; + }; + }; + responses: { + /** @description Session issued */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionResponse"]; + }; + }; + /** @description No client cert subject */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiError"]; + }; + }; + }; + }; + end_session: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Session ended */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + list_certificates: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Certificates */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CertListResponse"]; + }; + }; + }; + }; + renew_certificate: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Gateway identifier */ + gateway_id: string; + /** @description Certificate usage */ + usage: components["schemas"]["CertificateUsageDto"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Renewal accepted */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["RenewAccepted"]; + }; + }; + /** @description Sub-CA adapter not implemented yet */ + 501: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiError"]; + }; + }; + }; + }; + get_config: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Current runtime config */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ConfigView"]; + }; + }; + }; + }; + update_config: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ConfigUpdate"]; + }; + }; + responses: { + /** @description Updated runtime config */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ConfigView"]; + }; + }; + }; + }; + list_gateways: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Gateways */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GatewayListResponse"]; + }; + }; + }; + }; + build_iconfig: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["IconfigRequest"]; + }; + }; + responses: { + /** @description iconfig.tar */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/x-tar": unknown; + }; + }; + /** @description HSM signature not implemented */ + 501: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiError"]; + }; + }; + }; + }; + preview_iconfig: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["IconfigRequest"]; + }; + }; + responses: { + /** @description Preview XML */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["IconfigPreview"]; + }; + }; + }; + }; + get_status: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Scheduler state */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SchedulerState"]; + }; + }; + }; + }; + set_paused: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PauseRequest"]; + }; + }; + responses: { + /** @description Pause state updated */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SchedulerState"]; + }; + }; + }; + }; + trigger_run: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Run accepted */ + 202: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Run already in progress */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiError"]; + }; + }; + }; + }; +} diff --git a/frontend/src/components/layout/sidebar.tsx b/frontend/src/components/layout/sidebar.tsx new file mode 100644 index 0000000..29db8b3 --- /dev/null +++ b/frontend/src/components/layout/sidebar.tsx @@ -0,0 +1,94 @@ +import { Link, useRouterState } from "@tanstack/react-router"; +import { + LayoutGrid, + ShieldCheck, + Sliders, + FileLock2, + CircleDot, +} from "lucide-react"; +import { cn } from "@/lib/utils"; + +type NavItem = { + to: string; + label: string; + hint: string; + icon: React.ComponentType<{ className?: string }>; +}; + +const NAV: NavItem[] = [ + { to: "/", label: "Übersicht", hint: "Status der PKI", icon: LayoutGrid }, + { to: "/certificates", label: "Zertifikate", hint: "Bestand & Erneuerung", icon: ShieldCheck }, + { to: "/configuration", label: "Konfiguration", hint: "Runtime-Parameter", icon: Sliders }, + { to: "/iconfig", label: "iconfig.tar", hint: "TR-03109-1 Builder", icon: FileLock2 }, +]; + +export function Sidebar() { + const path = useRouterState({ select: (s) => s.location.pathname }); + + return ( + + ); +} diff --git a/frontend/src/components/layout/topbar.tsx b/frontend/src/components/layout/topbar.tsx new file mode 100644 index 0000000..a83a652 --- /dev/null +++ b/frontend/src/components/layout/topbar.tsx @@ -0,0 +1,84 @@ +import { useMutation, useQuery } from "@tanstack/react-query"; +import { useNavigate } from "@tanstack/react-router"; +import { toast } from "sonner"; +import { LogOut, Play, ShieldHalf } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { api, unwrap } from "@/lib/api"; +import { fmtRelative } from "@/lib/format"; + +export function Topbar({ title, subtitle }: { title: string; subtitle?: string }) { + const navigate = useNavigate(); + + const me = useQuery({ + queryKey: ["auth", "me"], + queryFn: async () => { + try { + return await unwrap(api.GET("/auth/me")); + } catch { + return null; + } + }, + }); + + const trigger = useMutation({ + mutationFn: () => unwrap(api.POST("/scheduler/trigger")), + onSuccess: () => toast.success("Erneuerungslauf gestartet"), + onError: (e: Error) => toast.error(`Trigger fehlgeschlagen: ${e.message}`), + }); + + const logout = useMutation({ + mutationFn: () => unwrap(api.DELETE("/auth/session")), + onSuccess: () => navigate({ to: "/login" }), + }); + + return ( +
+
+
+

+ {title} +

+ {subtitle && ( + {subtitle} + )} +
+ +
+ + +
+ +
+ + {me.data?.subject ?? "—"} + + + Sitzung läuft ab {fmtRelative(me.data?.expires_at)} + +
+ mTLS +
+ + +
+
+
+ ); +} diff --git a/frontend/src/components/state-badge.tsx b/frontend/src/components/state-badge.tsx new file mode 100644 index 0000000..521acbe --- /dev/null +++ b/frontend/src/components/state-badge.tsx @@ -0,0 +1,25 @@ +import { Badge } from "@/components/ui/badge"; + +export type CertState = "valid" | "expiring" | "expired" | "pending"; + +const STATE_TONE: Record = { + valid: "success", + expiring: "warn", + expired: "danger", + pending: "neutral", +}; + +const STATE_LABEL: Record = { + valid: "Gültig", + expiring: "Erneuerung fällig", + expired: "Abgelaufen", + pending: "Ausstehend", +}; + +export function StateBadge({ state }: { state: CertState }) { + return ( + + {STATE_LABEL[state]} + + ); +} diff --git a/frontend/src/components/ui/badge.tsx b/frontend/src/components/ui/badge.tsx new file mode 100644 index 0000000..a3be78e --- /dev/null +++ b/frontend/src/components/ui/badge.tsx @@ -0,0 +1,32 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "@/lib/utils"; + +const badgeVariants = cva( + "inline-flex items-center gap-1.5 rounded-full px-2.5 h-[22px] text-[11px] font-medium tracking-[0.01em] uppercase border", + { + variants: { + tone: { + neutral: "bg-overlay text-ink-mute border-line", + accent: "bg-accent-soft text-accent border-accent-line", + success: "bg-success-soft text-success border-success/30", + warn: "bg-warn-soft text-warn border-warn/30", + danger: "bg-danger-soft text-danger border-danger/30", + outline: "bg-paper text-ink-mute border-line", + }, + dot: { + true: "before:content-[''] before:w-1.5 before:h-1.5 before:rounded-full before:bg-current before:opacity-90", + false: "", + }, + }, + defaultVariants: { tone: "neutral", dot: false }, + }, +); + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +export function Badge({ className, tone, dot, ...props }: BadgeProps) { + return ; +} diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx new file mode 100644 index 0000000..8d24a7f --- /dev/null +++ b/frontend/src/components/ui/button.tsx @@ -0,0 +1,54 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "@/lib/utils"; + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-[8px] text-[13px] font-medium tracking-[-0.005em] transition-[background,box-shadow,transform] duration-100 disabled:pointer-events-none disabled:opacity-50 focus-ring active:scale-[0.985]", + { + variants: { + variant: { + primary: + "bg-ink text-paper hover:bg-ink/90 shadow-[inset_0_1px_0_rgba(255,255,255,0.08),0_1px_0_rgba(0,0,0,0.2)]", + accent: + "bg-accent text-paper hover:bg-accent/90 shadow-[inset_0_1px_0_rgba(255,255,255,0.16),0_1px_0_rgba(0,0,0,0.18)]", + outline: + "bg-paper text-ink border border-line hover:bg-overlay hover:border-line-strong", + ghost: "text-ink-mute hover:bg-overlay hover:text-ink", + soft: "bg-overlay text-ink hover:bg-line/60", + danger: + "bg-danger text-paper hover:bg-danger/90 shadow-[inset_0_1px_0_rgba(255,255,255,0.16)]", + link: "text-accent underline-offset-4 hover:underline px-0 h-auto", + }, + size: { + sm: "h-7 px-2.5 text-[12px]", + md: "h-9 px-3.5", + lg: "h-10 px-5 text-[14px]", + icon: "h-9 w-9", + }, + }, + defaultVariants: { variant: "primary", size: "md" }, + }, +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +export const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ( + + ); + }, +); +Button.displayName = "Button"; + +export { buttonVariants }; diff --git a/frontend/src/components/ui/card.tsx b/frontend/src/components/ui/card.tsx new file mode 100644 index 0000000..8ececa2 --- /dev/null +++ b/frontend/src/components/ui/card.tsx @@ -0,0 +1,44 @@ +import * as React from "react"; +import { cn } from "@/lib/utils"; + +export const Card = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +Card.displayName = "Card"; + +export const CardHeader = ({ className, ...p }: React.HTMLAttributes) => ( +
+); + +export const CardTitle = ({ className, ...p }: React.HTMLAttributes) => ( +

+); + +export const CardDescription = ({ + className, + ...p +}: React.HTMLAttributes) => ( +

+); + +export const CardBody = ({ className, ...p }: React.HTMLAttributes) => ( +

+); + +export const CardFooter = ({ className, ...p }: React.HTMLAttributes) => ( +
+); diff --git a/frontend/src/components/ui/dialog.tsx b/frontend/src/components/ui/dialog.tsx new file mode 100644 index 0000000..23d150e --- /dev/null +++ b/frontend/src/components/ui/dialog.tsx @@ -0,0 +1,97 @@ +import * as React from "react"; +import * as RDialog from "@radix-ui/react-dialog"; +import { X } from "lucide-react"; +import { cn } from "@/lib/utils"; + +export const Dialog = RDialog.Root; +export const DialogTrigger = RDialog.Trigger; +export const DialogClose = RDialog.Close; + +export const DialogPortal = ({ children, ...props }: RDialog.DialogPortalProps) => ( + {children} +); + +export const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = "DialogOverlay"; + +export const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Schließen + + + +)); +DialogContent.displayName = "DialogContent"; + +export const DialogHeader = ({ className, ...p }: React.HTMLAttributes) => ( +
+); + +export const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = "DialogTitle"; + +export const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = "DialogDescription"; + +export const DialogBody = ({ className, ...p }: React.HTMLAttributes) => ( +
+); + +export const DialogFooter = ({ className, ...p }: React.HTMLAttributes) => ( +
+); diff --git a/frontend/src/components/ui/input.tsx b/frontend/src/components/ui/input.tsx new file mode 100644 index 0000000..63f244e --- /dev/null +++ b/frontend/src/components/ui/input.tsx @@ -0,0 +1,24 @@ +import * as React from "react"; +import { cn } from "@/lib/utils"; + +export interface InputProps extends React.InputHTMLAttributes { + mono?: boolean; +} + +export const Input = React.forwardRef( + ({ className, mono, ...props }, ref) => ( + + ), +); +Input.displayName = "Input"; diff --git a/frontend/src/components/ui/label.tsx b/frontend/src/components/ui/label.tsx new file mode 100644 index 0000000..17901d8 --- /dev/null +++ b/frontend/src/components/ui/label.tsx @@ -0,0 +1,18 @@ +import * as React from "react"; +import * as RLabel from "@radix-ui/react-label"; +import { cn } from "@/lib/utils"; + +export const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Label.displayName = "Label"; diff --git a/frontend/src/components/ui/separator.tsx b/frontend/src/components/ui/separator.tsx new file mode 100644 index 0000000..329cb12 --- /dev/null +++ b/frontend/src/components/ui/separator.tsx @@ -0,0 +1,21 @@ +import * as React from "react"; +import * as RSep from "@radix-ui/react-separator"; +import { cn } from "@/lib/utils"; + +export const Separator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => ( + +)); +Separator.displayName = "Separator"; diff --git a/frontend/src/components/ui/skeleton.tsx b/frontend/src/components/ui/skeleton.tsx new file mode 100644 index 0000000..c29e6b1 --- /dev/null +++ b/frontend/src/components/ui/skeleton.tsx @@ -0,0 +1,10 @@ +import { cn } from "@/lib/utils"; + +export function Skeleton({ className, ...props }: React.HTMLAttributes) { + return ( +
+ ); +} diff --git a/frontend/src/components/ui/switch.tsx b/frontend/src/components/ui/switch.tsx new file mode 100644 index 0000000..0d9bfe4 --- /dev/null +++ b/frontend/src/components/ui/switch.tsx @@ -0,0 +1,27 @@ +import * as React from "react"; +import * as RSwitch from "@radix-ui/react-switch"; +import { cn } from "@/lib/utils"; + +export const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +Switch.displayName = "Switch"; diff --git a/frontend/src/components/ui/table.tsx b/frontend/src/components/ui/table.tsx new file mode 100644 index 0000000..0548186 --- /dev/null +++ b/frontend/src/components/ui/table.tsx @@ -0,0 +1,64 @@ +import * as React from "react"; +import { cn } from "@/lib/utils"; + +export function Table({ className, ...p }: React.HTMLAttributes) { + return ( +
+ + + ); +} + +export function THead({ className, ...p }: React.HTMLAttributes) { + return ( + + ); +} + +export function TBody({ className, ...p }: React.HTMLAttributes) { + return ( + + ); +} + +export function TR({ className, ...p }: React.HTMLAttributes) { + return ( + + ); +} + +export function TH({ className, ...p }: React.ThHTMLAttributes) { + return ( + + ); +} + +function TableSkeleton() { + return ( + <> + {[0, 1, 2, 3].map((i) => ( + + {Array.from({ length: 7 }).map((_, j) => ( + + ))} + + ))} + + ); +} + +const LABEL_USAGE: Record = { + tls: "TLS", + signature: "Signatur", + encryption: "Verschlüsselung", +}; +const LABEL_STATE: Record<"all" | CertState, string> = { + all: "Alle", + valid: "Gültig", + expiring: "Erneuerung", + expired: "Abgelaufen", + pending: "Ausstehend", +}; + +function compareBy( + key: SortKey, + a: Schemas["CertificateDto"], + b: Schemas["CertificateDto"], +): number { + switch (key) { + case "gateway": + return a.gateway_id.localeCompare(b.gateway_id); + case "usage": + return a.usage.localeCompare(b.usage); + case "not_after": + return a.not_after.localeCompare(b.not_after); + case "state": + return STATE_ORDER[a.state] - STATE_ORDER[b.state]; + } +} + +const STATE_ORDER: Record = { + expired: 0, + expiring: 1, + valid: 2, +}; + +function toggleSort( + next: SortKey, + current: SortKey, + dir: "asc" | "desc", + setKey: (k: SortKey) => void, + setDir: (d: "asc" | "desc") => void, +) { + if (current === next) { + setDir(dir === "asc" ? "desc" : "asc"); + } else { + setKey(next); + setDir("asc"); + } +} + +async function copySerial(serial: string) { + try { + await navigator.clipboard.writeText(serial); + toast.success("Serial kopiert"); + } catch { + toast.error("Kopieren fehlgeschlagen"); + } +} diff --git a/frontend/src/routes/_app.configuration.tsx b/frontend/src/routes/_app.configuration.tsx new file mode 100644 index 0000000..9599dab --- /dev/null +++ b/frontend/src/routes/_app.configuration.tsx @@ -0,0 +1,457 @@ +import { useEffect, useState } from "react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { createFileRoute } from "@tanstack/react-router"; +import { toast } from "sonner"; +import { + Cable, + Cpu, + Mailbox, + RotateCcw, + Save, + Settings2, + TimerReset, +} from "lucide-react"; + +import { Topbar } from "@/components/layout/topbar"; +import { Card, CardBody, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Switch } from "@/components/ui/switch"; +import { Separator } from "@/components/ui/separator"; +import { api, unwrap, type Schemas, ApiError } from "@/lib/api"; +import { cn } from "@/lib/utils"; + +export const Route = createFileRoute("/_app/configuration")({ + component: ConfigurationPage, +}); + +function ConfigurationPage() { + const qc = useQueryClient(); + const view = useQuery({ + queryKey: ["config"], + queryFn: () => unwrap(api.GET("/config")), + }); + + const [draft, setDraft] = useState(null); + useEffect(() => { + if (view.data && !draft) setDraft(view.data.config); + }, [view.data, draft]); + + const save = useMutation({ + mutationFn: (body: Schemas["ConfigUpdate"]) => + unwrap(api.PUT("/config", { body })), + onSuccess: (data) => { + toast.success("Konfiguration gespeichert"); + qc.setQueryData(["config"], data); + setDraft(data.config); + }, + onError: (e: ApiError) => + toast.error("Speichern fehlgeschlagen", { description: e.message }), + }); + + const testAlert = useMutation({ + mutationFn: () => + unwrap( + api.POST("/alerts/test", { + body: { subject: "SMTP-Test", body: "Konsole → Mail-Adapter" }, + }), + ), + onSuccess: () => toast.success("Test-Alert gesendet"), + onError: (e: ApiError) => toast.error("Test fehlgeschlagen", { description: e.message }), + }); + + if (!draft || !view.data) { + return ( + <> + +
+
+ Bereite Formular vor. +
+
+ + ); + } + + const dirty = JSON.stringify(draft) !== JSON.stringify(view.data.config); + const restartFields = new Set(view.data.restart_required_fields); + + const patch: Schemas["ConfigUpdate"] = { + cron_schedule: draft.cron_schedule, + days_window: draft.days_window, + sub_ca: draft.sub_ca, + smtp: draft.smtp, + hsm: draft.hsm, + }; + + return ( + <> + +
+ + + + + setDraft({ ...draft, cron_schedule: e.target.value }) + } + /> + Sekunde Minute Stunde Tag Monat Wochentag. + + + + setDraft({ ...draft, days_window: Number(e.target.value) }) + } + /> + Default 30. SM-PKI CP empfiehlt frühe Erneuerung. + + + + + + + + + setDraft({ + ...draft, + sub_ca: { ...draft.sub_ca, endpoint: e.target.value }, + }) + } + placeholder="https://test-ca.local/soap" + /> + + + + + + setDraft({ + ...draft, + sub_ca: { ...draft.sub_ca, client_cert_path: e.target.value }, + }) + } + placeholder="/etc/smgw/ca-client.crt.pem" + /> + + + + setDraft({ + ...draft, + sub_ca: { ...draft.sub_ca, client_key_path: e.target.value }, + }) + } + placeholder="/etc/smgw/ca-client.key.pem" + /> + + + + + + setDraft({ + ...draft, + sub_ca: { ...draft.sub_ca, ca_bundle_path: e.target.value }, + }) + } + placeholder="/etc/smgw/sub-ca-chain.pem" + /> + + + + + testAlert.mutate()} + disabled={testAlert.isPending} + > + Test-Alert + + } + > + + + + setDraft({ + ...draft, + smtp: { ...draft.smtp, host: e.target.value }, + }) + } + /> + + + + setDraft({ + ...draft, + smtp: { ...draft.smtp, port: Number(e.target.value) }, + }) + } + /> + + +
+ + setDraft({ + ...draft, + smtp: { ...draft.smtp, starttls: v }, + }) + } + /> + + {draft.smtp.starttls ? "aktiv" : "inaktiv"} + +
+
+
+ + + + setDraft({ + ...draft, + smtp: { ...draft.smtp, from: e.target.value }, + }) + } + /> + + + + setDraft({ + ...draft, + smtp: { ...draft.smtp, to: e.target.value }, + }) + } + /> + + +
+ + + + + + setDraft({ + ...draft, + hsm: { ...draft.hsm, module_path: e.target.value }, + }) + } + /> + + + + setDraft({ + ...draft, + hsm: { + ...draft.hsm, + slot: e.target.value === "" ? null : Number(e.target.value), + }, + }) + } + placeholder="auto" + /> + + + + setDraft({ + ...draft, + hsm: { ...draft.hsm, pin_env_var: e.target.value }, + }) + } + /> + + + + + + + + + + + + + + +
+ + view.data && setDraft(view.data.config)} + onSave={() => save.mutate(patch)} + saving={save.isPending} + /> + + ); +} + +function SectionCard({ + icon: Icon, + title, + desc, + aside, + children, +}: { + icon: React.ComponentType<{ className?: string }>; + title: string; + desc: string; + aside?: React.ReactNode; + children: React.ReactNode; +}) { + return ( + + +
+
+ +
+
+ {title} +

+ {desc} +

+
+
+ {aside} +
+ + {children} +
+ ); +} + +function Row({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ); +} + +function Field({ + label, + restartRequired, + children, +}: { + label: string; + restartRequired?: boolean; + children: React.ReactNode; +}) { + return ( +
+
+ + {restartRequired && Restart nötig} +
+ {children} +
+ ); +} + +function Hint({ children }: { children: React.ReactNode }) { + return

{children}

; +} + +function SaveBar({ + visible, + onRevert, + onSave, + saving, +}: { + visible: boolean; + onRevert: () => void; + onSave: () => void; + saving: boolean; +}) { + return ( +
+
+
+ + Ungespeicherte Änderungen +
+ + +
+
+ ); +} diff --git a/frontend/src/routes/_app.iconfig.tsx b/frontend/src/routes/_app.iconfig.tsx new file mode 100644 index 0000000..13e43dd --- /dev/null +++ b/frontend/src/routes/_app.iconfig.tsx @@ -0,0 +1,230 @@ +import { useState } from "react"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { createFileRoute } from "@tanstack/react-router"; +import { toast } from "sonner"; +import { + Download, + FileLock2, + KeyRound, + ScanLine, + Shield, + Wand2, +} from "lucide-react"; + +import { Topbar } from "@/components/layout/topbar"; +import { Card, CardBody, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Separator } from "@/components/ui/separator"; +import { api, unwrap, type Schemas, ApiError } from "@/lib/api"; + +export const Route = createFileRoute("/_app/iconfig")({ + component: IconfigPage, +}); + +const PROFILES = [ + { id: "smgw-default", label: "SMGW Default", note: "TR-03109-1 Standardprofil" }, + { id: "lab-debug", label: "Lab Debug", note: "Erweiterte Diagnose-Felder" }, +]; + +function IconfigPage() { + const gateways = useQuery({ + queryKey: ["gateways"], + queryFn: () => unwrap(api.GET("/gateways")), + }); + + const [form, setForm] = useState({ + gateway_id: "", + admin_key_label: "", + profile: PROFILES[0].id, + }); + + const [preview, setPreview] = useState(null); + + const previewMut = useMutation({ + mutationFn: (body: Schemas["IconfigRequest"]) => + unwrap(api.POST("/iconfig/preview", { body })), + onSuccess: (d) => setPreview(d.xml), + onError: (e: ApiError) => + toast.error("Vorschau fehlgeschlagen", { description: e.message }), + }); + + const buildMut = useMutation({ + mutationFn: (body: Schemas["IconfigRequest"]) => + unwrap(api.POST("/iconfig/build", { body })), + onError: (e: ApiError) => + toast.error("Build abgelehnt", { description: e.message }), + }); + + const ready = form.gateway_id.length > 0 && form.admin_key_label.length > 0; + + return ( + <> + +
+ + +
+
+ +
+
+ Profil zusammensetzen +

+ Auswahl Gateway, Admin-Schlüssel, Profilvorlage. Vorschau bevor signiert wird. +

+
+
+
+ + +
+ + +

+ {gateways.data?.items.length ?? 0} Gateways registriert. +

+
+ +
+ + + setForm({ ...form, admin_key_label: e.target.value }) + } + placeholder="GW-ADM-01" + /> +

+ PKCS#11-Label im HSM. Wird über sign_xml referenziert. +

+
+ +
+ +
+ {PROFILES.map((p) => { + const selected = form.profile === p.id; + return ( + + ); + })} +
+
+ + + +
+ + +
+ +
+
+ +
+ Signatur erfolgt server-seitig über{" "} + HsmPort::sign_xml{" "} + auf kanonisierten Bytes (C14N), nicht auf pretty-printed XML. +
+
+
+
+
+ + + +
+
+ +
+
+ Vorschau +

+ Unsignierter XML-Body. Inhalt wird vor Signatur kanonisiert. +

+
+
+ +
+ + +
+{preview ??
+  `// Vorschau erscheint hier, sobald „Vorschau erzeugen" geklickt wurde.
+// Die Datei wird auf dem Backend gebaut — diese Konsole rendert nur das Ergebnis.`}
+            
+
+
+
+ + ); +} + +function downloadXml(xml: string, gateway: string) { + const blob = new Blob([xml], { type: "application/xml" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `iconfig-${gateway || "preview"}.xml`; + a.click(); + URL.revokeObjectURL(url); +} diff --git a/frontend/src/routes/_app.index.tsx b/frontend/src/routes/_app.index.tsx new file mode 100644 index 0000000..d73310d --- /dev/null +++ b/frontend/src/routes/_app.index.tsx @@ -0,0 +1,381 @@ +import { useQuery } from "@tanstack/react-query"; +import { createFileRoute } from "@tanstack/react-router"; +import { + Activity, + AlertCircle, + CalendarClock, + CheckCircle2, + Clock, + Hourglass, + Info, + ShieldAlert, + TriangleAlert, +} from "lucide-react"; + +import { Topbar } from "@/components/layout/topbar"; +import { Card, CardBody, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Skeleton } from "@/components/ui/skeleton"; +import { api, unwrap, type Schemas } from "@/lib/api"; +import { fmtIso, fmtRelative } from "@/lib/format"; +import { cn } from "@/lib/utils"; + +export const Route = createFileRoute("/_app/")({ + component: Dashboard, +}); + +function Dashboard() { + const certs = useQuery({ + queryKey: ["certs"], + queryFn: () => unwrap(api.GET("/certs")), + refetchInterval: 60_000, + }); + const scheduler = useQuery({ + queryKey: ["scheduler"], + queryFn: () => unwrap(api.GET("/scheduler")), + refetchInterval: 10_000, + }); + const alerts = useQuery({ + queryKey: ["alerts"], + queryFn: () => unwrap(api.GET("/alerts")), + refetchInterval: 30_000, + }); + + const counts = countByState(certs.data?.items ?? []); + + return ( + <> + +
+
+ + 0} + /> + + +
+ +
+ + +
+ Renewal-Scheduler +

+ Periodischer Lauf gem. SM-PKI CP. Manueller Trigger via Topbar. +

+
+ + {scheduler.data?.paused ? "Pausiert" : "Aktiv"} + +
+ + + {scheduler.data?.cron_schedule ?? "—"} + + } + /> + + {scheduler.data?.days_window ?? "—"} Tage + + } + /> + + {scheduler.data?.last_handled ?? 0} + + } + /> + + + + {fmtRelative(scheduler.data?.last_run_at)} + +
+ } + sub={fmtIso(scheduler.data?.last_run_at)} + /> + Noch kein Lauf + ) : scheduler.data.last_run_ok ? ( + + Erfolgreich + + ) : ( + + Fehler + + ) + } + sub={scheduler.data?.last_error ?? undefined} + /> + + + + Single-flight: parallele Läufe werden via Semaphore unterdrückt. + + tokio-cron-scheduler + + + + + +
+ Ereignisse +

+ Operator-Alerts, jüngste zuerst. +

+
+ +
+ + {alerts.isLoading ? ( + + ) : (alerts.data?.items.length ?? 0) === 0 ? ( + + ) : ( +
    + {alerts.data!.items + .slice() + .reverse() + .slice(0, 6) + .map((a, idx) => ( +
  • + +
  • + ))} +
+ )} +
+
+ + + + ); +} + +function countByState(items: Schemas["CertificateDto"][]) { + let valid = 0, + expiring = 0, + expired = 0; + for (const c of items) { + if (c.state === "valid") valid += 1; + else if (c.state === "expiring") expiring += 1; + else if (c.state === "expired") expired += 1; + } + return { valid, expiring, expired, pending: 0, total: items.length }; +} + +type Tone = "success" | "warn" | "danger" | "neutral"; + +const TONE_DECOR: Record = { + success: { + ring: "before:bg-success/60", + iconBg: "bg-success-soft", + iconColor: "text-success", + }, + warn: { + ring: "before:bg-warn/70", + iconBg: "bg-warn-soft", + iconColor: "text-warn", + }, + danger: { + ring: "before:bg-danger/70", + iconBg: "bg-danger-soft", + iconColor: "text-danger", + }, + neutral: { + ring: "before:bg-line-strong", + iconBg: "bg-overlay", + iconColor: "text-ink-mute", + }, +}; + +function StatCard({ + label, + value, + hint, + tone, + icon: Icon, + emphasise = false, +}: { + label: string; + value: number | undefined; + hint: string; + tone: Tone; + icon: React.ComponentType<{ className?: string }>; + emphasise?: boolean; +}) { + const t = TONE_DECOR[tone]; + return ( +
+
+ + {label} + +
+ +
+
+
+ + {value ?? } + +
+

{hint}

+
+ ); +} + +function KeyValue({ + label, + value, + sub, +}: { + label: string; + value: React.ReactNode; + sub?: string; +}) { + return ( +
+ + {label} + +
{value}
+ {sub && {sub}} +
+ ); +} + +function AlertRow({ entry }: { entry: Schemas["AlertEntry"] }) { + const tone = + entry.severity === "error" + ? "danger" + : entry.severity === "warning" + ? "warn" + : "accent"; + const Icon = + entry.severity === "error" + ? AlertCircle + : entry.severity === "warning" + ? TriangleAlert + : Info; + return ( +
+
+ +
+
+
+ + {entry.subject} + + + {fmtRelative(entry.at)} + +
+

{entry.body}

+
+
+ ); +} + +function EmptyAlerts() { + return ( +
+
+ +
+

Keine Ereignisse

+

+ Erfolgreiche Läufe und Alerts erscheinen hier. +

+
+ ); +} + +function AlertSkeletons() { + return ( +
+ {[0, 1, 2].map((i) => ( +
+ +
+ + +
+
+ ))} +
+ ); +} diff --git a/frontend/src/routes/_app.tsx b/frontend/src/routes/_app.tsx new file mode 100644 index 0000000..e75d553 --- /dev/null +++ b/frontend/src/routes/_app.tsx @@ -0,0 +1,25 @@ +import { Outlet, createFileRoute, redirect } from "@tanstack/react-router"; + +import { Sidebar } from "@/components/layout/sidebar"; +import { api } from "@/lib/api"; + +export const Route = createFileRoute("/_app")({ + beforeLoad: async () => { + const { response } = await api.GET("/auth/me"); + if (response.status === 401 || response.status === 403) { + throw redirect({ to: "/login" }); + } + }, + component: AppLayout, +}); + +function AppLayout() { + return ( +
+ +
+ +
+
+ ); +} diff --git a/frontend/src/routes/login.tsx b/frontend/src/routes/login.tsx new file mode 100644 index 0000000..b0c283d --- /dev/null +++ b/frontend/src/routes/login.tsx @@ -0,0 +1,139 @@ +import { useState } from "react"; +import { useMutation } from "@tanstack/react-query"; +import { createFileRoute, useNavigate } from "@tanstack/react-router"; +import { ShieldCheck, KeyRound, AlertTriangle } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Badge } from "@/components/ui/badge"; +import { api, unwrap, ApiError } from "@/lib/api"; + +export const Route = createFileRoute("/login")({ + component: LoginPage, +}); + +function LoginPage() { + const navigate = useNavigate(); + const [devSubject, setDevSubject] = useState("CN=lab.operator,OU=PKI,O=SMGW"); + const [err, setErr] = useState(null); + + const login = useMutation({ + mutationFn: () => + unwrap( + api.POST("/auth/session", { + body: { dev_subject: devSubject }, + }), + ), + onError: (e: ApiError) => setErr(e.message), + onSuccess: () => navigate({ to: "/" }), + }); + + return ( +
+ + +
+
+
+ + Sitzung +
+

+ Anmeldung über mTLS +

+

+ Ihr Client-Zertifikat wird vom Reverse Proxy terminiert und im Header{" "} + X-Forwarded-Cert-Subject{" "} + übergeben. Im Lab-Modus ist ein Dev-Subject als Fallback erlaubt. +

+ +
{ + e.preventDefault(); + setErr(null); + login.mutate(); + }} + className="mt-7 space-y-4" + > +
+ + setDevSubject(e.target.value)} + mono + placeholder="CN=…" + /> +

+ Wird ignoriert, sobald der Proxy ein gültiges Cert-Subject mitschickt. +

+
+ + {err && ( +
+ + {err} +
+ )} + + + + +
+ Cookies sind HttpOnly und{" "} + SameSite=Strict. Sitzungsdauer 8 h. +
+
+
+
+ ); +} diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..a5cfbe2 --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": false, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..b40f72c --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true, + "noEmit": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..5605935 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,36 @@ +import { defineConfig, loadEnv } from "vite"; +import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; +import { TanStackRouterVite } from "@tanstack/router-plugin/vite"; +import path from "node:path"; + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), ""); + const apiTarget = env.VITE_API_PROXY_TARGET ?? "http://localhost:8443"; + + return { + plugins: [ + TanStackRouterVite({ target: "react", autoCodeSplitting: true }), + react(), + tailwindcss(), + ], + resolve: { + alias: { "@": path.resolve(__dirname, "./src") }, + }, + server: { + host: "0.0.0.0", + port: 5173, + proxy: { + "/api": { + target: apiTarget, + changeOrigin: true, + secure: false, + }, + }, + }, + build: { + target: "es2022", + sourcemap: true, + }, + }; +});
+ ); +} + +export function TD({ className, ...p }: React.TdHTMLAttributes) { + return ( + + ); +} diff --git a/frontend/src/components/ui/tabs.tsx b/frontend/src/components/ui/tabs.tsx new file mode 100644 index 0000000..a70a9fc --- /dev/null +++ b/frontend/src/components/ui/tabs.tsx @@ -0,0 +1,45 @@ +import * as React from "react"; +import * as RTabs from "@radix-ui/react-tabs"; +import { cn } from "@/lib/utils"; + +export const Tabs = RTabs.Root; + +export const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsList.displayName = "TabsList"; + +export const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsTrigger.displayName = "TabsTrigger"; + +export const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsContent.displayName = "TabsContent"; diff --git a/frontend/src/global.d.ts b/frontend/src/global.d.ts new file mode 100644 index 0000000..a56ffd1 --- /dev/null +++ b/frontend/src/global.d.ts @@ -0,0 +1,11 @@ +/// + +declare module "*.css" { + const content: string; + export default content; +} + +declare module "*.svg" { + const content: string; + export default content; +} diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..37a350b --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,85 @@ +@import "tailwindcss"; + +@theme { + /* Refined-minimal light theme. Warm paper canvas, ink primary, BSI-ish blue accent. */ + --color-canvas: oklch(98.4% 0.005 95); + --color-paper: oklch(100% 0 0); + --color-ink: oklch(18% 0.01 270); + --color-ink-mute: oklch(42% 0.01 270); + --color-ink-faint: oklch(62% 0.01 270); + --color-line: oklch(91% 0.005 270); + --color-line-strong: oklch(82% 0.008 270); + --color-overlay: oklch(96% 0.005 270); + + --color-accent: oklch(45% 0.16 260); + --color-accent-soft: oklch(96% 0.03 260); + --color-accent-line: oklch(86% 0.06 260); + + --color-success: oklch(48% 0.12 152); + --color-success-soft: oklch(96% 0.04 152); + --color-warn: oklch(58% 0.16 65); + --color-warn-soft: oklch(96% 0.06 80); + --color-danger: oklch(50% 0.21 25); + --color-danger-soft: oklch(96% 0.04 25); + + --font-display: "Bricolage Grotesque", system-ui, sans-serif; + --font-sans: "Geist", "Bricolage Grotesque", system-ui, sans-serif; + --font-mono: "Geist Mono", "JetBrains Mono", ui-monospace, monospace; + --font-serif: "Instrument Serif", "Iowan Old Style", Georgia, serif; + + --radius-card: 12px; + --radius-control: 8px; +} + +@layer base { + html, body, #root { height: 100%; } + body { + font-family: var(--font-sans); + font-feature-settings: "ss01", "cv11"; + background: + radial-gradient(1200px 600px at 100% -10%, oklch(95% 0.025 260 / 0.6), transparent 60%), + radial-gradient(900px 500px at -10% 110%, oklch(95% 0.02 80 / 0.5), transparent 60%), + var(--color-canvas); + background-attachment: fixed; + } + ::selection { background: var(--color-accent); color: white; } + h1, h2, h3, h4 { font-family: var(--font-display); letter-spacing: -0.02em; } + .num { font-feature-settings: "tnum", "ss02"; } + .mono { font-family: var(--font-mono); } + .serif { font-family: var(--font-serif); } +} + +@layer components { + .surface { + background: var(--color-paper); + border: 1px solid var(--color-line); + border-radius: var(--radius-card); + box-shadow: + 0 1px 0 rgba(20, 24, 40, 0.02), + 0 8px 24px -20px rgba(20, 24, 40, 0.18); + } + .surface-inset { + background: var(--color-overlay); + border: 1px solid var(--color-line); + border-radius: var(--radius-card); + } + .hairline { border-color: var(--color-line); } + .focus-ring { + outline: 2px solid transparent; + outline-offset: 2px; + transition: box-shadow 120ms ease; + } + .focus-ring:focus-visible { + box-shadow: + 0 0 0 2px var(--color-paper), + 0 0 0 4px var(--color-accent); + } + .grid-bg { + background-image: + linear-gradient(var(--color-line) 1px, transparent 1px), + linear-gradient(90deg, var(--color-line) 1px, transparent 1px); + background-size: 28px 28px; + background-position: -1px -1px; + mask-image: radial-gradient(ellipse at 50% 0%, black 30%, transparent 75%); + } +} diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts new file mode 100644 index 0000000..d92406c --- /dev/null +++ b/frontend/src/lib/api.ts @@ -0,0 +1,45 @@ +import createClient from "openapi-fetch"; +import type { paths, components } from "@/api/schema"; + +/** + * Single API client. `credentials: "include"` so the session cookie issued + * by `POST /api/auth/session` is sent with every subsequent request. + * + * In dev Vite proxies `/api/*` to the Rust backend (see vite.config.ts). + * In prod the SPA is served behind the same reverse proxy that terminates + * mTLS for the backend. + */ +export const api = createClient({ + baseUrl: "/api", + credentials: "include", +}); + +export type Schemas = components["schemas"]; + +export class ApiError extends Error { + constructor( + public readonly status: number, + public readonly code: string, + message: string, + ) { + super(message); + } +} + +type FetchResult = { + data?: T; + error?: { code?: string; message?: string }; + response: Response; +}; + +export async function unwrap(call: Promise>): Promise { + const { data, error, response } = await call; + if (!response.ok || error) { + throw new ApiError( + response.status, + error?.code ?? "unknown", + error?.message ?? response.statusText, + ); + } + return data as T; +} diff --git a/frontend/src/lib/format.ts b/frontend/src/lib/format.ts new file mode 100644 index 0000000..8e3ed4e --- /dev/null +++ b/frontend/src/lib/format.ts @@ -0,0 +1,24 @@ +import { formatDistanceToNowStrict, formatISO9075, parseISO } from "date-fns"; + +export function fmtIso(input?: string | null): string { + if (!input) return "—"; + try { + return formatISO9075(parseISO(input)); + } catch { + return input; + } +} + +export function fmtRelative(input?: string | null): string { + if (!input) return "—"; + try { + return formatDistanceToNowStrict(parseISO(input), { addSuffix: true }); + } catch { + return input; + } +} + +export function truncateSerial(serial: string, head = 6, tail = 6): string { + if (serial.length <= head + tail + 1) return serial; + return `${serial.slice(0, head)}…${serial.slice(-tail)}`; +} diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts new file mode 100644 index 0000000..a5ef193 --- /dev/null +++ b/frontend/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..e037dc2 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,49 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { RouterProvider, createRouter } from "@tanstack/react-router"; +import { Toaster } from "sonner"; + +import { routeTree } from "@/routeTree.gen"; +import "@/index.css"; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 30_000, + retry: 1, + refetchOnWindowFocus: false, + }, + }, +}); + +const router = createRouter({ + routeTree, + context: { queryClient }, + defaultPreload: "intent", + defaultPreloadStaleTime: 0, +}); + +declare module "@tanstack/react-router" { + interface Register { + router: typeof router; + } +} + +const rootEl = document.getElementById("root")!; +createRoot(rootEl).render( + + + + + + , +); diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts new file mode 100644 index 0000000..315ff95 --- /dev/null +++ b/frontend/src/routeTree.gen.ts @@ -0,0 +1,161 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as LoginRouteImport } from './routes/login' +import { Route as AppRouteImport } from './routes/_app' +import { Route as AppIndexRouteImport } from './routes/_app.index' +import { Route as AppIconfigRouteImport } from './routes/_app.iconfig' +import { Route as AppConfigurationRouteImport } from './routes/_app.configuration' +import { Route as AppCertificatesRouteImport } from './routes/_app.certificates' + +const LoginRoute = LoginRouteImport.update({ + id: '/login', + path: '/login', + getParentRoute: () => rootRouteImport, +} as any) +const AppRoute = AppRouteImport.update({ + id: '/_app', + getParentRoute: () => rootRouteImport, +} as any) +const AppIndexRoute = AppIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => AppRoute, +} as any) +const AppIconfigRoute = AppIconfigRouteImport.update({ + id: '/iconfig', + path: '/iconfig', + getParentRoute: () => AppRoute, +} as any) +const AppConfigurationRoute = AppConfigurationRouteImport.update({ + id: '/configuration', + path: '/configuration', + getParentRoute: () => AppRoute, +} as any) +const AppCertificatesRoute = AppCertificatesRouteImport.update({ + id: '/certificates', + path: '/certificates', + getParentRoute: () => AppRoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof AppIndexRoute + '/login': typeof LoginRoute + '/certificates': typeof AppCertificatesRoute + '/configuration': typeof AppConfigurationRoute + '/iconfig': typeof AppIconfigRoute +} +export interface FileRoutesByTo { + '/login': typeof LoginRoute + '/certificates': typeof AppCertificatesRoute + '/configuration': typeof AppConfigurationRoute + '/iconfig': typeof AppIconfigRoute + '/': typeof AppIndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/_app': typeof AppRouteWithChildren + '/login': typeof LoginRoute + '/_app/certificates': typeof AppCertificatesRoute + '/_app/configuration': typeof AppConfigurationRoute + '/_app/iconfig': typeof AppIconfigRoute + '/_app/': typeof AppIndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/login' | '/certificates' | '/configuration' | '/iconfig' + fileRoutesByTo: FileRoutesByTo + to: '/login' | '/certificates' | '/configuration' | '/iconfig' | '/' + id: + | '__root__' + | '/_app' + | '/login' + | '/_app/certificates' + | '/_app/configuration' + | '/_app/iconfig' + | '/_app/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + AppRoute: typeof AppRouteWithChildren + LoginRoute: typeof LoginRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/login': { + id: '/login' + path: '/login' + fullPath: '/login' + preLoaderRoute: typeof LoginRouteImport + parentRoute: typeof rootRouteImport + } + '/_app': { + id: '/_app' + path: '' + fullPath: '/' + preLoaderRoute: typeof AppRouteImport + parentRoute: typeof rootRouteImport + } + '/_app/': { + id: '/_app/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof AppIndexRouteImport + parentRoute: typeof AppRoute + } + '/_app/iconfig': { + id: '/_app/iconfig' + path: '/iconfig' + fullPath: '/iconfig' + preLoaderRoute: typeof AppIconfigRouteImport + parentRoute: typeof AppRoute + } + '/_app/configuration': { + id: '/_app/configuration' + path: '/configuration' + fullPath: '/configuration' + preLoaderRoute: typeof AppConfigurationRouteImport + parentRoute: typeof AppRoute + } + '/_app/certificates': { + id: '/_app/certificates' + path: '/certificates' + fullPath: '/certificates' + preLoaderRoute: typeof AppCertificatesRouteImport + parentRoute: typeof AppRoute + } + } +} + +interface AppRouteChildren { + AppCertificatesRoute: typeof AppCertificatesRoute + AppConfigurationRoute: typeof AppConfigurationRoute + AppIconfigRoute: typeof AppIconfigRoute + AppIndexRoute: typeof AppIndexRoute +} + +const AppRouteChildren: AppRouteChildren = { + AppCertificatesRoute: AppCertificatesRoute, + AppConfigurationRoute: AppConfigurationRoute, + AppIconfigRoute: AppIconfigRoute, + AppIndexRoute: AppIndexRoute, +} + +const AppRouteWithChildren = AppRoute._addFileChildren(AppRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + AppRoute: AppRouteWithChildren, + LoginRoute: LoginRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/frontend/src/routes/__root.tsx b/frontend/src/routes/__root.tsx new file mode 100644 index 0000000..cf77304 --- /dev/null +++ b/frontend/src/routes/__root.tsx @@ -0,0 +1,10 @@ +import { Outlet, createRootRouteWithContext } from "@tanstack/react-router"; +import type { QueryClient } from "@tanstack/react-query"; + +export interface RouterContext { + queryClient: QueryClient; +} + +export const Route = createRootRouteWithContext()({ + component: () => , +}); diff --git a/frontend/src/routes/_app.certificates.tsx b/frontend/src/routes/_app.certificates.tsx new file mode 100644 index 0000000..c636fa4 --- /dev/null +++ b/frontend/src/routes/_app.certificates.tsx @@ -0,0 +1,456 @@ +import { useMemo, useState } from "react"; +import { + useMutation, + useQuery, + useQueryClient, +} from "@tanstack/react-query"; +import { createFileRoute } from "@tanstack/react-router"; +import { toast } from "sonner"; +import { + Copy, + RefreshCcw, + Search, + ShieldCheck, + ArrowUpDown, + Eye, +} from "lucide-react"; + +import { Topbar } from "@/components/layout/topbar"; +import { Card } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { + Table, + THead, + TBody, + TR, + TH, + TD, +} from "@/components/ui/table"; +import { Badge } from "@/components/ui/badge"; +import { StateBadge, type CertState } from "@/components/state-badge"; +import { Skeleton } from "@/components/ui/skeleton"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogBody, + DialogFooter, + DialogClose, +} from "@/components/ui/dialog"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; +import { api, unwrap, type Schemas, ApiError } from "@/lib/api"; +import { fmtIso, fmtRelative, truncateSerial } from "@/lib/format"; +import { cn } from "@/lib/utils"; + +export const Route = createFileRoute("/_app/certificates")({ + component: CertificatesPage, +}); + +type SortKey = "gateway" | "usage" | "not_after" | "state"; + +function CertificatesPage() { + const [q, setQ] = useState(""); + const [stateFilter, setStateFilter] = useState<"all" | CertState>("all"); + const [sortKey, setSortKey] = useState("not_after"); + const [sortDir, setSortDir] = useState<"asc" | "desc">("asc"); + const [selected, setSelected] = useState(null); + + const qc = useQueryClient(); + const certs = useQuery({ + queryKey: ["certs"], + queryFn: () => unwrap(api.GET("/certs")), + refetchInterval: 30_000, + }); + + const renew = useMutation({ + mutationFn: (input: { gateway_id: string; usage: Schemas["CertificateUsageDto"] }) => + unwrap( + api.POST("/certs/{gateway_id}/{usage}/renew", { + params: { path: input }, + }), + ), + onSuccess: (data) => { + toast.success("Erneuerung beauftragt", { + description: `messageID ${data.message_id}`, + }); + qc.invalidateQueries({ queryKey: ["certs"] }); + }, + onError: (e: ApiError) => + toast.error("Erneuerung abgelehnt", { description: e.message }), + }); + + const filtered = useMemo(() => { + const items = certs.data?.items ?? []; + return items + .filter((c) => (stateFilter === "all" ? true : c.state === stateFilter)) + .filter((c) => + q.trim().length === 0 + ? true + : `${c.gateway_id} ${c.serial} ${c.usage}` + .toLowerCase() + .includes(q.toLowerCase()), + ) + .sort((a, b) => { + const dir = sortDir === "asc" ? 1 : -1; + const cmp = compareBy(sortKey, a, b); + return cmp * dir; + }); + }, [certs.data, q, stateFilter, sortKey, sortDir]); + + return ( + <> + +
+
+
+ + setQ(e.target.value)} + placeholder="Gateway, Serial, Profil…" + className="pl-9" + /> +
+ +
+ {(["all", "valid", "expiring", "expired"] as const).map((s) => ( + + ))} +
+ +
+ + {filtered.length}/{certs.data?.items.length ?? 0} + + Einträge +
+
+ + + + + + toggleSort("gateway", sortKey, sortDir, setSortKey, setSortDir)} + /> + + toggleSort("usage", sortKey, sortDir, setSortKey, setSortDir)} + /> + toggleSort("not_after", sortKey, sortDir, setSortKey, setSortDir)} + /> + + toggleSort("state", sortKey, sortDir, setSortKey, setSortDir)} + /> + + + + + {certs.isLoading && } + {!certs.isLoading && filtered.length === 0 && ( + + + + )} + {filtered.map((c) => ( + + + + + + + + + + ))} + +
SerialRestlaufzeitAktionen
+ Keine Treffer. +
+
+ + {c.gateway_id} + + + Profil · {LABEL_USAGE[c.usage]} + +
+
+ + + {LABEL_USAGE[c.usage]} + {fmtIso(c.not_after)} + + {fmtRelative(c.not_after)} + + + + +
+ + +
+
+
+
+ + !o && setSelected(null)} + > + {selected && } + + + ); +} + +function DetailDrawer({ cert }: { cert: Schemas["CertificateDto"] }) { + return ( + + +
+ + Zertifikatsdetails +
+ {cert.gateway_id} + + Verwendung {LABEL_USAGE[cert.usage]} · Status{" "} + {LABEL_STATE[cert.state as CertState]} + +
+ + + + + Übersicht + PEM + + + + {cert.serial} + +
+ {fmtIso(cert.not_before)} + {fmtIso(cert.not_after)} +
+ + + {cert.days_to_expiry} Tage + + +
+ +
+{`-----BEGIN CERTIFICATE-----
+[Backend liefert PEM nach Anbindung an /api/certs/{id}/pem]
+-----END CERTIFICATE-----`}
+            
+
+
+
+ + + + + + + +
+ ); +} + +function Field({ label, children }: { label: string; children: React.ReactNode }) { + return ( +
+ + {label} + +
{children}
+
+ ); +} + +function ColHeader({ + label, + active, + dir, + onClick, +}: { + label: string; + active: boolean; + dir: "asc" | "desc"; + onClick: () => void; +}) { + return ( +
+ +
+ +