Compare commits
11 Commits
fix/stando
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb4747cfeb | ||
|
|
0634d8c236 | ||
|
|
987b8c9c8f | ||
|
|
5d4afb5936 | ||
|
|
4863eadcce | ||
|
|
f933ecc19e | ||
|
|
38021cbc51 | ||
|
|
f71cf51eb4 | ||
|
|
f2578cedab | ||
|
|
a8d07ba2ab | ||
|
|
2e56a92b70 |
17
.env.example
17
.env.example
@@ -4,7 +4,10 @@
|
|||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
|
|
||||||
# Datenbank (Postgres)
|
# Datenbank (Postgres)
|
||||||
DATABASE_URL=postgres://floriannetz:floriannetz@localhost:5432/floriannetz
|
# Datenbank (Postgres). Format: postgresql://USER:PASSWORT@HOST:PORT/DB
|
||||||
|
# Lokal (Host -> Docker-Postgres via docker-compose.dev.yml): HOST=localhost.
|
||||||
|
# Im Container setzt docker-compose.yml HOST automatisch auf den Service "postgres".
|
||||||
|
DATABASE_URL=postgresql://floriannetz:floriannetz@localhost:5432/floriannetz
|
||||||
|
|
||||||
# Auth.js / NextAuth
|
# Auth.js / NextAuth
|
||||||
# AUTH_SECRET muss >= 32 Zeichen sein (z. B. `openssl rand -base64 32`)
|
# AUTH_SECRET muss >= 32 Zeichen sein (z. B. `openssl rand -base64 32`)
|
||||||
@@ -17,6 +20,10 @@ AUTH_TRUST_HOST=true
|
|||||||
AUTHENTIK_ISSUER=http://localhost:9000/application/o/floriannetz/
|
AUTHENTIK_ISSUER=http://localhost:9000/application/o/floriannetz/
|
||||||
AUTHENTIK_CLIENT_ID=floriannetz
|
AUTHENTIK_CLIENT_ID=floriannetz
|
||||||
AUTHENTIK_CLIENT_SECRET=bitte-setzen
|
AUTHENTIK_CLIENT_SECRET=bitte-setzen
|
||||||
|
# Mitglieder dieser Authentik-Gruppe erhalten beim Login automatisch
|
||||||
|
# platform_admin. Wer NICHT in der Gruppe ist, wird vom SSO-Login abgewiesen.
|
||||||
|
# Setup siehe docs/reference/authentik-setup.md.
|
||||||
|
AUTHENTIK_ADMIN_GROUP=floriannetz-admins
|
||||||
|
|
||||||
# Geo (interne Dienste; Defaults zeigen auf Docker-Compose-Hostnamen)
|
# Geo (interne Dienste; Defaults zeigen auf Docker-Compose-Hostnamen)
|
||||||
OSRM_URL=http://osrm:5000
|
OSRM_URL=http://osrm:5000
|
||||||
@@ -27,11 +34,13 @@ HAVERSINE_KMH=50
|
|||||||
# Deployment / externes Traefik
|
# Deployment / externes Traefik
|
||||||
# APP_HOST ist der öffentliche Hostname (Traefik-Routing + AUTH_URL-Basis).
|
# APP_HOST ist der öffentliche Hostname (Traefik-Routing + AUTH_URL-Basis).
|
||||||
# In Produktion: AUTH_URL=https://${APP_HOST} und AUTH_TRUST_HOST=true setzen.
|
# In Produktion: AUTH_URL=https://${APP_HOST} und AUTH_TRUST_HOST=true setzen.
|
||||||
APP_HOST=floriannetz.example.at
|
APP_HOST=florian.feuerwehr-rems.at
|
||||||
# Traefik-Zertifikatsauflöser (muss in der externen Traefik-Instanz definiert sein).
|
# Traefik-Zertifikatsauflöser (muss in der externen Traefik-Instanz definiert sein).
|
||||||
TRAEFIK_CERTRESOLVER=letsencrypt
|
TRAEFIK_CERTRESOLVER=letsencrypt
|
||||||
# Name des externen, von Traefik verwalteten Docker-Netzes.
|
# Name des externen, von Traefik verwalteten Docker-Netzes
|
||||||
TRAEFIK_NETWORK=traefik
|
# (im feuerwehr_dashboard heißt es "frontend"). Muss existieren:
|
||||||
|
# docker network create frontend
|
||||||
|
TRAEFIK_NETWORK=frontend
|
||||||
# Optionaler Katalog-Seed beim Container-Start (idempotent).
|
# Optionaler Katalog-Seed beim Container-Start (idempotent).
|
||||||
RUN_SEED=false
|
RUN_SEED=false
|
||||||
# Postgres-Zugangsdaten für den Compose-Postgres-Service.
|
# Postgres-Zugangsdaten für den Compose-Postgres-Service.
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -9,6 +9,9 @@ tests/e2e/.auth/
|
|||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Coverage-Report (vitest --coverage), generiertes Artefakt.
|
||||||
|
coverage/
|
||||||
|
|
||||||
# Generiertes Artefakt: wird im Docker-builder aus src/db/seed gebündelt
|
# Generiertes Artefakt: wird im Docker-builder aus src/db/seed gebündelt
|
||||||
# (scripts/build-seed-bundle.mjs), nicht eingecheckt.
|
# (scripts/build-seed-bundle.mjs), nicht eingecheckt.
|
||||||
docker/seed.mjs
|
docker/seed.mjs
|
||||||
|
|||||||
25
Dockerfile
25
Dockerfile
@@ -9,14 +9,33 @@ ARG NODE_VERSION=22
|
|||||||
# --- deps: Produktions- und Build-Abhängigkeiten installieren -----------------
|
# --- deps: Produktions- und Build-Abhängigkeiten installieren -----------------
|
||||||
FROM node:${NODE_VERSION}-alpine AS deps
|
FROM node:${NODE_VERSION}-alpine AS deps
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
# Nur Manifeste kopieren -> Layer-Cache bleibt stabil, solange sich Deps nicht ändern.
|
# node:alpine bündelt npm 10, das bei plattformfremden optionalen Transitiv-Deps
|
||||||
COPY package.json package-lock.json ./
|
# (z. B. @node-rs/argon2 -> *-wasm32-wasi / @emnapi) strenger ist. npm 11 wie im
|
||||||
RUN npm ci
|
# feuerwehr_dashboard verwenden.
|
||||||
|
RUN npm install -g npm@11
|
||||||
|
# .npmrc erzwingt das ÖFFENTLICHE npm-Registry. Der committete Lockfile wurde
|
||||||
|
# gegen einen internen Mirror erzeugt (resolved-URLs zeigen dorthin, daher der
|
||||||
|
# npm-ci-Fehler) und wird im Build bewusst NICHT verwendet — Auflösung frisch aus
|
||||||
|
# der öffentlichen Registry (gleiches Vorgehen wie feuerwehr_dashboard/frontend).
|
||||||
|
COPY package.json .npmrc ./
|
||||||
|
RUN npm install --no-audit --no-fund
|
||||||
|
|
||||||
# --- builder: Next.js im Standalone-Modus bauen -------------------------------
|
# --- builder: Next.js im Standalone-Modus bauen -------------------------------
|
||||||
FROM node:${NODE_VERSION}-alpine AS builder
|
FROM node:${NODE_VERSION}-alpine AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
# Build-Zeit-Platzhalter: src/lib/env.ts validiert beim Import (Fail-Fast).
|
||||||
|
# `next build` evaluiert beim "Collecting page data" die Server-Routen (u. a.
|
||||||
|
# /api/auth/[...nextauth]) -> ohne gesetzte Variablen bricht der Import ab.
|
||||||
|
# Diese Werte sind NUR für den Build (erfüllen das Zod-Schema); Server-env wird
|
||||||
|
# NICHT ins Bundle inlined und die builder-Stage landet NICHT im Runtime-Image.
|
||||||
|
# Echte Werte kommen zur Laufzeit aus docker-compose.
|
||||||
|
ENV DATABASE_URL=postgresql://build:build@localhost:5432/build \
|
||||||
|
AUTH_SECRET=build_only_placeholder_secret_min_32_chars_long \
|
||||||
|
AUTH_URL=https://build.invalid \
|
||||||
|
AUTHENTIK_ISSUER=https://build.invalid/application/o/floriannetz/ \
|
||||||
|
AUTHENTIK_CLIENT_ID=build \
|
||||||
|
AUTHENTIK_CLIENT_SECRET=build
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
COPY . .
|
COPY . .
|
||||||
# next.config.ts setzt output:"standalone" -> erzeugt .next/standalone/server.js.
|
# next.config.ts setzt output:"standalone" -> erzeugt .next/standalone/server.js.
|
||||||
|
|||||||
153
Makefile
153
Makefile
@@ -1,38 +1,151 @@
|
|||||||
# FlorianNetz — Deployment-Makefile (externes Traefik).
|
# FlorianNetz — Makefile
|
||||||
|
# Lokale Entwicklung, Datenbank (Migrationen/Seeds) und Deployment (externes Traefik).
|
||||||
#
|
#
|
||||||
# Ziele:
|
# Schnellstart (lokal, Postgres via Docker):
|
||||||
# make build - baut das App-Image (Next.js standalone, non-root)
|
# make setup # install + Postgres hoch + migrate + seed-all
|
||||||
# make up - startet den Stack (App + Postgres + Geo) hinter Traefik
|
# make dev # Dev-Server -> http://localhost:3000
|
||||||
# make down - stoppt den Stack
|
|
||||||
# make logs - folgt den App-Logs
|
|
||||||
# make deploy - build + up (Standard-Deploy)
|
|
||||||
# make data - bereitet die OSRM-Geodaten vor (Download + Preprocessing)
|
|
||||||
# make config - validiert die Compose-Konfiguration
|
|
||||||
#
|
#
|
||||||
# Hinweis: up/data/deploy benötigen Docker (+ Netzzugriff/RAM/Disk) und werden
|
# Nur Build + Migrate (z. B. CI / vor Deploy):
|
||||||
# NICHT in CI/Sandbox ausgeführt. Das externe Traefik-Netz muss existieren:
|
# make build-app migrate
|
||||||
# docker network create traefik
|
#
|
||||||
|
# Voll-Deploy hinter externem Traefik (Docker):
|
||||||
|
# docker network create frontend # einmalig (externes Traefik-Netz)
|
||||||
|
# make deploy
|
||||||
|
#
|
||||||
|
# `make help` listet alle Ziele.
|
||||||
|
|
||||||
|
SHELL := /bin/bash
|
||||||
COMPOSE = docker compose --env-file .env
|
COMPOSE = docker compose --env-file .env
|
||||||
|
# Lokale DB-Ziele binden den Dev-Override ein (veröffentlicht Postgres-Port 5432).
|
||||||
|
COMPOSE_DEV = docker compose --env-file .env -f docker-compose.yml -f docker-compose.dev.yml
|
||||||
|
|
||||||
.PHONY: build up down logs deploy data config
|
.DEFAULT_GOAL := help
|
||||||
|
|
||||||
build:
|
# ---------------------------------------------------------------------------
|
||||||
|
.PHONY: help
|
||||||
|
help: ## Diese Übersicht anzeigen
|
||||||
|
@grep -hE '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) \
|
||||||
|
| awk 'BEGIN{FS=":.*?## "}{printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}'
|
||||||
|
|
||||||
|
# --- Umgebung -------------------------------------------------------------
|
||||||
|
.PHONY: env install
|
||||||
|
env: ## .env aus .env.example erzeugen (falls noch nicht vorhanden)
|
||||||
|
@if [ ! -f .env ]; then cp .env.example .env && echo "→ .env aus .env.example erstellt (Werte anpassen!)"; else echo "→ .env existiert bereits"; fi
|
||||||
|
|
||||||
|
install: ## npm-Abhängigkeiten installieren
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# --- Lokale Entwicklung & Qualität ---------------------------------------
|
||||||
|
.PHONY: dev build-app lint typecheck test test-cov check
|
||||||
|
dev: ## Next.js Dev-Server (http://localhost:3000)
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
build-app: ## Next.js Production-Build (ohne Docker)
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
lint: ## ESLint
|
||||||
|
npm run lint
|
||||||
|
|
||||||
|
typecheck: ## TypeScript prüfen (tsc --noEmit)
|
||||||
|
npm run typecheck
|
||||||
|
|
||||||
|
test: ## Unit-Tests (Vitest)
|
||||||
|
npm run test
|
||||||
|
|
||||||
|
test-cov: ## Unit-Tests mit Coverage
|
||||||
|
npm run test:coverage
|
||||||
|
|
||||||
|
check: lint typecheck test ## Lint + Typecheck + Unit-Tests (Offline-DoD)
|
||||||
|
|
||||||
|
# --- E2E (braucht laufenden Server + Browser) ----------------------------
|
||||||
|
.PHONY: e2e-install e2e e2e-gating
|
||||||
|
e2e-install: ## Playwright-Browser installieren (einmalig)
|
||||||
|
npx playwright install
|
||||||
|
|
||||||
|
e2e: ## Komplette Playwright-E2E-Suite
|
||||||
|
npm run test:e2e
|
||||||
|
|
||||||
|
e2e-gating: ## Nur die Default-deny-Gating-Suite
|
||||||
|
npm run test:e2e:gating
|
||||||
|
|
||||||
|
# --- Datenbank (lokal; Postgres via Docker, Port auf Host veröffentlicht) -
|
||||||
|
.PHONY: db-up db-down db-wait generate migrate seed-auth seed seed-all db-check studio db-reset
|
||||||
|
db-up: ## Nur Postgres starten (Docker, Port 5432 lokal)
|
||||||
|
$(COMPOSE_DEV) up -d postgres
|
||||||
|
|
||||||
|
db-down: ## Postgres-Container stoppen
|
||||||
|
$(COMPOSE_DEV) stop postgres
|
||||||
|
|
||||||
|
db-wait: ## Warten bis Postgres bereit ist (max ~60s)
|
||||||
|
@echo "→ Warte auf Postgres…"; \
|
||||||
|
for i in $$(seq 1 30); do \
|
||||||
|
if $(COMPOSE_DEV) exec -T postgres sh -c 'pg_isready -U "$$POSTGRES_USER" -d "$$POSTGRES_DB"' >/dev/null 2>&1; then \
|
||||||
|
echo "→ Postgres bereit."; exit 0; \
|
||||||
|
fi; sleep 2; \
|
||||||
|
done; echo "✗ Postgres nicht bereit (Timeout)."; exit 1
|
||||||
|
|
||||||
|
generate: ## Drizzle-Migration aus dem Schema generieren
|
||||||
|
npm run db:generate
|
||||||
|
|
||||||
|
migrate: ## Migrationen anwenden (DATABASE_URL aus .env -> localhost:5432)
|
||||||
|
npm run db:migrate
|
||||||
|
|
||||||
|
seed-auth: ## Ersten Platform-Admin anlegen (idempotent)
|
||||||
|
npm run db:seed-auth
|
||||||
|
|
||||||
|
seed: ## NÖ-Katalog seeden: Merkmale/Vorlagen/Kategorien (idempotent)
|
||||||
|
npm run db:seed
|
||||||
|
|
||||||
|
seed-all: seed-auth seed ## Auth- + Katalog-Seed
|
||||||
|
|
||||||
|
db-check: ## Drizzle-Schema-/Migrationskonsistenz prüfen (offline)
|
||||||
|
npm run db:check
|
||||||
|
|
||||||
|
studio: ## Drizzle Studio öffnen (DB-Browser)
|
||||||
|
npm run db:studio
|
||||||
|
|
||||||
|
db-reset: ## ACHTUNG: Postgres-Volume löschen, neu migrieren + seeden
|
||||||
|
$(COMPOSE_DEV) rm -sf postgres
|
||||||
|
-docker volume rm florian-netz_postgres-data
|
||||||
|
$(MAKE) db-up db-wait migrate seed-all
|
||||||
|
|
||||||
|
# --- Erststart (lokal, von 0) --------------------------------------------
|
||||||
|
.PHONY: setup
|
||||||
|
setup: install env db-up db-wait migrate seed-all ## Komplettes lokales Setup von 0
|
||||||
|
@echo ""
|
||||||
|
@echo "✓ Setup fertig. Login-Admin via 'make seed-auth' angelegt. Weiter mit: make dev"
|
||||||
|
|
||||||
|
# --- Deployment (externes Traefik; braucht Docker) -----------------------
|
||||||
|
# Externes Netz muss existieren: docker network create frontend
|
||||||
|
.PHONY: build up up-core down logs ps deploy deploy-core migrate-stack data config
|
||||||
|
build: ## App-Image bauen (Next.js standalone, non-root)
|
||||||
$(COMPOSE) build app
|
$(COMPOSE) build app
|
||||||
|
|
||||||
up:
|
up: ## Stack starten (App + Postgres + Geo) hinter Traefik
|
||||||
$(COMPOSE) up -d
|
$(COMPOSE) up -d
|
||||||
|
|
||||||
down:
|
up-core: ## Nur App + Postgres starten (OHNE Geo/OSRM/Nominatim) — wenig RAM nötig
|
||||||
|
$(COMPOSE) up -d --build app postgres
|
||||||
|
|
||||||
|
down: ## Stack stoppen
|
||||||
$(COMPOSE) down
|
$(COMPOSE) down
|
||||||
|
|
||||||
logs:
|
logs: ## App-Logs folgen
|
||||||
$(COMPOSE) logs -f app
|
$(COMPOSE) logs -f app
|
||||||
|
|
||||||
deploy: build up
|
ps: ## Status der Stack-Container
|
||||||
|
$(COMPOSE) ps
|
||||||
|
|
||||||
data:
|
deploy: build up ## build + up (voller Stack inkl. Geo; migrate via Entrypoint)
|
||||||
|
|
||||||
|
deploy-core: ## build + up-core (App + Postgres, ohne Geo; Geo später per 'make data' + 'make up')
|
||||||
|
$(MAKE) up-core
|
||||||
|
|
||||||
|
migrate-stack: ## Migrationen im laufenden App-Container ausführen (manuell)
|
||||||
|
$(COMPOSE) exec app node docker/migrate.mjs
|
||||||
|
|
||||||
|
data: ## OSRM-Geodaten vorbereiten (Download + Preprocessing; viel RAM/Disk)
|
||||||
./scripts/prepare-osm-data.sh
|
./scripts/prepare-osm-data.sh
|
||||||
|
|
||||||
config:
|
config: ## Compose-Konfiguration validieren
|
||||||
$(COMPOSE) config --services
|
$(COMPOSE) config --services
|
||||||
|
|||||||
11
docker-compose.dev.yml
Normal file
11
docker-compose.dev.yml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Lokale Entwicklung: veröffentlicht den Postgres-Port auf dem Host (5432),
|
||||||
|
# damit auf dem HOST laufende Befehle (`make migrate`, `make seed`, `npm run db:*`)
|
||||||
|
# die Datenbank über DATABASE_URL (…@localhost:5432/…) erreichen.
|
||||||
|
#
|
||||||
|
# Wird NUR von den lokalen DB-Zielen des Makefiles eingebunden
|
||||||
|
# (docker compose -f docker-compose.yml -f docker-compose.dev.yml …),
|
||||||
|
# NICHT vom Produktiv-Deploy — dort bleibt Postgres app-intern (kein offener Port).
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
@@ -1,18 +1,22 @@
|
|||||||
# FlorianNetz — Basis-Compose hinter EXTERNEM Traefik.
|
# FlorianNetz — Basis-Compose hinter EXTERNEM Traefik.
|
||||||
#
|
#
|
||||||
|
# Ausgerichtet auf das bestehende Setup von feuerwehr_dashboard:
|
||||||
|
# - externes, von Traefik verwaltetes Netz heißt "frontend" (external: true)
|
||||||
|
# - Router: entrypoints=websecure, tls + certresolver=letsencrypt
|
||||||
|
# - explizite Router->Service-Bindung, loadbalancer.server.port=3000
|
||||||
|
# - traefik.docker.network = das externe "frontend"-Netz
|
||||||
|
#
|
||||||
# Es gibt bewusst KEINEN eigenen Proxy-/Traefik-Service: Routing/TLS übernimmt
|
# Es gibt bewusst KEINEN eigenen Proxy-/Traefik-Service: Routing/TLS übernimmt
|
||||||
# eine separat betriebene Traefik-Instanz, die am externen Netz "${TRAEFIK_NETWORK}"
|
# die separat betriebene Traefik-Instanz am Netz "${TRAEFIK_NETWORK}" (Default:
|
||||||
# (Default: traefik) lauscht. Dieses Netz muss bereits existieren:
|
# frontend). Dieses Netz muss bereits existieren:
|
||||||
# docker network create traefik
|
# docker network create frontend
|
||||||
#
|
#
|
||||||
# Geo-Dienste (osrm, nominatim) sind hier mit ihren Laufzeit-Verträgen definiert;
|
# Postgres/Geo liegen am internen Bridge-Netz (keine veröffentlichten Ports,
|
||||||
# das schwergewichtige Daten-Preprocessing/Volume kommt aus docker-compose.geo.yml
|
# also nicht öffentlich erreichbar) — der App-Container hat über dieses Netz
|
||||||
# (siehe scripts/prepare-osm-data.sh / infra/geo).
|
# zugleich Egress (z. B. für den Authentik-OIDC-Token-Austausch).
|
||||||
#
|
#
|
||||||
# Start:
|
# Start: docker compose --env-file .env up -d
|
||||||
# docker compose --env-file .env up -d
|
# Lokal: docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
|
||||||
# Lokal ohne Traefik/TLS:
|
|
||||||
# docker compose -f docker-compose.yml -f docker-compose.override.yml up -d
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
@@ -24,7 +28,7 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
environment:
|
environment:
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
DATABASE_URL: postgres://${POSTGRES_USER:-floriannetz}:${POSTGRES_PASSWORD:-floriannetz}@postgres:5432/${POSTGRES_DB:-floriannetz}
|
DATABASE_URL: postgresql://${POSTGRES_USER:-floriannetz}:${POSTGRES_PASSWORD:-floriannetz}@postgres:5432/${POSTGRES_DB:-floriannetz}
|
||||||
# Forwarded-Header + sichere Cookies hinter Traefik.
|
# Forwarded-Header + sichere Cookies hinter Traefik.
|
||||||
AUTH_TRUST_HOST: "true"
|
AUTH_TRUST_HOST: "true"
|
||||||
AUTH_URL: https://${APP_HOST}
|
AUTH_URL: https://${APP_HOST}
|
||||||
@@ -32,13 +36,14 @@ services:
|
|||||||
AUTHENTIK_ISSUER: ${AUTHENTIK_ISSUER}
|
AUTHENTIK_ISSUER: ${AUTHENTIK_ISSUER}
|
||||||
AUTHENTIK_CLIENT_ID: ${AUTHENTIK_CLIENT_ID}
|
AUTHENTIK_CLIENT_ID: ${AUTHENTIK_CLIENT_ID}
|
||||||
AUTHENTIK_CLIENT_SECRET: ${AUTHENTIK_CLIENT_SECRET}
|
AUTHENTIK_CLIENT_SECRET: ${AUTHENTIK_CLIENT_SECRET}
|
||||||
|
AUTHENTIK_ADMIN_GROUP: ${AUTHENTIK_ADMIN_GROUP:-floriannetz-admins}
|
||||||
OSRM_URL: http://osrm:5000
|
OSRM_URL: http://osrm:5000
|
||||||
NOMINATIM_URL: http://nominatim:8080
|
NOMINATIM_URL: http://nominatim:8080
|
||||||
GEO_HTTP_TIMEOUT_MS: ${GEO_HTTP_TIMEOUT_MS:-4000}
|
GEO_HTTP_TIMEOUT_MS: ${GEO_HTTP_TIMEOUT_MS:-4000}
|
||||||
HAVERSINE_KMH: ${HAVERSINE_KMH:-50}
|
HAVERSINE_KMH: ${HAVERSINE_KMH:-50}
|
||||||
RUN_SEED: ${RUN_SEED:-false}
|
RUN_SEED: ${RUN_SEED:-false}
|
||||||
networks:
|
networks:
|
||||||
- traefik
|
- frontend
|
||||||
- internal
|
- internal
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test:
|
||||||
@@ -51,11 +56,12 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.docker.network=${TRAEFIK_NETWORK:-traefik}"
|
- "traefik.docker.network=${TRAEFIK_NETWORK:-frontend}"
|
||||||
- "traefik.http.routers.floriannetz.rule=Host(`${APP_HOST}`)"
|
|
||||||
- "traefik.http.routers.floriannetz.entrypoints=websecure"
|
- "traefik.http.routers.floriannetz.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.floriannetz.rule=Host(`${APP_HOST}`)"
|
||||||
- "traefik.http.routers.floriannetz.tls=true"
|
- "traefik.http.routers.floriannetz.tls=true"
|
||||||
- "traefik.http.routers.floriannetz.tls.certresolver=${TRAEFIK_CERTRESOLVER:-letsencrypt}"
|
- "traefik.http.routers.floriannetz.tls.certresolver=${TRAEFIK_CERTRESOLVER:-letsencrypt}"
|
||||||
|
- "traefik.http.routers.floriannetz.service=floriannetz"
|
||||||
- "traefik.http.services.floriannetz.loadbalancer.server.port=3000"
|
- "traefik.http.services.floriannetz.loadbalancer.server.port=3000"
|
||||||
# Security-Header-Middleware (zusätzlich zu next.config.ts; defense-in-depth).
|
# Security-Header-Middleware (zusätzlich zu next.config.ts; defense-in-depth).
|
||||||
- "traefik.http.routers.floriannetz.middlewares=floriannetz-sechdrs"
|
- "traefik.http.routers.floriannetz.middlewares=floriannetz-sechdrs"
|
||||||
@@ -137,10 +143,12 @@ volumes:
|
|||||||
nominatim-data:
|
nominatim-data:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
# Externes, von der separaten Traefik-Instanz verwaltetes Netz.
|
# Externes, von der separaten Traefik-Instanz verwaltetes Netz (wie im
|
||||||
traefik:
|
# feuerwehr_dashboard "frontend"). Muss existieren: docker network create frontend
|
||||||
|
frontend:
|
||||||
external: true
|
external: true
|
||||||
name: ${TRAEFIK_NETWORK:-traefik}
|
name: ${TRAEFIK_NETWORK:-frontend}
|
||||||
# Internes Netz: Postgres/Geo sind nur app-intern erreichbar, nicht öffentlich.
|
# Internes Bridge-Netz: Postgres/Geo ohne veröffentlichte Ports (nicht
|
||||||
|
# öffentlich), zugleich Egress für den App-Container (Authentik-OIDC).
|
||||||
internal:
|
internal:
|
||||||
internal: true
|
driver: bridge
|
||||||
|
|||||||
53
docs/reference/authentik-setup.md
Normal file
53
docs/reference/authentik-setup.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# Authentik-Integration & Admin-Zugang (FlorianNetz)
|
||||||
|
|
||||||
|
FlorianNetz nutzt Authentik als **OIDC-Identitätsanbieter**. Der **Admin-Zugang
|
||||||
|
(platform_admin) wird zentral über eine Authentik-Gruppe** gesteuert — nicht über
|
||||||
|
manuell gesetzte DB-Rollen.
|
||||||
|
|
||||||
|
## Wie es funktioniert
|
||||||
|
|
||||||
|
- Anmeldung über Authentik (OIDC). FlorianNetz fordert die Scopes
|
||||||
|
`openid email profile groups` an.
|
||||||
|
- Im `signIn`-Callback (`src/auth.ts`) wird der `groups`-Claim ausgewertet:
|
||||||
|
- **Mitglied der Admin-Gruppe** (`AUTHENTIK_ADMIN_GROUP`, Standard
|
||||||
|
`floriannetz-admins`) → wird (idempotent) als `platform_admin` in `users`
|
||||||
|
angelegt/aktualisiert und eingeloggt.
|
||||||
|
- **Kein Mitglied** → Login wird **abgewiesen** (`return false`).
|
||||||
|
- Folge: Admins werden **in Authentik** verwaltet (Gruppenmitgliedschaft), nicht
|
||||||
|
per `seed-auth`. Ein erstes manuelles Seeding entfällt (kein Henne-Ei-Problem).
|
||||||
|
- **Wehr-Konten** (wehr_admin/wehr_read) bleiben **lokale** App-Konten
|
||||||
|
(E-Mail+Passwort), die Wehr-Admins selbst anlegen — sie nutzen NICHT Authentik.
|
||||||
|
|
||||||
|
## Einrichtung in Authentik
|
||||||
|
|
||||||
|
1. **Gruppe anlegen:** z. B. `floriannetz-admins`; gewünschte Admin-Benutzer
|
||||||
|
hinzufügen. (Muss exakt `AUTHENTIK_ADMIN_GROUP` entsprechen.)
|
||||||
|
2. **Provider anlegen:** OAuth2/OpenID Provider
|
||||||
|
- Redirect-URI: `https://<APP_HOST>/api/auth/callback/authentik`
|
||||||
|
- Signing Key wie üblich; Client-Typ „Confidential".
|
||||||
|
3. **Scopes/Property-Mappings:** dem Provider die Scope-Mappings
|
||||||
|
`openid`, `email`, `profile` **und** das Gruppen-Mapping zuweisen, das den
|
||||||
|
`groups`-Claim liefert (Authentik-Standard: „authentik default OAuth Mapping:
|
||||||
|
OpenID 'groups'"). Ohne dieses Mapping enthält das Token keine `groups` und
|
||||||
|
**niemand** erhält Admin-Zugang.
|
||||||
|
4. **Application anlegen** und mit dem Provider verknüpfen; Slug muss zum
|
||||||
|
`AUTHENTIK_ISSUER` passen (`…/application/o/<slug>/`).
|
||||||
|
5. **Client-ID/-Secret** aus dem Provider übernehmen.
|
||||||
|
|
||||||
|
## Umgebungsvariablen
|
||||||
|
|
||||||
|
```
|
||||||
|
AUTHENTIK_ISSUER=https://auth.example.at/application/o/floriannetz/
|
||||||
|
AUTHENTIK_CLIENT_ID=…
|
||||||
|
AUTHENTIK_CLIENT_SECRET=…
|
||||||
|
AUTHENTIK_ADMIN_GROUP=floriannetz-admins
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prüfen
|
||||||
|
|
||||||
|
- Mitglied von `floriannetz-admins` meldet sich an → landet als Admin in
|
||||||
|
`/admin`; in `users` existiert eine Zeile `authTyp='authentik'`,
|
||||||
|
`rolle='platform_admin'`.
|
||||||
|
- Nicht-Mitglied meldet sich an → Login abgewiesen (zurück zu `/login`).
|
||||||
|
- `groups`-Claim fehlt (Mapping nicht zugewiesen) → alle SSO-Logins abgewiesen
|
||||||
|
(erwartetes Fail-safe-Verhalten: kein Claim ⇒ kein Admin).
|
||||||
@@ -18,17 +18,20 @@ Der Stack besteht aus genau vier Services (kein Proxy):
|
|||||||
|
|
||||||
Netze:
|
Netze:
|
||||||
|
|
||||||
- **`traefik`** — externes, von Traefik verwaltetes Netz (`external: true`,
|
- **`frontend`** — externes, von Traefik verwaltetes Netz (`external: true`,
|
||||||
Name aus `TRAEFIK_NETWORK`, Default `traefik`). Nur `app` hängt daran.
|
Name aus `TRAEFIK_NETWORK`, Default `frontend` — wie im feuerwehr_dashboard).
|
||||||
- **`internal`** — internes Netz (`internal: true`); Postgres und die Geo-Dienste
|
Nur `app` hängt daran (Proxy↔App).
|
||||||
sind ausschließlich für die App erreichbar, nie öffentlich.
|
- **`internal`** — internes Bridge-Netz; Postgres und die Geo-Dienste haben
|
||||||
|
**keine veröffentlichten Ports** (nicht öffentlich erreichbar). Über dieses
|
||||||
|
Netz hat der App-Container zugleich **Egress** (z. B. für den
|
||||||
|
Authentik-OIDC-Token-Austausch).
|
||||||
|
|
||||||
## Voraussetzungen
|
## Voraussetzungen
|
||||||
|
|
||||||
Das externe Traefik-Netz muss existieren, bevor der Stack startet:
|
Das externe Traefik-Netz muss existieren, bevor der Stack startet:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker network create traefik
|
docker network create frontend
|
||||||
```
|
```
|
||||||
|
|
||||||
Die externe Traefik-Instanz muss:
|
Die externe Traefik-Instanz muss:
|
||||||
@@ -36,7 +39,7 @@ Die externe Traefik-Instanz muss:
|
|||||||
- einen Entrypoint `websecure` (Port 443) bereitstellen,
|
- einen Entrypoint `websecure` (Port 443) bereitstellen,
|
||||||
- einen Zertifikatsauflöser anbieten, dessen Name `TRAEFIK_CERTRESOLVER`
|
- einen Zertifikatsauflöser anbieten, dessen Name `TRAEFIK_CERTRESOLVER`
|
||||||
entspricht (Default `letsencrypt`),
|
entspricht (Default `letsencrypt`),
|
||||||
- am Netz `traefik` lauschen (`providers.docker` mit `exposedByDefault=false`).
|
- am Netz `frontend` lauschen (`providers.docker` mit `exposedByDefault=false`).
|
||||||
|
|
||||||
Die App-Labels in `docker-compose.yml` setzen Router (`Host(\`${APP_HOST}\`)`,
|
Die App-Labels in `docker-compose.yml` setzen Router (`Host(\`${APP_HOST}\`)`,
|
||||||
`entrypoints=websecure`, `tls.certresolver`), Service-Port `3000` und eine
|
`entrypoints=websecure`, `tls.certresolver`), Service-Port `3000` und eine
|
||||||
@@ -48,16 +51,17 @@ Vollständiger Vertrag in `.env.example`. Für den Betrieb hinter Traefik zwinge
|
|||||||
|
|
||||||
| Variable | Beispiel / Hinweis |
|
| Variable | Beispiel / Hinweis |
|
||||||
| ------------------------- | ---------------------------------------------------- |
|
| ------------------------- | ---------------------------------------------------- |
|
||||||
| `APP_HOST` | öffentlicher Hostname, z. B. `floriannetz.example.at` |
|
| `APP_HOST` | öffentlicher Hostname, z. B. `florian.feuerwehr-rems.at` |
|
||||||
| `AUTH_URL` | `https://${APP_HOST}` — Basis für Callback + Cookies |
|
| `AUTH_URL` | `https://${APP_HOST}` — Basis für Callback + Cookies |
|
||||||
| `AUTH_TRUST_HOST` | `true` — Auth.js vertraut den Forwarded-Headern |
|
| `AUTH_TRUST_HOST` | `true` — Auth.js vertraut den Forwarded-Headern |
|
||||||
| `AUTH_SECRET` | >= 32 Zeichen (`openssl rand -base64 32`) |
|
| `AUTH_SECRET` | >= 32 Zeichen (`openssl rand -base64 32`) |
|
||||||
| `AUTHENTIK_ISSUER` | OIDC-Issuer-URL der Authentik-Anwendung |
|
| `AUTHENTIK_ISSUER` | OIDC-Issuer-URL der Authentik-Anwendung |
|
||||||
| `AUTHENTIK_CLIENT_ID` | Client-ID der Authentik-Anwendung |
|
| `AUTHENTIK_CLIENT_ID` | Client-ID der Authentik-Anwendung |
|
||||||
| `AUTHENTIK_CLIENT_SECRET` | Client-Secret der Authentik-Anwendung |
|
| `AUTHENTIK_CLIENT_SECRET` | Client-Secret der Authentik-Anwendung |
|
||||||
|
| `AUTHENTIK_ADMIN_GROUP` | Authentik-Gruppe → platform_admin (Default `floriannetz-admins`; s. authentik-setup.md) |
|
||||||
| `DATABASE_URL` | wird in Compose aus `POSTGRES_*` zusammengesetzt |
|
| `DATABASE_URL` | wird in Compose aus `POSTGRES_*` zusammengesetzt |
|
||||||
| `TRAEFIK_CERTRESOLVER` | Name des Traefik-Zertifikatsauflösers |
|
| `TRAEFIK_CERTRESOLVER` | Name des Traefik-Zertifikatsauflösers |
|
||||||
| `TRAEFIK_NETWORK` | Name des externen Traefik-Netzes (Default `traefik`) |
|
| `TRAEFIK_NETWORK` | Name des externen Traefik-Netzes (Default `frontend`) |
|
||||||
|
|
||||||
## Forwarded-Header & sichere Cookies
|
## Forwarded-Header & sichere Cookies
|
||||||
|
|
||||||
@@ -86,6 +90,10 @@ https://${APP_HOST}/api/auth/callback/authentik
|
|||||||
Der Pfad `callback/authentik` entspricht dem NextAuth-Provider-Namen. Bei lokaler
|
Der Pfad `callback/authentik` entspricht dem NextAuth-Provider-Namen. Bei lokaler
|
||||||
Entwicklung zusätzlich `http://localhost:3000/api/auth/callback/authentik`.
|
Entwicklung zusätzlich `http://localhost:3000/api/auth/callback/authentik`.
|
||||||
|
|
||||||
|
**Admin-Zugang über Gruppe:** Dem Provider muss das `groups`-Scope-Mapping
|
||||||
|
zugewiesen sein, und es muss die Gruppe aus `AUTHENTIK_ADMIN_GROUP` existieren —
|
||||||
|
nur deren Mitglieder werden `platform_admin`. Details: `authentik-setup.md`.
|
||||||
|
|
||||||
## Health-Check & Middleware-Allowlist
|
## Health-Check & Middleware-Allowlist
|
||||||
|
|
||||||
`GET /api/health` ist **öffentlich** (anonym `200`, nur Liveness, keine
|
`GET /api/health` ist **öffentlich** (anonym `200`, nur Liveness, keine
|
||||||
@@ -107,7 +115,7 @@ App-Healthcheck pingen `http://127.0.0.1:3000/api/health`.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env # Werte setzen (APP_HOST, AUTH_*, AUTHENTIK_*, POSTGRES_*)
|
cp .env.example .env # Werte setzen (APP_HOST, AUTH_*, AUTHENTIK_*, POSTGRES_*)
|
||||||
docker network create traefik # einmalig, falls nicht vorhanden
|
docker network create frontend # einmalig, falls nicht vorhanden
|
||||||
make data # einmalig: OSRM-Geodaten vorbereiten (groß, dauert)
|
make data # einmalig: OSRM-Geodaten vorbereiten (groß, dauert)
|
||||||
make deploy # build + up
|
make deploy # build + up
|
||||||
```
|
```
|
||||||
|
|||||||
650
package-lock.json
generated
650
package-lock.json
generated
@@ -34,6 +34,7 @@
|
|||||||
"@types/pg": "^8.11.11",
|
"@types/pg": "^8.11.11",
|
||||||
"@types/react": "^19.0.10",
|
"@types/react": "^19.0.10",
|
||||||
"@types/react-dom": "^19.0.4",
|
"@types/react-dom": "^19.0.4",
|
||||||
|
"@vitest/coverage-v8": "^3.2.6",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"drizzle-kit": "^0.30.4",
|
"drizzle-kit": "^0.30.4",
|
||||||
"eslint": "^9.21.0",
|
"eslint": "^9.21.0",
|
||||||
@@ -59,6 +60,20 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@ampproject/remapping": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://npm.apple.com/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/gen-mapping": "^0.3.5",
|
||||||
|
"@jridgewell/trace-mapping": "^0.3.24"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@auth/core": {
|
"node_modules/@auth/core": {
|
||||||
"version": "0.41.2",
|
"version": "0.41.2",
|
||||||
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/@auth/core/-/core-0.41.2.tgz",
|
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/@auth/core/-/core-0.41.2.tgz",
|
||||||
@@ -88,6 +103,64 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/helper-string-parser": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helper-validator-identifier": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://npm.apple.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/parser": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://npm.apple.com/@babel/parser/-/parser-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/types": "^7.29.7"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"parser": "bin/babel-parser.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/types": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://npm.apple.com/@babel/types/-/types-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/helper-string-parser": "^7.29.7",
|
||||||
|
"@babel/helper-validator-identifier": "^7.29.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@bcoe/v8-coverage": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://npm.apple.com/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@drizzle-team/brocli": {
|
"node_modules/@drizzle-team/brocli": {
|
||||||
"version": "0.10.2",
|
"version": "0.10.2",
|
||||||
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/@drizzle-team/brocli/-/brocli-0.10.2.tgz",
|
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/@drizzle-team/brocli/-/brocli-0.10.2.tgz",
|
||||||
@@ -1687,6 +1760,33 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@isaacs/cliui": {
|
||||||
|
"version": "8.0.2",
|
||||||
|
"resolved": "https://npm.apple.com/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||||
|
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"string-width": "^5.1.2",
|
||||||
|
"string-width-cjs": "npm:string-width@^4.2.0",
|
||||||
|
"strip-ansi": "^7.0.1",
|
||||||
|
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
|
||||||
|
"wrap-ansi": "^8.1.0",
|
||||||
|
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@istanbuljs/schema": {
|
||||||
|
"version": "0.1.6",
|
||||||
|
"resolved": "https://npm.apple.com/@istanbuljs/schema/-/schema-0.1.6.tgz",
|
||||||
|
"integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@jridgewell/gen-mapping": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
"version": "0.3.13",
|
"version": "0.3.13",
|
||||||
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||||
@@ -2211,6 +2311,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@pkgjs/parseargs": {
|
||||||
|
"version": "0.11.0",
|
||||||
|
"resolved": "https://npm.apple.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||||
|
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@playwright/test": {
|
"node_modules/@playwright/test": {
|
||||||
"version": "1.60.0",
|
"version": "1.60.0",
|
||||||
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/@playwright/test/-/test-1.60.0.tgz",
|
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/@playwright/test/-/test-1.60.0.tgz",
|
||||||
@@ -4011,6 +4121,39 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@vitest/coverage-v8": {
|
||||||
|
"version": "3.2.6",
|
||||||
|
"resolved": "https://npm.apple.com/@vitest/coverage-v8/-/coverage-v8-3.2.6.tgz",
|
||||||
|
"integrity": "sha512-LsAdmUapA0qSN306d8+zOyawM0hFm2m2Hg9IwVNIKBm+qJV8cijiq2c+gxKZcB1HCfIWAy+0qEZDCUQA58A1cw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@ampproject/remapping": "^2.3.0",
|
||||||
|
"@bcoe/v8-coverage": "^1.0.2",
|
||||||
|
"ast-v8-to-istanbul": "^0.3.3",
|
||||||
|
"debug": "^4.4.1",
|
||||||
|
"istanbul-lib-coverage": "^3.2.2",
|
||||||
|
"istanbul-lib-report": "^3.0.1",
|
||||||
|
"istanbul-lib-source-maps": "^5.0.6",
|
||||||
|
"istanbul-reports": "^3.1.7",
|
||||||
|
"magic-string": "^0.30.17",
|
||||||
|
"magicast": "^0.3.5",
|
||||||
|
"std-env": "^3.9.0",
|
||||||
|
"test-exclude": "^7.0.1",
|
||||||
|
"tinyrainbow": "^2.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@vitest/browser": "3.2.6",
|
||||||
|
"vitest": "3.2.6"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@vitest/browser": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vitest/expect": {
|
"node_modules/@vitest/expect": {
|
||||||
"version": "3.2.6",
|
"version": "3.2.6",
|
||||||
"resolved": "https://npm.apple.com/@vitest/expect/-/expect-3.2.6.tgz",
|
"resolved": "https://npm.apple.com/@vitest/expect/-/expect-3.2.6.tgz",
|
||||||
@@ -4160,6 +4303,19 @@
|
|||||||
"url": "https://github.com/sponsors/epoberezkin"
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ansi-regex": {
|
||||||
|
"version": "6.2.2",
|
||||||
|
"resolved": "https://npm.apple.com/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||||
|
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ansi-styles": {
|
"node_modules/ansi-styles": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://npm.apple.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
"resolved": "https://npm.apple.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
@@ -4397,6 +4553,25 @@
|
|||||||
"integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==",
|
"integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/ast-v8-to-istanbul": {
|
||||||
|
"version": "0.3.12",
|
||||||
|
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz",
|
||||||
|
"integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/trace-mapping": "^0.3.31",
|
||||||
|
"estree-walker": "^3.0.3",
|
||||||
|
"js-tokens": "^10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ast-v8-to-istanbul/node_modules/js-tokens": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://npm.apple.com/js-tokens/-/js-tokens-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/async-function": {
|
"node_modules/async-function": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://npm.apple.com/async-function/-/async-function-1.0.0.tgz",
|
"resolved": "https://npm.apple.com/async-function/-/async-function-1.0.0.tgz",
|
||||||
@@ -5155,6 +5330,12 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eastasianwidth": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://npm.apple.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.368",
|
"version": "1.5.368",
|
||||||
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/electron-to-chromium/-/electron-to-chromium-1.5.368.tgz",
|
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/electron-to-chromium/-/electron-to-chromium-1.5.368.tgz",
|
||||||
@@ -5977,6 +6158,22 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/foreground-child": {
|
||||||
|
"version": "3.3.1",
|
||||||
|
"resolved": "https://npm.apple.com/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||||
|
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"cross-spawn": "^7.0.6",
|
||||||
|
"signal-exit": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fraction.js": {
|
"node_modules/fraction.js": {
|
||||||
"version": "5.3.4",
|
"version": "5.3.4",
|
||||||
"resolved": "https://npm.apple.com/fraction.js/-/fraction.js-5.3.4.tgz",
|
"resolved": "https://npm.apple.com/fraction.js/-/fraction.js-5.3.4.tgz",
|
||||||
@@ -6176,6 +6373,27 @@
|
|||||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/glob": {
|
||||||
|
"version": "10.5.0",
|
||||||
|
"resolved": "https://npm.apple.com/glob/-/glob-10.5.0.tgz",
|
||||||
|
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
||||||
|
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"foreground-child": "^3.1.0",
|
||||||
|
"jackspeak": "^3.1.2",
|
||||||
|
"minimatch": "^9.0.4",
|
||||||
|
"minipass": "^7.1.2",
|
||||||
|
"package-json-from-dist": "^1.0.0",
|
||||||
|
"path-scurry": "^1.11.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"glob": "dist/esm/bin.mjs"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/glob-parent": {
|
"node_modules/glob-parent": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://npm.apple.com/glob-parent/-/glob-parent-6.0.2.tgz",
|
"resolved": "https://npm.apple.com/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||||
@@ -6188,6 +6406,30 @@
|
|||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/glob/node_modules/brace-expansion": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://npm.apple.com/brace-expansion/-/brace-expansion-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/glob/node_modules/minimatch": {
|
||||||
|
"version": "9.0.9",
|
||||||
|
"resolved": "https://npm.apple.com/minimatch/-/minimatch-9.0.9.tgz",
|
||||||
|
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16 || 14 >=14.17"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/globals": {
|
"node_modules/globals": {
|
||||||
"version": "14.0.0",
|
"version": "14.0.0",
|
||||||
"resolved": "https://npm.apple.com/globals/-/globals-14.0.0.tgz",
|
"resolved": "https://npm.apple.com/globals/-/globals-14.0.0.tgz",
|
||||||
@@ -6317,6 +6559,13 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/html-escaper": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://npm.apple.com/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://npm.apple.com/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://npm.apple.com/ignore/-/ignore-5.3.2.tgz",
|
||||||
@@ -6541,6 +6790,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-fullwidth-code-point": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://npm.apple.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-generator-function": {
|
"node_modules/is-generator-function": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://npm.apple.com/is-generator-function/-/is-generator-function-1.1.2.tgz",
|
"resolved": "https://npm.apple.com/is-generator-function/-/is-generator-function-1.1.2.tgz",
|
||||||
@@ -6770,6 +7028,58 @@
|
|||||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/istanbul-lib-coverage": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://npm.apple.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/istanbul-lib-report": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://npm.apple.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"istanbul-lib-coverage": "^3.0.0",
|
||||||
|
"make-dir": "^4.0.0",
|
||||||
|
"supports-color": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/istanbul-lib-source-maps": {
|
||||||
|
"version": "5.0.6",
|
||||||
|
"resolved": "https://npm.apple.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz",
|
||||||
|
"integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/trace-mapping": "^0.3.23",
|
||||||
|
"debug": "^4.1.1",
|
||||||
|
"istanbul-lib-coverage": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/istanbul-reports": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"html-escaper": "^2.0.0",
|
||||||
|
"istanbul-lib-report": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/iterator.prototype": {
|
"node_modules/iterator.prototype": {
|
||||||
"version": "1.1.5",
|
"version": "1.1.5",
|
||||||
"resolved": "https://npm.apple.com/iterator.prototype/-/iterator.prototype-1.1.5.tgz",
|
"resolved": "https://npm.apple.com/iterator.prototype/-/iterator.prototype-1.1.5.tgz",
|
||||||
@@ -6787,6 +7097,22 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jackspeak": {
|
||||||
|
"version": "3.4.3",
|
||||||
|
"resolved": "https://npm.apple.com/jackspeak/-/jackspeak-3.4.3.tgz",
|
||||||
|
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@isaacs/cliui": "^8.0.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@pkgjs/parseargs": "^0.11.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jiti": {
|
"node_modules/jiti": {
|
||||||
"version": "1.21.7",
|
"version": "1.21.7",
|
||||||
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/jiti/-/jiti-1.21.7.tgz",
|
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/jiti/-/jiti-1.21.7.tgz",
|
||||||
@@ -6979,6 +7305,12 @@
|
|||||||
"integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
|
"integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/lru-cache": {
|
||||||
|
"version": "10.4.3",
|
||||||
|
"resolved": "https://npm.apple.com/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||||
|
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.21",
|
"version": "0.30.21",
|
||||||
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/magic-string/-/magic-string-0.30.21.tgz",
|
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
@@ -6989,6 +7321,33 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/magicast": {
|
||||||
|
"version": "0.3.5",
|
||||||
|
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/magicast/-/magicast-0.3.5.tgz",
|
||||||
|
"integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/parser": "^7.25.4",
|
||||||
|
"@babel/types": "^7.25.4",
|
||||||
|
"source-map-js": "^1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/make-dir": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://npm.apple.com/make-dir/-/make-dir-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"semver": "^7.5.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
@@ -7043,6 +7402,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/minipass": {
|
||||||
|
"version": "7.1.3",
|
||||||
|
"resolved": "https://npm.apple.com/minipass/-/minipass-7.1.3.tgz",
|
||||||
|
"integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16 || 14 >=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://npm.apple.com/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://npm.apple.com/ms/-/ms-2.1.3.tgz",
|
||||||
@@ -7449,6 +7817,13 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/package-json-from-dist": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BlueOak-1.0.0"
|
||||||
|
},
|
||||||
"node_modules/parent-module": {
|
"node_modules/parent-module": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://npm.apple.com/parent-module/-/parent-module-1.0.1.tgz",
|
"resolved": "https://npm.apple.com/parent-module/-/parent-module-1.0.1.tgz",
|
||||||
@@ -7485,6 +7860,22 @@
|
|||||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/path-scurry": {
|
||||||
|
"version": "1.11.1",
|
||||||
|
"resolved": "https://npm.apple.com/path-scurry/-/path-scurry-1.11.1.tgz",
|
||||||
|
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"lru-cache": "^10.2.0",
|
||||||
|
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16 || 14 >=14.18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pathe": {
|
"node_modules/pathe": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://npm.apple.com/pathe/-/pathe-2.0.3.tgz",
|
"resolved": "https://npm.apple.com/pathe/-/pathe-2.0.3.tgz",
|
||||||
@@ -8625,6 +9016,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/signal-exit": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://npm.apple.com/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map": {
|
"node_modules/source-map": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
"resolved": "https://npm.apple.com/source-map/-/source-map-0.6.1.tgz",
|
"resolved": "https://npm.apple.com/source-map/-/source-map-0.6.1.tgz",
|
||||||
@@ -8697,6 +9100,68 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/string-width": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://npm.apple.com/string-width/-/string-width-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"eastasianwidth": "^0.2.0",
|
||||||
|
"emoji-regex": "^9.2.2",
|
||||||
|
"strip-ansi": "^7.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/string-width-cjs": {
|
||||||
|
"name": "string-width",
|
||||||
|
"version": "4.2.3",
|
||||||
|
"resolved": "https://npm.apple.com/string-width/-/string-width-4.2.3.tgz",
|
||||||
|
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"emoji-regex": "^8.0.0",
|
||||||
|
"is-fullwidth-code-point": "^3.0.0",
|
||||||
|
"strip-ansi": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/string-width-cjs/node_modules/ansi-regex": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/string-width-cjs/node_modules/emoji-regex": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/string-width-cjs/node_modules/strip-ansi": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://npm.apple.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/string.prototype.includes": {
|
"node_modules/string.prototype.includes": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://npm.apple.com/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
|
"resolved": "https://npm.apple.com/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
|
||||||
@@ -8805,6 +9270,46 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/strip-ansi": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://npm.apple.com/strip-ansi/-/strip-ansi-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^6.2.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/strip-ansi-cjs": {
|
||||||
|
"name": "strip-ansi",
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://npm.apple.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/strip-bom": {
|
"node_modules/strip-bom": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/strip-bom/-/strip-bom-3.0.0.tgz",
|
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/strip-bom/-/strip-bom-3.0.0.tgz",
|
||||||
@@ -9012,6 +9517,57 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/test-exclude": {
|
||||||
|
"version": "7.0.2",
|
||||||
|
"resolved": "https://npm.apple.com/test-exclude/-/test-exclude-7.0.2.tgz",
|
||||||
|
"integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@istanbuljs/schema": "^0.1.2",
|
||||||
|
"glob": "^10.4.1",
|
||||||
|
"minimatch": "^10.2.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/test-exclude/node_modules/balanced-match": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://npm.apple.com/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/test-exclude/node_modules/brace-expansion": {
|
||||||
|
"version": "5.0.6",
|
||||||
|
"resolved": "https://npm.apple.com/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
||||||
|
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^4.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/test-exclude/node_modules/minimatch": {
|
||||||
|
"version": "10.2.5",
|
||||||
|
"resolved": "https://npm.apple.com/minimatch/-/minimatch-10.2.5.tgz",
|
||||||
|
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^5.0.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/thenify": {
|
"node_modules/thenify": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://npm.apple.com/thenify/-/thenify-3.3.1.tgz",
|
"resolved": "https://npm.apple.com/thenify/-/thenify-3.3.1.tgz",
|
||||||
@@ -10495,6 +11051,7 @@
|
|||||||
"resolved": "https://npm.apple.com/vitest/-/vitest-3.2.6.tgz",
|
"resolved": "https://npm.apple.com/vitest/-/vitest-3.2.6.tgz",
|
||||||
"integrity": "sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==",
|
"integrity": "sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/chai": "^5.2.2",
|
"@types/chai": "^5.2.2",
|
||||||
"@vitest/expect": "3.2.6",
|
"@vitest/expect": "3.2.6",
|
||||||
@@ -10702,6 +11259,99 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wrap-ansi": {
|
||||||
|
"version": "8.1.0",
|
||||||
|
"resolved": "https://npm.apple.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||||
|
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^6.1.0",
|
||||||
|
"string-width": "^5.0.1",
|
||||||
|
"strip-ansi": "^7.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi-cjs": {
|
||||||
|
"name": "wrap-ansi",
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://npm.apple.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.0.0",
|
||||||
|
"string-width": "^4.1.0",
|
||||||
|
"strip-ansi": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
|
||||||
|
"version": "4.2.3",
|
||||||
|
"resolved": "https://npm.apple.com/string-width/-/string-width-4.2.3.tgz",
|
||||||
|
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"emoji-regex": "^8.0.0",
|
||||||
|
"is-fullwidth-code-point": "^3.0.0",
|
||||||
|
"strip-ansi": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://npm.apple.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi/node_modules/ansi-styles": {
|
||||||
|
"version": "6.2.3",
|
||||||
|
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/ansi-styles/-/ansi-styles-6.2.3.tgz",
|
||||||
|
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/xtend": {
|
"node_modules/xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/xtend/-/xtend-4.0.2.tgz",
|
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/xtend/-/xtend-4.0.2.tgz",
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
"@types/pg": "^8.11.11",
|
"@types/pg": "^8.11.11",
|
||||||
"@types/react": "^19.0.10",
|
"@types/react": "^19.0.10",
|
||||||
"@types/react-dom": "^19.0.4",
|
"@types/react-dom": "^19.0.4",
|
||||||
|
"@vitest/coverage-v8": "^3.2.6",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"drizzle-kit": "^0.30.4",
|
"drizzle-kit": "^0.30.4",
|
||||||
"eslint": "^9.21.0",
|
"eslint": "^9.21.0",
|
||||||
|
|||||||
3
public/.gitkeep
Normal file
3
public/.gitkeep
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Platzhalter, damit das public/-Verzeichnis existiert und vom Docker-Build
|
||||||
|
# (COPY /app/public ./public) sowie von Next.js (statische Assets) genutzt werden
|
||||||
|
# kann. Statische Dateien (z. B. favicon.ico, robots.txt) hier ablegen.
|
||||||
@@ -30,6 +30,9 @@ export const authConfig = {
|
|||||||
issuer: process.env.AUTHENTIK_ISSUER!,
|
issuer: process.env.AUTHENTIK_ISSUER!,
|
||||||
clientId: process.env.AUTHENTIK_CLIENT_ID!,
|
clientId: process.env.AUTHENTIK_CLIENT_ID!,
|
||||||
clientSecret: process.env.AUTHENTIK_CLIENT_SECRET!,
|
clientSecret: process.env.AUTHENTIK_CLIENT_SECRET!,
|
||||||
|
// `groups`-Claim anfordern, damit der Admin-Zugang über die
|
||||||
|
// Authentik-Gruppenmitgliedschaft gesteuert werden kann (signIn-Callback).
|
||||||
|
authorization: { params: { scope: "openid email profile groups" } },
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
callbacks: {
|
callbacks: {
|
||||||
|
|||||||
66
src/auth.ts
66
src/auth.ts
@@ -7,12 +7,47 @@ import { users } from "@/db/schema";
|
|||||||
import { authConfig } from "./auth.config";
|
import { authConfig } from "./auth.config";
|
||||||
import { verifyPassword } from "@/lib/auth/password";
|
import { verifyPassword } from "@/lib/auth/password";
|
||||||
import { checkRateLimit, recordAttempt } from "@/lib/auth/rate-limit";
|
import { checkRateLimit, recordAttempt } from "@/lib/auth/rate-limit";
|
||||||
|
import { extractGroups, isAdminGroupMember } from "@/lib/auth/authentik";
|
||||||
|
import { ROLES } from "@/lib/auth/roles";
|
||||||
|
import { env } from "@/lib/env";
|
||||||
|
|
||||||
const credSchema = z.object({
|
const credSchema = z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
password: z.string().min(1),
|
password: z.string().min(1),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stellt sicher, dass ein Authentik-Admin (Mitglied der Admin-Gruppe) als
|
||||||
|
* platform_admin in `users` existiert — für eine stabile id (Audit/FKs).
|
||||||
|
* Idempotent über die eindeutige E-Mail; benötigt KEIN vorheriges Seeding.
|
||||||
|
*/
|
||||||
|
async function upsertAuthentikAdmin(email: string, name: string | null) {
|
||||||
|
const normalized = email.toLowerCase();
|
||||||
|
const rows = await db
|
||||||
|
.insert(users)
|
||||||
|
.values({
|
||||||
|
email: normalized,
|
||||||
|
name: name ?? normalized,
|
||||||
|
rolle: ROLES.PLATFORM_ADMIN,
|
||||||
|
authTyp: "authentik",
|
||||||
|
aktiv: true,
|
||||||
|
brigadeId: null,
|
||||||
|
})
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: users.email,
|
||||||
|
set: {
|
||||||
|
rolle: ROLES.PLATFORM_ADMIN,
|
||||||
|
authTyp: "authentik",
|
||||||
|
aktiv: true,
|
||||||
|
...(name ? { name } : {}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
const row = rows[0];
|
||||||
|
if (!row) throw new Error("Authentik-Admin-Upsert lieferte keine Zeile");
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
export const { handlers, auth, signIn, signOut } = NextAuth({
|
export const { handlers, auth, signIn, signOut } = NextAuth({
|
||||||
...authConfig,
|
...authConfig,
|
||||||
providers: [
|
providers: [
|
||||||
@@ -50,17 +85,32 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
|
|||||||
],
|
],
|
||||||
callbacks: {
|
callbacks: {
|
||||||
...authConfig.callbacks,
|
...authConfig.callbacks,
|
||||||
// Authentik-Login-Gate: nur vorgemerkte, aktive authentik-Konten zulassen.
|
// Authentik-Login = Admin-Zugang, gesteuert über die Authentik-GRUPPE:
|
||||||
async signIn({ user, account }) {
|
// Nur Mitglieder von AUTHENTIK_ADMIN_GROUP dürfen rein und werden
|
||||||
|
// (idempotent) als platform_admin angelegt. Alle anderen werden abgewiesen.
|
||||||
|
async signIn({ user, account, profile }) {
|
||||||
if (account?.provider === "authentik") {
|
if (account?.provider === "authentik") {
|
||||||
const email = user.email;
|
const email = user.email;
|
||||||
if (!email) return false;
|
if (!email) {
|
||||||
const u = await db.query.users.findFirst({
|
console.warn("[auth] Authentik-Login ohne E-Mail abgelehnt.");
|
||||||
where: eq(users.email, email),
|
return false;
|
||||||
});
|
}
|
||||||
if (!u || !u.aktiv || u.authTyp !== "authentik") return false;
|
const groups = extractGroups(profile);
|
||||||
|
if (!isAdminGroupMember(groups, env.AUTHENTIK_ADMIN_GROUP)) {
|
||||||
|
console.warn(
|
||||||
|
`[auth] Authentik-Login abgelehnt: "${email}" ist nicht in Gruppe ` +
|
||||||
|
`"${env.AUTHENTIK_ADMIN_GROUP}". Erhaltene Gruppen: ${JSON.stringify(groups)}` +
|
||||||
|
(groups.length
|
||||||
|
? ""
|
||||||
|
: " — leer: vermutlich fehlt das 'groups'-Scope-Mapping im " +
|
||||||
|
"Authentik-Provider (oder der 'groups'-Scope wird nicht angefragt)."),
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const u = await upsertAuthentikAdmin(email, user.name ?? null);
|
||||||
|
user.id = u.id;
|
||||||
user.role = u.rolle;
|
user.role = u.rolle;
|
||||||
user.brigadeId = u.brigadeId ?? null;
|
user.brigadeId = null;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|||||||
|
|
||||||
const VALID_ENV = {
|
const VALID_ENV = {
|
||||||
NODE_ENV: "test",
|
NODE_ENV: "test",
|
||||||
DATABASE_URL: "postgres://user:pass@localhost:5432/floriannetz",
|
DATABASE_URL: "postgresql://user:pass@localhost:5432/floriannetz",
|
||||||
AUTH_SECRET: "x".repeat(32),
|
AUTH_SECRET: "x".repeat(32),
|
||||||
AUTH_URL: "http://localhost:3000",
|
AUTH_URL: "http://localhost:3000",
|
||||||
AUTHENTIK_ISSUER: "http://localhost:9000/application/o/floriannetz/",
|
AUTHENTIK_ISSUER: "http://localhost:9000/application/o/floriannetz/",
|
||||||
|
|||||||
38
src/lib/auth/__tests__/authentik.test.ts
Normal file
38
src/lib/auth/__tests__/authentik.test.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { extractGroups, isAdminGroupMember } from "@/lib/auth/authentik";
|
||||||
|
|
||||||
|
describe("isAdminGroupMember", () => {
|
||||||
|
it("true, wenn die Admin-Gruppe enthalten ist", () => {
|
||||||
|
expect(
|
||||||
|
isAdminGroupMember(["wehr-x", "floriannetz-admins"], "floriannetz-admins"),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("false, wenn die Admin-Gruppe fehlt", () => {
|
||||||
|
expect(isAdminGroupMember(["wehr-x"], "floriannetz-admins")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("false bei leerer Gruppenliste", () => {
|
||||||
|
expect(isAdminGroupMember([], "floriannetz-admins")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("extractGroups", () => {
|
||||||
|
it("liest den groups-Claim", () => {
|
||||||
|
expect(extractGroups({ sub: "1", groups: ["a", "b"] })).toEqual(["a", "b"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leeres Array, wenn kein groups-Claim vorhanden ist", () => {
|
||||||
|
expect(extractGroups({ sub: "1" })).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leeres Array bei undefined/null", () => {
|
||||||
|
expect(extractGroups(undefined)).toEqual([]);
|
||||||
|
expect(extractGroups(null)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("defensiv: ignoriert nicht-string-Arrays", () => {
|
||||||
|
expect(extractGroups({ groups: "kein-array" })).toEqual([]);
|
||||||
|
expect(extractGroups({ groups: [1, 2, 3] })).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
25
src/lib/auth/authentik.ts
Normal file
25
src/lib/auth/authentik.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reine Helfer für die Authentik-Gruppensteuerung des Admin-Zugangs.
|
||||||
|
* Bewusst OHNE DB-/Node-Importe, damit sie isoliert unit-testbar sind und
|
||||||
|
* auch im Edge-Pfad unbedenklich wären.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const profileWithGroups = z
|
||||||
|
.object({ groups: z.array(z.string()).optional() })
|
||||||
|
.passthrough();
|
||||||
|
|
||||||
|
/** Extrahiert den `groups`-Claim aus dem Authentik-OIDC-Profil (defensiv). */
|
||||||
|
export function extractGroups(profile: unknown): string[] {
|
||||||
|
const parsed = profileWithGroups.safeParse(profile);
|
||||||
|
return parsed.success && parsed.data.groups ? parsed.data.groups : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Entscheidung: Ist der Benutzer Mitglied der konfigurierten Admin-Gruppe? */
|
||||||
|
export function isAdminGroupMember(
|
||||||
|
groups: readonly string[],
|
||||||
|
adminGroup: string,
|
||||||
|
): boolean {
|
||||||
|
return groups.includes(adminGroup);
|
||||||
|
}
|
||||||
@@ -10,6 +10,8 @@ const serverSchema = z.object({
|
|||||||
AUTHENTIK_ISSUER: z.string().url(),
|
AUTHENTIK_ISSUER: z.string().url(),
|
||||||
AUTHENTIK_CLIENT_ID: z.string().min(1),
|
AUTHENTIK_CLIENT_ID: z.string().min(1),
|
||||||
AUTHENTIK_CLIENT_SECRET: z.string().min(1),
|
AUTHENTIK_CLIENT_SECRET: z.string().min(1),
|
||||||
|
// Authentik-Gruppe, deren Mitglieder automatisch platform_admin werden.
|
||||||
|
AUTHENTIK_ADMIN_GROUP: z.string().min(1).default("floriannetz-admins"),
|
||||||
// Geo:
|
// Geo:
|
||||||
OSRM_URL: z.string().url().default("http://osrm:5000"),
|
OSRM_URL: z.string().url().default("http://osrm:5000"),
|
||||||
NOMINATIM_URL: z.string().url().default("http://nominatim:8080"),
|
NOMINATIM_URL: z.string().url().default("http://nominatim:8080"),
|
||||||
|
|||||||
@@ -61,4 +61,46 @@ describe("orderByEintreffzeit", () => {
|
|||||||
const result = await orderByEintreffzeit(origin, [a, b], etaTable);
|
const result = await orderByEintreffzeit(origin, [a, b], etaTable);
|
||||||
expect(result.map((r) => r.id)).toEqual(["a", "b"]);
|
expect(result.map((r) => r.id)).toEqual(["a", "b"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("(f) OSRM ohne Route fuer EINEN Kandidaten: einzelner Haversine-Fallback", async () => {
|
||||||
|
// OSRM liefert eine Tabelle, kennt aber fuer 'fern' keine Route (null).
|
||||||
|
// -> nur dieser Treffer faellt auf Haversine zurueck, 'nah' bleibt osrm.
|
||||||
|
const etaTable = vi.fn().mockResolvedValue({
|
||||||
|
durations: [[0, 120, null]], // origin -> [nah=120s, fern=keine Route]
|
||||||
|
distances: [[0, 3000, null]],
|
||||||
|
});
|
||||||
|
const result = await orderByEintreffzeit(origin, [nah, fern], etaTable);
|
||||||
|
const nahRes = result.find((r) => r.id === "nah")!;
|
||||||
|
const fernRes = result.find((r) => r.id === "fern")!;
|
||||||
|
expect(nahRes.eta.mode).toBe("osrm");
|
||||||
|
expect(nahRes.eta.isFallback).toBe(false);
|
||||||
|
expect(nahRes.eta.durationSec).toBe(120);
|
||||||
|
// 'fern' ohne OSRM-Route -> Haversine-Fallback mit gesetzter Dauer.
|
||||||
|
expect(fernRes.eta.mode).toBe("haversine");
|
||||||
|
expect(fernRes.eta.isFallback).toBe(true);
|
||||||
|
expect(fernRes.eta.durationSec).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("(g) nur Kandidaten ohne Koordinaten: OSRM wird nicht gerufen, alle ans Ende", async () => {
|
||||||
|
// coords.length === 0 -> computeEtas ruft etaTable gar nicht auf.
|
||||||
|
const ohne2: Hit = { id: "ohne2", brigadeCoords: null };
|
||||||
|
const etaTable = vi.fn().mockResolvedValue({ durations: [[0]], distances: [[0]] });
|
||||||
|
const result = await orderByEintreffzeit(origin, [ohne, ohne2], etaTable);
|
||||||
|
expect(etaTable).not.toHaveBeenCalled();
|
||||||
|
expect(result.map((r) => r.id)).toEqual(["ohne", "ohne2"]);
|
||||||
|
expect(result.every((r) => r.eta.durationSec === null)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("(h) OSRM-Tabelle ohne distances-Zeile: distanceMeters faellt auf null", async () => {
|
||||||
|
// distances fehlt komplett -> `?? []` greift, distanceMeters wird null,
|
||||||
|
// durationSec aus durations bleibt jedoch erhalten (mode=osrm).
|
||||||
|
const etaTable = vi.fn().mockResolvedValue({
|
||||||
|
durations: [[0, 90]],
|
||||||
|
// kein distances-Feld
|
||||||
|
});
|
||||||
|
const result = await orderByEintreffzeit(origin, [nah], etaTable);
|
||||||
|
expect(result[0]!.eta.mode).toBe("osrm");
|
||||||
|
expect(result[0]!.eta.durationSec).toBe(90);
|
||||||
|
expect(result[0]!.eta.distanceMeters).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,17 +2,19 @@
|
|||||||
* Sicherheits-Header, eingehängt in next.config.ts.
|
* Sicherheits-Header, eingehängt in next.config.ts.
|
||||||
*
|
*
|
||||||
* Content-Security-Policy ist der zentrale Querschnitts-Schutz (Implementierungs-
|
* Content-Security-Policy ist der zentrale Querschnitts-Schutz (Implementierungs-
|
||||||
* plan Z.1314): in Produktion strikt mit default-src 'self', frame-ancestors 'none'
|
* plan Z.1314): default-src 'self', frame-ancestors 'none', form-action 'self',
|
||||||
* und form-action 'self'. Im Dev-Modus benötigt Next.js (HMR/React-Refresh) eine
|
* object-src 'none'. script-src erlaubt 'unsafe-inline' (KEIN 'unsafe-eval' in
|
||||||
* gelockerte script-src/connect-src-Variante ('unsafe-eval' + ws: für den Dev-Socket).
|
* Prod), da Next.js (App Router) Inline-Bootstrap-/Hydration-Skripte ohne Nonce
|
||||||
|
* ausliefert — eine strikte nonce-basierte CSP ginge nur über die Middleware
|
||||||
|
* (Hardening-Option). Im Dev zusätzlich 'unsafe-eval' + ws: (HMR/React-Refresh).
|
||||||
*/
|
*/
|
||||||
const isProd = process.env.NODE_ENV === "production";
|
const isProd = process.env.NODE_ENV === "production";
|
||||||
|
|
||||||
const CSP = [
|
const CSP = [
|
||||||
"default-src 'self'",
|
"default-src 'self'",
|
||||||
// Dev braucht eval (React Refresh) + inline; Prod bleibt strikt.
|
// Next.js braucht Inline-Skripte (Bootstrap/Hydration, ohne Nonce); Dev zusätzlich eval.
|
||||||
isProd
|
isProd
|
||||||
? "script-src 'self'"
|
? "script-src 'self' 'unsafe-inline'"
|
||||||
: "script-src 'self' 'unsafe-eval' 'unsafe-inline'",
|
: "script-src 'self' 'unsafe-eval' 'unsafe-inline'",
|
||||||
"style-src 'self' 'unsafe-inline'",
|
"style-src 'self' 'unsafe-inline'",
|
||||||
"img-src 'self' data: blob:",
|
"img-src 'self' data: blob:",
|
||||||
|
|||||||
@@ -40,8 +40,11 @@ export function filePathToRoute(filePath: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isPublic(route: string): boolean {
|
function isPublic(route: string): boolean {
|
||||||
|
// Nur exakter Treffer oder echtes Unterpfad-Segment zählt als öffentlich.
|
||||||
|
// KEIN reiner String-Präfix: sonst wären z. B. "/loginhelp" oder
|
||||||
|
// "/api/healthz" fälschlich anonym erreichbar und entkämen dem Drift-Check.
|
||||||
return PUBLIC_ALLOWLIST.some(
|
return PUBLIC_ALLOWLIST.some(
|
||||||
(p) => route === p || route.startsWith(p + "/") || route.startsWith(p),
|
(p) => route === p || route.startsWith(p + "/"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,18 @@ describe("findUndeclaredRoutes", () => {
|
|||||||
expect(findUndeclaredRoutes(discovered, declared)).toEqual([]);
|
expect(findUndeclaredRoutes(discovered, declared)).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("flaggt Routen mit reinem String-Präfix als ungetestet (kein Pfadsegment)", () => {
|
||||||
|
const declared = new Set<string>();
|
||||||
|
// "/loginhelp" beginnt zwar mit "/login" und "/api/healthz" mit
|
||||||
|
// "/api/health", sind aber KEINE Unterpfade -> müssen gegated werden.
|
||||||
|
const discovered = ["/loginhelp", "/api/healthz", "/api/authentication"];
|
||||||
|
expect(findUndeclaredRoutes(discovered, declared)).toEqual([
|
||||||
|
"/loginhelp",
|
||||||
|
"/api/healthz",
|
||||||
|
"/api/authentication",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it("PUBLIC_ALLOWLIST enthält /api/health und /login", () => {
|
it("PUBLIC_ALLOWLIST enthält /api/health und /login", () => {
|
||||||
expect(PUBLIC_ALLOWLIST).toContain("/api/health");
|
expect(PUBLIC_ALLOWLIST).toContain("/api/health");
|
||||||
expect(PUBLIC_ALLOWLIST).toContain("/login");
|
expect(PUBLIC_ALLOWLIST).toContain("/login");
|
||||||
|
|||||||
@@ -19,9 +19,31 @@ export default defineConfig({
|
|||||||
coverage: {
|
coverage: {
|
||||||
provider: "v8",
|
provider: "v8",
|
||||||
// Querschnitt-Kern muss hoch abgedeckt sein (Definition of Done #7):
|
// Querschnitt-Kern muss hoch abgedeckt sein (Definition of Done #7):
|
||||||
// src/lib/search und src/lib/geo >= 90 %.
|
// die REINE, offline testbare Logik in src/lib/search und src/lib/geo
|
||||||
|
// >= 90 %. Reine I/O-Wrapper (DB-Queries via Drizzle, HTTP zu
|
||||||
|
// Nominatim/OSRM) sowie reine Typ-Module sind hier ausgenommen: sie
|
||||||
|
// brauchen ein laufendes Postgres bzw. erreichbare Dienste und werden
|
||||||
|
// über Integrations-/E2E-Tests abgesichert (im Sandbox deferred, da
|
||||||
|
// kein Postgres/Server verfügbar).
|
||||||
include: ["src/lib/search/**", "src/lib/geo/**"],
|
include: ["src/lib/search/**", "src/lib/geo/**"],
|
||||||
|
exclude: [
|
||||||
|
// Reine Typdefinitionen (keine ausführbaren Zeilen).
|
||||||
|
"src/lib/search/types.ts",
|
||||||
|
"src/lib/geo/types.ts",
|
||||||
|
// DB-gebundene Query-Builder (Drizzle gegen Postgres).
|
||||||
|
"src/lib/search/facets.ts",
|
||||||
|
"src/lib/search/query-brigades.ts",
|
||||||
|
"src/lib/search/query-equipment.ts",
|
||||||
|
"src/lib/search/query-vehicles.ts",
|
||||||
|
"src/lib/geo/candidates.ts",
|
||||||
|
// Externe HTTP-Dienste (Nominatim-Geocoding, OSRM-Routing).
|
||||||
|
"src/lib/geo/nominatim.ts",
|
||||||
|
"src/lib/geo/osrm.ts",
|
||||||
|
],
|
||||||
|
// Per-Datei statt global: jede eingeschlossene reine-Logik-Datei muss
|
||||||
|
// die Schwelle selbst erreichen (kein Verwässern über den Durchschnitt).
|
||||||
thresholds: {
|
thresholds: {
|
||||||
|
perFile: true,
|
||||||
lines: 90,
|
lines: 90,
|
||||||
functions: 90,
|
functions: 90,
|
||||||
statements: 90,
|
statements: 90,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// Es wird KEINE echte DB-Verbindung geöffnet (Pool ist lazy bis zur Query).
|
// Es wird KEINE echte DB-Verbindung geöffnet (Pool ist lazy bis zur Query).
|
||||||
const TEST_ENV: Record<string, string> = {
|
const TEST_ENV: Record<string, string> = {
|
||||||
NODE_ENV: "test",
|
NODE_ENV: "test",
|
||||||
DATABASE_URL: "postgres://test:test@localhost:5432/test",
|
DATABASE_URL: "postgresql://test:test@localhost:5432/test",
|
||||||
AUTH_SECRET: "test-secret-mindestens-32-zeichen-lang-xxxx",
|
AUTH_SECRET: "test-secret-mindestens-32-zeichen-lang-xxxx",
|
||||||
AUTH_URL: "http://localhost:3000",
|
AUTH_URL: "http://localhost:3000",
|
||||||
AUTH_TRUST_HOST: "true",
|
AUTH_TRUST_HOST: "true",
|
||||||
|
|||||||
Reference in New Issue
Block a user