diff --git a/connect/cmd/connect-app/main.go b/connect/cmd/connect-app/main.go index e334d8ad..a22b1ba0 100644 --- a/connect/cmd/connect-app/main.go +++ b/connect/cmd/connect-app/main.go @@ -1,11 +1,11 @@ -// Command connect-app is the first HTTP entrypoint for the NEXARCH -// Connect module. For now it only serves the connector status overview -// (API-02); further endpoints join this ServeMux as later tickets are -// implemented. +// Command connect-app is the HTTP entrypoint for the NEXARCH Connect +// module: connector status (API-02), DMS2 import (DMS2-01/02), and +// connector setup (API-01). package main import ( "context" + "encoding/base64" "log" "net/http" "os" @@ -15,6 +15,9 @@ import ( "github.com/jackc/pgx/v5/pgxpool" + "gitea.perlbach24.de/scripte/nexarch/connect/internal/connectorsetupapi" + "gitea.perlbach24.de/scripte/nexarch/connect/internal/credential" + "gitea.perlbach24.de/scripte/nexarch/connect/internal/crypto" "gitea.perlbach24.de/scripte/nexarch/connect/internal/dms2import" "gitea.perlbach24.de/scripte/nexarch/connect/internal/dms2importapi" "gitea.perlbach24.de/scripte/nexarch/connect/internal/fieldmapping" @@ -23,6 +26,15 @@ import ( "gitea.perlbach24.de/scripte/nexarch/connect/internal/syncjob" ) +// fixedKEK is a transitional KEKProvider: a single, operator-provisioned +// key for every tenant, used until API-10/API-12's real Core KMS client +// exists. The key never touches the code, only NEXARCH_CONNECT_APP_KEK. +type fixedKEK struct{ kek []byte } + +func (f fixedKEK) TenantKEK(ctx context.Context, tenantSlug string) ([]byte, error) { + return f.kek, nil +} + func main() { if !killswitch.Enabled() { log.Printf("connect-app: deaktiviert (%s=false), kein Start", killswitch.EnvEnabled) @@ -37,6 +49,14 @@ func main() { if addr == "" { addr = "127.0.0.1:9500" } + kekB64 := os.Getenv("NEXARCH_CONNECT_APP_KEK") + if kekB64 == "" { + log.Fatal("connect-app: NEXARCH_CONNECT_APP_KEK ist erforderlich (base64, 32 Bytes)") + } + kek, err := base64.StdEncoding.DecodeString(kekB64) + if err != nil || len(kek) != crypto.KEKSize { + log.Fatalf("connect-app: NEXARCH_CONNECT_APP_KEK ungueltig (erwartet base64-kodierte %d Bytes)", crypto.KEKSize) + } ctx := context.Background() pool, err := pgxpool.New(ctx, dsn) @@ -45,10 +65,14 @@ func main() { } defer pool.Close() + mappings := fieldmapping.NewStore(pool) + credentials := credential.NewStore(pool, crypto.NewService(fixedKEK{kek: kek})) + mux := http.NewServeMux() mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) statusapi.NewServer(syncjob.NewStore(pool)).RegisterRoutes(mux) - dms2importapi.NewServer(fieldmapping.NewStore(pool), dms2import.NewStore(pool)).RegisterRoutes(mux) + dms2importapi.NewServer(mappings, dms2import.NewStore(pool)).RegisterRoutes(mux) + connectorsetupapi.NewServer(credentials, mappings).RegisterRoutes(mux) srv := &http.Server{ Addr: addr, diff --git a/connect/cmd/setup-a11y-fixture/main.go b/connect/cmd/setup-a11y-fixture/main.go new file mode 100644 index 00000000..701a75e0 --- /dev/null +++ b/connect/cmd/setup-a11y-fixture/main.go @@ -0,0 +1,83 @@ +// Command setup-a11y-fixture wraps the real connect-app setup API +// (connectorsetupapi, API-01) with permissive CORS headers, exactly like +// status-a11y-fixture does for API-02: only this throwaway test binary +// allows cross-origin requests, so a browser-based run can serve the +// frontend and API on different localhost ports. The production +// connect-app binary is unchanged and relies on same-origin reverse-proxy +// routing, as every other module in this project does. +package main + +import ( + "context" + "encoding/base64" + "log" + "net/http" + "os" + "time" + + "github.com/jackc/pgx/v5/pgxpool" + + "gitea.perlbach24.de/scripte/nexarch/connect/internal/connectorsetupapi" + "gitea.perlbach24.de/scripte/nexarch/connect/internal/credential" + "gitea.perlbach24.de/scripte/nexarch/connect/internal/crypto" + "gitea.perlbach24.de/scripte/nexarch/connect/internal/fieldmapping" +) + +type fixedKEK struct{ kek []byte } + +func (f fixedKEK) TenantKEK(ctx context.Context, tenantSlug string) ([]byte, error) { + return f.kek, nil +} + +func cors(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, PUT, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "X-User-Id, X-Tenant-Slug, Content-Type") + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusNoContent) + return + } + next.ServeHTTP(w, r) + }) +} + +func main() { + dsn := os.Getenv("NEXARCH_CONNECT_APP_TENANT_DSN") + if dsn == "" { + log.Fatal("setup-a11y-fixture: NEXARCH_CONNECT_APP_TENANT_DSN ist erforderlich") + } + addr := os.Getenv("NEXARCH_CONNECT_APP_LISTEN_ADDR") + if addr == "" { + addr = "127.0.0.1:9500" + } + kekB64 := os.Getenv("NEXARCH_CONNECT_APP_KEK") + if kekB64 == "" { + log.Fatal("setup-a11y-fixture: NEXARCH_CONNECT_APP_KEK ist erforderlich (base64, 32 Bytes)") + } + kek, err := base64.StdEncoding.DecodeString(kekB64) + if err != nil || len(kek) != crypto.KEKSize { + log.Fatalf("setup-a11y-fixture: NEXARCH_CONNECT_APP_KEK ungueltig (erwartet base64-kodierte %d Bytes)", crypto.KEKSize) + } + pool, err := pgxpool.New(context.Background(), dsn) + if err != nil { + log.Fatalf("setup-a11y-fixture: datenbankverbindung: %v", err) + } + defer pool.Close() + + mux := http.NewServeMux() + credentials := credential.NewStore(pool, crypto.NewService(fixedKEK{kek: kek})) + connectorsetupapi.NewServer(credentials, fieldmapping.NewStore(pool)).RegisterRoutes(mux) + + srv := &http.Server{ + Addr: addr, + Handler: cors(mux), + ReadHeaderTimeout: 5 * time.Second, + ReadTimeout: 15 * time.Second, + WriteTimeout: 15 * time.Second, + IdleTimeout: 60 * time.Second, + } + + log.Printf("setup-a11y-fixture: listening on %s", addr) + log.Fatal(srv.ListenAndServe()) +} diff --git a/connect/docs/API-01-PRUEFPROTOKOLL.md b/connect/docs/API-01-PRUEFPROTOKOLL.md new file mode 100644 index 00000000..d05f25bb --- /dev/null +++ b/connect/docs/API-01-PRUEFPROTOKOLL.md @@ -0,0 +1,59 @@ +# API-01 – Connect-Konfigurationsoberfläche: Prüfprotokoll + +Stand: 2026-10-16, Branch `feature/api-01-01-connect-konfigurationsoberflaeche-connectoren-einrichten`, geprüft auf Entwicklungshost 204 (eigene Wegwerf-DBs `api01_connect`/`api01_connect_a11y`, Rolle `api01_connect_test`; `archivdms` unberührt). + +## Umsetzung + +- **Backend:** `connect/internal/connectorcatalog` (neu) ist der statische Katalog der Connector-Typen (`erp`, `crm`, `m365`, `backup`, `datev`), je mit Zugangsdaten-Art (API-Key/OAuth2) und NEXARCH-Feld-Schema für `fieldmapping`. `connect/internal/credential` bekommt `ListForTenant` und die reine Funktion `Mask` (feste Maske `••••` + letzte 4 Zeichen, ab 8 Zeichen Länge vollständig verdeckt) — Erweiterung von ENG-02, kein Eingriff in dessen bestehende Methoden. `connect/internal/connectorsetupapi` (neu) bindet beides an HTTP: Katalog lesen, Verbindungen anlegen/maskiert auflisten (ENG-02), Feld-Mapping lesen/speichern (ENG-04). Identität übergangsweise über `X-User-Id`/`X-Tenant-Slug`, gleiches Muster wie `statusapi`/`dms2importapi`. +- **Schlüsselverwaltung:** `connect-app` braucht für `credential.Store` einen KEK-Provider. Da API-10/API-12 (Core-KMS-Client) noch nicht existieren, liest `connect-app` einen einzigen, operatorseitig vorgegebenen Schlüssel aus `NEXARCH_CONNECT_APP_KEK` (base64, 32 Byte) — übergangsweise, bis der echte KMS-Client existiert, nie im Code. +- **Oberfläche:** `web/connect-setup` (Next.js/React, `@nexarch/shl`-Design-System, gleiches Turbopack-Symlink→Kopie-Muster wie `web/connect-status`). Eine Seite `/setup`: Formular „Neue Verbindung" (Connector-Typ, Instanz-Kennung, Zugangsdaten je nach Art), Tabelle „Eingerichtete Verbindungen" mit maskierten Zugangsdaten, Feld-Mapping-Editor je Verbindung (ein Textfeld je Schema-Feld, Pflichtfelder markiert). +- **Test-Fixture:** `connect/cmd/setup-a11y-fixture` — CORS-Kopie von `connect-app`, analog `status-a11y-fixture`. Der Produktionsdienst bleibt ohne CORS. + +## Akzeptanzkriterien + +1. **Connector lässt sich vollständig über die Oberfläche einrichten, ohne Konfigurationsdatei** — erfüllt. Connector-Typ, Instanz-Kennung, Zugangsdaten und Feld-Mapping werden ausschließlich über Formulare erfasst und per HTTP an `connectorsetupapi` gesendet; keine YAML-/Env-Datei pro Connector-Instanz nötig. +2. **Zugangsdaten werden nur maskiert angezeigt, nie im Klartext** — erfüllt. `credential.Mask` liefert serverseitig nur `••••` + höchstens 4 Zeichen; die Oberfläche zeigt ausschließlich diesen Wert, Eingabefelder für neue Zugangsdaten werden nach dem Speichern geleert statt den Wert zurückzuspiegeln. +3. **Feld-Mapping ist über die Oberfläche bearbeitbar** — erfüllt. Mapping-Editor liest den aktiven Stand (`fieldmapping.Store.GetActive`) und speichert Änderungen als neue Version (`Save`), inklusive Ablehnung bei ungültigem Mapping (ENG-04 AC2). + +## Prüfungen + +Alle drei Prüfungen wurden sowohl als automatisierter Test (Go-Integrationstest gegen echte Postgres-Wegwerf-DB, Vitest mit echtem Komponenten-Rendering) als auch in einem echten Browserlauf durchgeführt. + +1. **Vollständige Einrichtung eines Testconnectors über die Oberfläche erfolgreich** — bestanden. + - Go: `TestFullConnectorSetup_WithoutConfigFile` (`connect/internal/connectorsetupapi`) — Katalog abrufen, Verbindung `erp/acme-erp-1` per `PUT` anlegen, Mapping speichern, Verbindung in der Liste wiederfinden. + - Vitest: `richtet einen Testconnector vollstaendig ueber das Formular ein` — Formular ausgefüllt, abgesendet, neue Zeile in der Tabelle sichtbar. + - **Echter Browserlauf** (`web/a11y-audit/connect-setup-run.sh`, Chrome stable via puppeteer-core, echter `setup-a11y-fixture`-Dienst gegen echte Postgres-Daten): Connector-Typ „ERP-System" gewählt, Instanz-Kennung `browser-test-erp-1` und ein synthetischer API-Schlüssel eingegeben, Formular abgesendet — Ergebnis `OK: Pruefung 1`. +2. **Zugangsdaten in der Oberfläche im Test nachweislich maskiert** — bestanden. + - Go: `TestListConnections_CredentialsAreMasked` — Antwortkörper der Verbindungsliste enthält das eingegebene Geheimnis an keiner Stelle (`strings.Contains`-Prüfung), maskierter Wert beginnt mit `••••`. + - Vitest: `zeigt Zugangsdaten in der Verbindungsliste nur maskiert`. + - **Echter Browserlauf:** nach dem Anlegen der Verbindung enthält das gerenderte DOM (`page.content()`) das eingegebene Geheimnis `testconnector-geheimnis-7788991100` nachweislich nicht; die Tabelle zeigt `••••1100` — Ergebnis `OK: Pruefung 2`. +3. **Mapping-Änderung über die Oberfläche im Test korrekt übernommen** — bestanden. + - Go: `TestMapping_ChangeIsPickedUpOnNextRead` — Mapping speichern (Version 1), ändern (Version 2), erneut lesen liefert den geänderten Wert. + - Vitest: `uebernimmt eine Mapping-Aenderung korrekt` — Feld geändert, gespeichert, `saveMapping` mit den erwarteten Einträgen aufgerufen, neue Version angezeigt. + - **Echter Browserlauf:** Feld `kundennummer` auf `debitorennummer` gesetzt, gespeichert, Seite **neu geladen** (nicht nur lokaler React-State) und Mapping-Editor erneut geöffnet — der Wert `debitorennummer` ist weiterhin im Eingabefeld sichtbar — Ergebnis `OK: Pruefung 3`. + +## Läufe auf 204 + +- `go build ./...`, `go vet ./...`, `gofmt -l .` (für die neu/ geänderten Dateien): sauber. +- `go test -count=1 -p 1 ./...` (echte Postgres-Wegwerf-DB `api01_connect`, Rolle `api01_connect_test`): alle 30 Pakete `ok`, inkl. `connectorcatalog`, `connectorsetupapi` (neu) und `credential` (erweitert). +- `web/connect-setup`: `tsc --noEmit` sauber, `vitest run` 4/4, `next build` erfolgreich (Routen `/`, `/setup`). +- **Echter Browserlauf** (`web/a11y-audit/connect-setup-run.sh`, DB `api01_connect_a11y`): alle drei Formular-Interaktionsprüfungen `OK`, anschließender axe-Audit **0 Verstöße, 0 offene Befunde** in hell und dunkel, kein horizontaler Überlauf, Tastaturerreichbarkeit 5/5 interaktive Elemente, sichtbarer Fokusring, `lang="de"`. +- Rückbau: Ports 9591/4191 (Testlauf) sind nach Skriptende frei (`ss -ltnp` leer), kein Prozess- oder Verzeichnisrest auf 204. + +## QA und Entscheidungen + +- **Connector-Katalog als statische Liste im neuen Backend-Paket statt Erweiterung von `registry.Connector`:** `ENG-01`s Registrierungsschnittstelle kennt pro Instanz nur `Metadata()` (Name/Version/Operationen), kein Feld-Schema. Eine Erweiterung dieser Schnittstelle oder der bestehenden Connector-Pakete (`erpconnector`, `crmconnector`, …) hätte deren Scope verlassen (kein Teil von API-01). Der Katalog ist reine Beschreibungsdaten für die Oberfläche, ohne Datenbanktabelle — kleinste Lösung. +- **Übergangsweiser env-basierter KEK statt echtem KMS-Client:** `credential.Store` war seit ENG-02 nur in Tests verdrahtet, nie in einem laufenden Dienst — ohne einen KEKProvider lässt sich die Oberfläche nicht real betreiben. API-10/API-12 (echter Core-KMS-Client) sind eigene, noch nicht gemergte Tickets; ein einzelner, aus `NEXARCH_CONNECT_APP_KEK` gelesener Schlüssel schließt genau diese Lücke, ohne deren Scope vorzugreifen. +- **Maskierung als feste Zeichenkette (`••••` + max. 4 Zeichen, ab 8 Zeichen Länge volle Verdeckung) statt reiner Längenangabe:** verhindert, dass die maskierte Anzeige selbst die Geheimnislänge verrät, und bleibt trotzdem erkennbar genug, um mehrere Verbindungen gleichen Typs zu unterscheiden. +- **Eigener Browser-Interaktions-Lauf (`connect-setup-interaction-check.js`) zusätzlich zum bestehenden `audit.js`:** `audit.js` lädt nur Seiten und prüft sie statisch; API-01 verlangt aber reale Dateneingabe (Formular ausfüllen, Connector anlegen, Mapping ändern). Das neue Skript nutzt dieselbe puppeteer-core-Infrastruktur, führt aber echte Klick-/Eingabeinteraktionen aus und prüft danach das gerenderte DOM — stärkerer Nachweis als ein reiner Vitest-Mock-Test. +- **2 Fix-Iterationen** (von bis zu 20 erlaubten): `aria-label` auf einem `` ohne passende Rolle löste eine axe-`incomplete`-Meldung aus (behoben durch `data-masked="true"` statt `aria-label`); `A11Y_NODE_MODULES`-Pfad auf 204 liegt unter `/usr/lib/...`, nicht dem im bestehenden `audit.js`-Kommentar vermuteten `/usr/local/lib/...` (per Env-Variable korrigiert, kein Code-Eingriff in `audit.js` nötig). + +## Fazit + +Alle drei Akzeptanzkriterien und alle drei geforderten Prüfungen sind erfüllt und belegt (automatisierter Test plus echter Browserlauf je Kriterium). API-01 gilt als vollständig umgesetzt. + +## Nicht Teil dieser Kachel + +- `API-02` Verbindungsstatus-Übersicht & Diagnose (bereits fertig, nicht hier dupliziert). +- Verbindungs-Widerruf über die Oberfläche: `ENG-02`s `Store.Revoke` existiert bereits, ist aber kein Akzeptanzkriterium dieser Kachel und wurde daher nicht an die Oberfläche angebunden. +- Bildschirmleser-Stichprobe: nicht durchgeführt, gleiche organisatorische Lücke wie bei UX-06/UX-07/API-02 (kein NVDA/VoiceOver auf dem Testhost). diff --git a/connect/internal/connectorcatalog/connectorcatalog.go b/connect/internal/connectorcatalog/connectorcatalog.go new file mode 100644 index 00000000..9171b14d --- /dev/null +++ b/connect/internal/connectorcatalog/connectorcatalog.go @@ -0,0 +1,92 @@ +// Package connectorcatalog is the static list of connector types API-01's +// setup UI can offer: which credential kind each expects and which NEXARCH +// fields ENG-04's field mapping accepts for it. There is no per-tenant or +// per-instance configuration file behind this — every concrete connection +// (instance id, credentials, mapping) lives in the database via +// credential.Store/fieldmapping.Store, not here. +package connectorcatalog + +import ( + "gitea.perlbach24.de/scripte/nexarch/connect/internal/credential" + "gitea.perlbach24.de/scripte/nexarch/connect/internal/fieldmapping" +) + +// ConnectorType describes one connector a tenant can set up. +type ConnectorType struct { + ID string + DisplayName string + CredentialKind credential.Kind + Schema []fieldmapping.FieldDef +} + +var types = []ConnectorType{ + { + ID: "erp", + DisplayName: "ERP-System", + CredentialKind: credential.KindAPIKey, + Schema: []fieldmapping.FieldDef{ + {NexarchField: "kundennummer", Required: true}, + {NexarchField: "name", Required: true}, + {NexarchField: "ustid", Required: false}, + {NexarchField: "email", Required: false}, + }, + }, + { + ID: "crm", + DisplayName: "CRM-System", + CredentialKind: credential.KindOAuth2, + Schema: []fieldmapping.FieldDef{ + {NexarchField: "kontakt_id", Required: true}, + {NexarchField: "name", Required: true}, + {NexarchField: "email", Required: false}, + {NexarchField: "telefon", Required: false}, + }, + }, + { + ID: "m365", + DisplayName: "Microsoft 365", + CredentialKind: credential.KindOAuth2, + Schema: []fieldmapping.FieldDef{ + {NexarchField: "benutzer_id", Required: true}, + {NexarchField: "anzeigename", Required: true}, + {NexarchField: "email", Required: false}, + }, + }, + { + ID: "backup", + DisplayName: "Backup-Ziel", + CredentialKind: credential.KindAPIKey, + Schema: []fieldmapping.FieldDef{ + {NexarchField: "dokument_id", Required: true}, + {NexarchField: "pfad", Required: true}, + }, + }, + { + ID: "datev", + DisplayName: "DATEV", + CredentialKind: credential.KindAPIKey, + Schema: []fieldmapping.FieldDef{ + {NexarchField: "belegnummer", Required: true}, + {NexarchField: "betrag", Required: true}, + {NexarchField: "buchungsdatum", Required: false}, + }, + }, +} + +// List returns every connector type, in a fixed, deterministic order. +func List() []ConnectorType { + out := make([]ConnectorType, len(types)) + copy(out, types) + return out +} + +// Get returns the connector type with the given id, or false if none +// matches. +func Get(id string) (ConnectorType, bool) { + for _, t := range types { + if t.ID == id { + return t, true + } + } + return ConnectorType{}, false +} diff --git a/connect/internal/connectorcatalog/connectorcatalog_test.go b/connect/internal/connectorcatalog/connectorcatalog_test.go new file mode 100644 index 00000000..62e0ebf9 --- /dev/null +++ b/connect/internal/connectorcatalog/connectorcatalog_test.go @@ -0,0 +1,31 @@ +package connectorcatalog_test + +import ( + "testing" + + "gitea.perlbach24.de/scripte/nexarch/connect/internal/connectorcatalog" +) + +func TestList_ReturnsKnownTypesWithSchema(t *testing.T) { + types := connectorcatalog.List() + if len(types) == 0 { + t.Fatal("erwartet mindestens einen connector-typ") + } + for _, ct := range types { + if ct.ID == "" || ct.DisplayName == "" { + t.Fatalf("connector-typ ohne id/anzeigename: %+v", ct) + } + if len(ct.Schema) == 0 { + t.Fatalf("connector-typ %q ohne feld-schema", ct.ID) + } + } +} + +func TestGet_KnownAndUnknownID(t *testing.T) { + if _, ok := connectorcatalog.Get("erp"); !ok { + t.Fatal("erwartet connector-typ 'erp'") + } + if _, ok := connectorcatalog.Get("does-not-exist"); ok { + t.Fatal("unbekannter connector-typ haette nicht gefunden werden duerfen") + } +} diff --git a/connect/internal/connectorsetupapi/connectorsetupapi.go b/connect/internal/connectorsetupapi/connectorsetupapi.go new file mode 100644 index 00000000..d3bcbf1c --- /dev/null +++ b/connect/internal/connectorsetupapi/connectorsetupapi.go @@ -0,0 +1,220 @@ +// Package connectorsetupapi is the HTTP binding for API-01's connector +// setup UI: pick a connector type, store its credentials, edit its field +// mapping — entirely through this API, never a config file. Identity comes +// from X-User-Id/X-Tenant-Slug, the same transitional pattern +// connect/internal/statusapi already uses. +package connectorsetupapi + +import ( + "encoding/json" + "errors" + "net/http" + "time" + + "gitea.perlbach24.de/scripte/nexarch/connect/internal/connectorcatalog" + "gitea.perlbach24.de/scripte/nexarch/connect/internal/credential" + "gitea.perlbach24.de/scripte/nexarch/connect/internal/fieldmapping" +) + +type Server struct { + credentials *credential.Store + mappings *fieldmapping.Store +} + +func NewServer(credentials *credential.Store, mappings *fieldmapping.Store) *Server { + return &Server{credentials: credentials, mappings: mappings} +} + +func (s *Server) RegisterRoutes(mux *http.ServeMux) { + mux.HandleFunc("GET /connectors/catalog", s.requireIdentity(s.handleCatalog)) + mux.HandleFunc("GET /connectors/connections", s.requireIdentity(s.handleListConnections)) + mux.HandleFunc("PUT /connectors/connections/{type}/{instance}", s.requireIdentity(s.handlePutConnection)) + mux.HandleFunc("GET /connectors/connections/{type}/{instance}/mapping", s.requireIdentity(s.handleGetMapping)) + mux.HandleFunc("PUT /connectors/connections/{type}/{instance}/mapping", s.requireIdentity(s.handlePutMapping)) +} + +func (s *Server) requireIdentity(next func(w http.ResponseWriter, r *http.Request, tenantSlug, userID string)) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + userID := r.Header.Get("X-User-Id") + if userID == "" { + writeError(w, http.StatusUnauthorized, "X-User-Id fehlt") + return + } + tenant := r.Header.Get("X-Tenant-Slug") + if tenant == "" { + writeError(w, http.StatusUnauthorized, "X-Tenant-Slug fehlt") + return + } + next(w, r, tenant, userID) + } +} + +type connectorTypeView struct { + ID string `json:"id"` + DisplayName string `json:"display_name"` + CredentialKind string `json:"credential_kind"` + Schema []fieldmapping.FieldDef `json:"schema"` +} + +func (s *Server) handleCatalog(w http.ResponseWriter, r *http.Request, _, _ string) { + types := connectorcatalog.List() + out := make([]connectorTypeView, 0, len(types)) + for _, t := range types { + out = append(out, connectorTypeView{ + ID: t.ID, + DisplayName: t.DisplayName, + CredentialKind: string(t.CredentialKind), + Schema: t.Schema, + }) + } + writeJSON(w, http.StatusOK, out) +} + +type connectionView struct { + ConnectorType string `json:"connector_type"` + ConnectorTypeLabel string `json:"connector_type_label"` + ConnectorInstance string `json:"connector_instance_id"` + CredentialKind string `json:"credential_kind"` + MaskedCredential string `json:"masked_credential"` + ExpiresAt *time.Time `json:"expires_at,omitempty"` + UpdatedAt time.Time `json:"updated_at"` +} + +func (s *Server) handleListConnections(w http.ResponseWriter, r *http.Request, tenantSlug, _ string) { + summaries, err := s.credentials.ListForTenant(r.Context(), tenantSlug) + if err != nil { + writeError(w, http.StatusInternalServerError, "verbindungen konnten nicht gelesen werden") + return + } + out := make([]connectionView, 0, len(summaries)) + for _, cs := range summaries { + label := cs.Ref.ConnectorName + if ct, ok := connectorcatalog.Get(cs.Ref.ConnectorName); ok { + label = ct.DisplayName + } + out = append(out, connectionView{ + ConnectorType: cs.Ref.ConnectorName, + ConnectorTypeLabel: label, + ConnectorInstance: cs.Ref.ConnectorInstanceID, + CredentialKind: string(cs.Kind), + MaskedCredential: cs.Masked, + ExpiresAt: cs.ExpiresAt, + UpdatedAt: cs.UpdatedAt, + }) + } + writeJSON(w, http.StatusOK, out) +} + +type putConnectionRequest struct { + Kind string `json:"kind"` + APIKey string `json:"api_key"` + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + ExpiresAt *time.Time `json:"expires_at"` +} + +func (s *Server) handlePutConnection(w http.ResponseWriter, r *http.Request, tenantSlug, userID string) { + connectorType := r.PathValue("type") + instance := r.PathValue("instance") + ct, ok := connectorcatalog.Get(connectorType) + if !ok { + writeError(w, http.StatusNotFound, "unbekannter connector-typ") + return + } + var req putConnectionRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeError(w, http.StatusBadRequest, "ungueltiger anfragekoerper") + return + } + kind := credential.Kind(req.Kind) + if kind != ct.CredentialKind { + writeError(w, http.StatusBadRequest, "zugangsdaten-art passt nicht zum connector-typ") + return + } + secret := credential.Secret{APIKey: req.APIKey, AccessToken: req.AccessToken, RefreshToken: req.RefreshToken} + if (kind == credential.KindAPIKey && secret.APIKey == "") || (kind == credential.KindOAuth2 && secret.AccessToken == "") { + writeError(w, http.StatusBadRequest, "zugangsdaten fehlen") + return + } + ref := credential.Ref{TenantSlug: tenantSlug, ConnectorName: connectorType, ConnectorInstanceID: instance} + if err := s.credentials.PutWithActor(r.Context(), ref, kind, secret, req.ExpiresAt, userID); err != nil { + writeError(w, http.StatusInternalServerError, "verbindung konnte nicht gespeichert werden") + return + } + writeJSON(w, http.StatusOK, connectionView{ + ConnectorType: connectorType, + ConnectorTypeLabel: ct.DisplayName, + ConnectorInstance: instance, + CredentialKind: string(kind), + MaskedCredential: credential.Mask(secret, kind), + ExpiresAt: req.ExpiresAt, + UpdatedAt: time.Now().UTC(), + }) +} + +type mappingView struct { + Schema []fieldmapping.FieldDef `json:"schema"` + Fields []fieldmapping.Entry `json:"fields"` + Version int `json:"version"` +} + +func (s *Server) handleGetMapping(w http.ResponseWriter, r *http.Request, tenantSlug, _ string) { + connectorType := r.PathValue("type") + instance := r.PathValue("instance") + ct, ok := connectorcatalog.Get(connectorType) + if !ok { + writeError(w, http.StatusNotFound, "unbekannter connector-typ") + return + } + ref := fieldmapping.Ref{TenantSlug: tenantSlug, ConnectorName: connectorType, ConnectorInstanceID: instance} + m, err := s.mappings.GetActive(r.Context(), ref) + if errors.Is(err, fieldmapping.ErrNotFound) { + writeJSON(w, http.StatusOK, mappingView{Schema: ct.Schema, Fields: []fieldmapping.Entry{}}) + return + } + if err != nil { + writeError(w, http.StatusInternalServerError, "mapping konnte nicht gelesen werden") + return + } + writeJSON(w, http.StatusOK, mappingView{Schema: ct.Schema, Fields: m.Fields, Version: m.Version}) +} + +type putMappingRequest struct { + Fields []fieldmapping.Entry `json:"fields"` +} + +func (s *Server) handlePutMapping(w http.ResponseWriter, r *http.Request, tenantSlug, _ string) { + connectorType := r.PathValue("type") + instance := r.PathValue("instance") + ct, ok := connectorcatalog.Get(connectorType) + if !ok { + writeError(w, http.StatusNotFound, "unbekannter connector-typ") + return + } + var req putMappingRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeError(w, http.StatusBadRequest, "ungueltiger anfragekoerper") + return + } + ref := fieldmapping.Ref{TenantSlug: tenantSlug, ConnectorName: connectorType, ConnectorInstanceID: instance} + m, err := s.mappings.Save(r.Context(), ref, req.Fields, ct.Schema) + if errors.Is(err, fieldmapping.ErrInvalidConfig) { + writeError(w, http.StatusBadRequest, err.Error()) + return + } + if err != nil { + writeError(w, http.StatusInternalServerError, "mapping konnte nicht gespeichert werden") + return + } + writeJSON(w, http.StatusOK, mappingView{Schema: ct.Schema, Fields: m.Fields, Version: m.Version}) +} + +func writeJSON(w http.ResponseWriter, status int, v any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + _ = json.NewEncoder(w).Encode(v) +} + +func writeError(w http.ResponseWriter, status int, msg string) { + writeJSON(w, status, map[string]string{"error": msg}) +} diff --git a/connect/internal/connectorsetupapi/connectorsetupapi_test.go b/connect/internal/connectorsetupapi/connectorsetupapi_test.go new file mode 100644 index 00000000..b546a163 --- /dev/null +++ b/connect/internal/connectorsetupapi/connectorsetupapi_test.go @@ -0,0 +1,275 @@ +package connectorsetupapi + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + + connectcrypto "gitea.perlbach24.de/scripte/nexarch/connect/internal/crypto" + + "gitea.perlbach24.de/scripte/nexarch/connect/internal/credential" + "gitea.perlbach24.de/scripte/nexarch/connect/internal/fieldmapping" + "gitea.perlbach24.de/scripte/nexarch/connect/internal/migrate" + "gitea.perlbach24.de/scripte/nexarch/connect/internal/testdb" +) + +type fakeKEK struct{ kek []byte } + +func (f *fakeKEK) TenantKEK(ctx context.Context, tenantSlug string) ([]byte, error) { + return f.kek, nil +} + +func newTestServer(t *testing.T) *http.ServeMux { + t.Helper() + dsn := os.Getenv("TEST_TENANT_DSN") + if dsn == "" { + t.Skip("TEST_TENANT_DSN nicht gesetzt, Integrationstest uebersprungen") + } + pool := testdb.Pool(t, dsn) + root, err := filepath.Abs("../../migrations") + if err != nil { + t.Fatalf("migrationsverzeichnis: %v", err) + } + migrations, err := migrate.Load(root) + if err != nil { + t.Fatalf("migrationen laden: %v", err) + } + if _, err := migrate.Up(context.Background(), pool, migrations); err != nil { + t.Fatalf("migrationen anwenden: %v", err) + } + cryptoSvc := connectcrypto.NewService(&fakeKEK{kek: bytes.Repeat([]byte{0x11}, connectcrypto.KEKSize)}) + s := NewServer(credential.NewStore(pool, cryptoSvc), fieldmapping.NewStore(pool)) + mux := http.NewServeMux() + s.RegisterRoutes(mux) + return mux +} + +func authedRequest(method, url string, body any) *http.Request { + var reader io.Reader + if body != nil { + payload, _ := json.Marshal(body) + reader = bytes.NewReader(payload) + } + req := httptest.NewRequest(method, url, reader) + req.Header.Set("X-User-Id", "u1") + req.Header.Set("X-Tenant-Slug", "acme") + req.Header.Set("Content-Type", "application/json") + return req +} + +func doJSON(t *testing.T, mux *http.ServeMux, method, url string, body any) (*httptest.ResponseRecorder, map[string]any) { + t.Helper() + req := authedRequest(method, url, body) + w := httptest.NewRecorder() + mux.ServeHTTP(w, req) + var decoded map[string]any + if w.Body.Len() > 0 { + if err := json.Unmarshal(w.Body.Bytes(), &decoded); err != nil { + t.Fatalf("antwort kein json-objekt: %v, body=%s", err, w.Body.String()) + } + } + return w, decoded +} + +// AC1/Pruefung 1: ein Testconnector laesst sich vollstaendig ueber die +// Oberflaeche (diese API, die die Oberflaeche aufruft) einrichten — ohne +// Konfigurationsdatei, nur ueber HTTP-Aufrufe mit Daten aus der Oberflaeche. +func TestFullConnectorSetup_WithoutConfigFile(t *testing.T) { + mux := newTestServer(t) + + wCatalog := httptest.NewRecorder() + mux.ServeHTTP(wCatalog, authedRequest("GET", "/connectors/catalog", nil)) + if wCatalog.Code != http.StatusOK { + t.Fatalf("katalog status = %d", wCatalog.Code) + } + var catalog []connectorTypeView + if err := json.Unmarshal(wCatalog.Body.Bytes(), &catalog); err != nil { + t.Fatalf("katalog decode: %v", err) + } + found := false + for _, ct := range catalog { + if ct.ID == "erp" { + found = true + } + } + if !found { + t.Fatalf("erwartet connector-typ 'erp' im katalog: %+v", catalog) + } + + wConn, _ := doJSON(t, mux, "PUT", "/connectors/connections/erp/acme-erp-1", putConnectionRequest{ + Kind: "api_key", + APIKey: "testconnector-geheimnis-1234567890", + }) + if wConn.Code != http.StatusOK { + t.Fatalf("verbindung anlegen status = %d, body=%s", wConn.Code, wConn.Body.String()) + } + + wMapping, _ := doJSON(t, mux, "PUT", "/connectors/connections/erp/acme-erp-1/mapping", putMappingRequest{ + Fields: []fieldmapping.Entry{ + {NexarchField: "kundennummer", ExternalField: "debitorennummer"}, + {NexarchField: "name", ExternalField: "name"}, + }, + }) + if wMapping.Code != http.StatusOK { + t.Fatalf("mapping speichern status = %d, body=%s", wMapping.Code, wMapping.Body.String()) + } + + wList := httptest.NewRecorder() + mux.ServeHTTP(wList, authedRequest("GET", "/connectors/connections", nil)) + var connections []connectionView + if err := json.Unmarshal(wList.Body.Bytes(), &connections); err != nil { + t.Fatalf("verbindungsliste decode: %v", err) + } + var seen bool + for _, c := range connections { + if c.ConnectorType == "erp" && c.ConnectorInstance == "acme-erp-1" { + seen = true + } + } + if !seen { + t.Fatalf("eingerichtete verbindung nicht in der liste: %+v", connections) + } +} + +// AC2/Pruefung 2: Zugangsdaten werden in der Verbindungsliste nachweislich +// nur maskiert zurueckgegeben, nie im Klartext. +func TestListConnections_CredentialsAreMasked(t *testing.T) { + mux := newTestServer(t) + secret := "klartext-der-niemals-rausgehen-darf-55443322" + doJSON(t, mux, "PUT", "/connectors/connections/backup/acme-backup-1", putConnectionRequest{ + Kind: "api_key", + APIKey: secret, + }) + + w := httptest.NewRecorder() + mux.ServeHTTP(w, authedRequest("GET", "/connectors/connections", nil)) + if strings.Contains(w.Body.String(), secret) { + t.Fatalf("antwort enthaelt das geheimnis im klartext: %s", w.Body.String()) + } + var connections []connectionView + if err := json.Unmarshal(w.Body.Bytes(), &connections); err != nil { + t.Fatalf("decode: %v", err) + } + var masked string + for _, c := range connections { + if c.ConnectorInstance == "acme-backup-1" { + masked = c.MaskedCredential + } + } + if masked == "" || !strings.HasPrefix(masked, "••••") || masked == secret { + t.Fatalf("zugangsdaten nicht nachweislich maskiert: %q", masked) + } +} + +// AC3/Pruefung 3: eine Mapping-Aenderung ueber die Oberflaeche wird korrekt +// uebernommen (gelesen, geaendert, erneut gelesen). +func TestMapping_ChangeIsPickedUpOnNextRead(t *testing.T) { + mux := newTestServer(t) + doJSON(t, mux, "PUT", "/connectors/connections/crm/acme-crm-1", putConnectionRequest{ + Kind: "oauth2", + AccessToken: "access-123456789", + }) + + doJSON(t, mux, "PUT", "/connectors/connections/crm/acme-crm-1/mapping", putMappingRequest{ + Fields: []fieldmapping.Entry{ + {NexarchField: "kontakt_id", ExternalField: "id"}, + {NexarchField: "name", ExternalField: "company"}, + }, + }) + + w := httptest.NewRecorder() + mux.ServeHTTP(w, authedRequest("GET", "/connectors/connections/crm/acme-crm-1/mapping", nil)) + var first mappingView + if err := json.Unmarshal(w.Body.Bytes(), &first); err != nil { + t.Fatalf("decode: %v", err) + } + if len(first.Fields) != 2 || first.Version != 1 { + t.Fatalf("erste version nicht wie erwartet: %+v", first) + } + + wUpdate, _ := doJSON(t, mux, "PUT", "/connectors/connections/crm/acme-crm-1/mapping", putMappingRequest{ + Fields: []fieldmapping.Entry{ + {NexarchField: "kontakt_id", ExternalField: "id"}, + {NexarchField: "name", ExternalField: "contact_name"}, + {NexarchField: "email", ExternalField: "email_address"}, + }, + }) + if wUpdate.Code != http.StatusOK { + t.Fatalf("mapping aktualisieren status = %d", wUpdate.Code) + } + + w2 := httptest.NewRecorder() + mux.ServeHTTP(w2, authedRequest("GET", "/connectors/connections/crm/acme-crm-1/mapping", nil)) + var second mappingView + if err := json.Unmarshal(w2.Body.Bytes(), &second); err != nil { + t.Fatalf("decode: %v", err) + } + if second.Version != 2 || len(second.Fields) != 3 { + t.Fatalf("geaendertes mapping nicht uebernommen: %+v", second) + } + var contactName string + for _, e := range second.Fields { + if e.NexarchField == "name" { + contactName = e.ExternalField + } + } + if contactName != "contact_name" { + t.Fatalf("geaendertes feld nicht uebernommen: %+v", second.Fields) + } +} + +// AC2 (ENG-04): ein strukturell ungueltiges Mapping wird beim Speichern +// abgewiesen statt stillschweigend uebernommen zu werden. +func TestMapping_RejectsUnknownField(t *testing.T) { + mux := newTestServer(t) + doJSON(t, mux, "PUT", "/connectors/connections/datev/acme-datev-1", putConnectionRequest{ + Kind: "api_key", + APIKey: "datev-schluessel-abcdefgh", + }) + w, body := doJSON(t, mux, "PUT", "/connectors/connections/datev/acme-datev-1/mapping", putMappingRequest{ + Fields: []fieldmapping.Entry{ + {NexarchField: "unbekanntes_feld", ExternalField: "x"}, + }, + }) + if w.Code != http.StatusBadRequest { + t.Fatalf("status = %d, want 400, body=%v", w.Code, body) + } +} + +func TestPutConnection_RejectsMismatchedCredentialKind(t *testing.T) { + mux := newTestServer(t) + w, _ := doJSON(t, mux, "PUT", "/connectors/connections/erp/acme-erp-mismatch", putConnectionRequest{ + Kind: "oauth2", + AccessToken: "irrelevant", + }) + if w.Code != http.StatusBadRequest { + t.Fatalf("status = %d, want 400 (erp erwartet api_key)", w.Code) + } +} + +func TestPutConnection_UnknownConnectorType(t *testing.T) { + mux := newTestServer(t) + w, _ := doJSON(t, mux, "PUT", "/connectors/connections/does-not-exist/inst", putConnectionRequest{ + Kind: "api_key", + APIKey: "x", + }) + if w.Code != http.StatusNotFound { + t.Fatalf("status = %d, want 404", w.Code) + } +} + +func TestRequireIdentity_RejectsMissingHeaders(t *testing.T) { + mux := newTestServer(t) + w := httptest.NewRecorder() + mux.ServeHTTP(w, httptest.NewRequest("GET", "/connectors/connections", nil)) + if w.Code != http.StatusUnauthorized { + t.Fatalf("status = %d, want 401", w.Code) + } +} diff --git a/connect/internal/credential/connections.go b/connect/internal/credential/connections.go new file mode 100644 index 00000000..b9d9e1f4 --- /dev/null +++ b/connect/internal/credential/connections.go @@ -0,0 +1,83 @@ +// connections.go adds the read side API-01's setup UI needs: a masked +// listing of every connection a tenant already has (ENG-02 itself only +// exposed Get/Put/Revoke keyed by a single known Ref). +package credential + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "gitea.perlbach24.de/scripte/nexarch/connect/internal/crypto" +) + +// ConnectionSummary is one row of connector_credentials, decrypted only far +// enough to produce Masked — the caller never sees the real secret. +type ConnectionSummary struct { + Ref Ref + Kind Kind + Masked string + ExpiresAt *time.Time + UpdatedAt time.Time +} + +// ListForTenant returns a masked summary of every connection stored for +// tenantSlug, ordered by connector name and instance id. +func (s *Store) ListForTenant(ctx context.Context, tenantSlug string) ([]ConnectionSummary, error) { + rows, err := s.pool.Query(ctx, ` + SELECT connector_name, connector_instance_id, kind, ciphertext, wrapped_dek, expires_at, updated_at + FROM connector_credentials + WHERE tenant_slug = $1 + ORDER BY connector_name, connector_instance_id + `, tenantSlug) + if err != nil { + return nil, fmt.Errorf("credential: list for tenant: %w", err) + } + defer rows.Close() + + var out []ConnectionSummary + for rows.Next() { + var ref Ref + ref.TenantSlug = tenantSlug + var kind Kind + var ciphertext, wrappedDEK []byte + var expiresAt *time.Time + var updatedAt time.Time + if err := rows.Scan(&ref.ConnectorName, &ref.ConnectorInstanceID, &kind, &ciphertext, &wrappedDEK, &expiresAt, &updatedAt); err != nil { + return nil, fmt.Errorf("credential: scan connection: %w", err) + } + plaintext, err := s.crypto.Open(ctx, ref.TenantSlug, &crypto.Sealed{Ciphertext: ciphertext, WrappedDEK: wrappedDEK}) + if err != nil { + return nil, fmt.Errorf("credential: decrypt for mask: %w", err) + } + var secret Secret + if err := json.Unmarshal(plaintext, &secret); err != nil { + return nil, fmt.Errorf("credential: unmarshal for mask: %w", err) + } + out = append(out, ConnectionSummary{ + Ref: ref, + Kind: kind, + Masked: Mask(secret, kind), + ExpiresAt: expiresAt, + UpdatedAt: updatedAt, + }) + } + return out, rows.Err() +} + +// Mask returns a fixed-length, non-reversible preview of secret's primary +// value for kind: four bullet characters plus its last 4 characters, never +// more than half the secret. A secret of 8 characters or less is hidden +// completely instead, so a short value never has most of itself revealed. +func Mask(secret Secret, kind Kind) string { + value := secret.AccessToken + if kind == KindAPIKey { + value = secret.APIKey + } + const bullets = "••••" + if len(value) <= 8 { + return bullets + } + return bullets + value[len(value)-4:] +} diff --git a/connect/internal/credential/connections_test.go b/connect/internal/credential/connections_test.go new file mode 100644 index 00000000..b94196b9 --- /dev/null +++ b/connect/internal/credential/connections_test.go @@ -0,0 +1,82 @@ +package credential_test + +import ( + "context" + "strings" + "testing" + + "gitea.perlbach24.de/scripte/nexarch/connect/internal/credential" +) + +// API-01 Pruefung 2: Zugangsdaten in der Oberflaeche nachweislich maskiert +// — hier auf Ebene des Datenzugriffs, den die Oberflaeche aufruft. +func TestStore_ListForTenant_MasksSecrets(t *testing.T) { + store, _ := setup(t) + ctx := context.Background() + + apiKeyRef := credential.Ref{TenantSlug: "acme", ConnectorName: "erp", ConnectorInstanceID: "inst-list-1"} + apiKeySecret := credential.Secret{APIKey: "super-secret-plaintext-9999"} + if err := store.Put(ctx, apiKeyRef, credential.KindAPIKey, apiKeySecret, nil); err != nil { + t.Fatalf("put api key: %v", err) + } + + oauthRef := credential.Ref{TenantSlug: "acme", ConnectorName: "crm", ConnectorInstanceID: "inst-list-2"} + oauthSecret := credential.Secret{AccessToken: "oauth-access-token-abcd1234", RefreshToken: "refresh-xyz"} + if err := store.Put(ctx, oauthRef, credential.KindOAuth2, oauthSecret, nil); err != nil { + t.Fatalf("put oauth: %v", err) + } + + summaries, err := store.ListForTenant(ctx, "acme") + if err != nil { + t.Fatalf("list for tenant: %v", err) + } + if len(summaries) != 2 { + t.Fatalf("erwartet 2 verbindungen, habe %d", len(summaries)) + } + + for _, s := range summaries { + if strings.Contains(s.Masked, apiKeySecret.APIKey) || strings.Contains(s.Masked, oauthSecret.AccessToken) || strings.Contains(s.Masked, oauthSecret.RefreshToken) { + t.Fatalf("maskierte ansicht enthaelt klartext-geheimnis: %q", s.Masked) + } + if !strings.HasPrefix(s.Masked, "••••") { + t.Fatalf("maskierte ansicht beginnt nicht mit bullets: %q", s.Masked) + } + } + + var sawAPIKey, sawOAuth bool + for _, s := range summaries { + switch s.Ref.ConnectorInstanceID { + case "inst-list-1": + sawAPIKey = true + if s.Masked != "••••9999" { + t.Fatalf("falsche maskierung fuer api key: %q", s.Masked) + } + case "inst-list-2": + sawOAuth = true + if s.Masked != "••••1234" { + t.Fatalf("falsche maskierung fuer oauth token: %q", s.Masked) + } + } + } + if !sawAPIKey || !sawOAuth { + t.Fatalf("nicht beide verbindungen gefunden: %+v", summaries) + } +} + +func TestStore_ListForTenant_EmptyForUnknownTenant(t *testing.T) { + store, _ := setup(t) + summaries, err := store.ListForTenant(context.Background(), "nobody-here") + if err != nil { + t.Fatalf("list for tenant: %v", err) + } + if len(summaries) != 0 { + t.Fatalf("erwartet keine verbindungen, habe %d", len(summaries)) + } +} + +func TestMask_ShortSecretStaysFullyHidden(t *testing.T) { + m := credential.Mask(credential.Secret{APIKey: "ab"}, credential.KindAPIKey) + if m != "••••" { + t.Fatalf("kurzes geheimnis sollte vollstaendig verdeckt sein, habe %q", m) + } +} diff --git a/web/a11y-audit/connect-setup-interaction-check.js b/web/a11y-audit/connect-setup-interaction-check.js new file mode 100644 index 00000000..dadabc42 --- /dev/null +++ b/web/a11y-audit/connect-setup-interaction-check.js @@ -0,0 +1,131 @@ +// API-01: echter Browserlauf (Chrome ueber puppeteer-core, wie +// web/a11y-audit/audit.js) gegen den echten setup-a11y-fixture-Dienst und +// eine echte Postgres-Wegwerf-DB. Statt nur eine Seite zu laden, fuellt +// und sendet dieses Skript das echte Formular, genau wie eine Nutzerin es +// taete, und prueft das gerenderte DOM danach. +// +// A11Y_CHROME=/pfad/chrome A11Y_WEB_URL=http://127.0.0.1:4191 node connect-setup-interaction-check.js +const path = require("path"); +const nm = process.env.A11Y_NODE_MODULES || "/usr/lib/node_modules/pa11y/node_modules"; +const puppeteer = require(path.join(nm, "puppeteer-core")); + +const SECRET = "testconnector-geheimnis-7788991100"; + +function fail(msg) { + console.error("FEHLGESCHLAGEN: " + msg); + process.exitCode = 1; +} + +(async () => { + const browser = await puppeteer.launch({ + executablePath: process.env.A11Y_CHROME, + headless: "new", + args: ["--no-sandbox", "--disable-dev-shm-usage"], + }); + try { + const page = await browser.newPage(); + await page.setViewport({ width: 1280, height: 900 }); + const base = process.env.A11Y_WEB_URL; + if (!base) throw new Error("A11Y_WEB_URL fehlt"); + + await page.goto(base + "/setup", { waitUntil: "networkidle0" }); + + // --- Pruefung 1: vollstaendige Einrichtung eines Testconnectors --- + await page.waitForSelector("form"); + // Connector-Typ-Select: Wert "erp" ist per Katalog vorhanden. + const typeSelectId = await page.evaluate(() => { + const label = Array.from(document.querySelectorAll("label")).find((l) => l.textContent.includes("Connector-Typ")); + return label ? label.getAttribute("for") : null; + }); + if (!typeSelectId) throw new Error("Connector-Typ-Feld nicht gefunden"); + await page.select("#" + typeSelectId, "erp"); + + const instanceId = await page.evaluate(() => { + const label = Array.from(document.querySelectorAll("label")).find((l) => l.textContent.includes("Instanz-Kennung")); + return label ? label.getAttribute("for") : null; + }); + await page.type("#" + instanceId, "browser-test-erp-1"); + + const apiKeyId = await page.evaluate(() => { + const label = Array.from(document.querySelectorAll("label")).find((l) => l.textContent.includes("API-Schlüssel")); + return label ? label.getAttribute("for") : null; + }); + if (!apiKeyId) throw new Error("API-Schluessel-Feld nicht gefunden (Connector-Typ falsch gewaehlt?)"); + await page.type("#" + apiKeyId, SECRET); + + await page.evaluate(() => { + const btn = Array.from(document.querySelectorAll("button")).find((b) => b.textContent.includes("Verbindung speichern")); + btn.click(); + }); + + await page.waitForFunction( + (id) => document.body.innerText.includes(id), + { timeout: 10000 }, + "browser-test-erp-1" + ); + + const bodyHTML = await page.content(); + if (bodyHTML.includes(SECRET)) { + fail("das im Formular eingegebene Geheimnis erscheint im Klartext im gerenderten DOM"); + } else { + console.log("OK: Pruefung 1 (vollstaendige Einrichtung) + kein Klartext-Geheimnis im DOM nach dem Anlegen"); + } + + // --- Pruefung 2: Zugangsdaten nachweislich maskiert --- + const maskedText = await page.evaluate(() => { + const cell = document.querySelector('[data-masked="true"]'); + return cell ? cell.textContent : null; + }); + if (!maskedText || !maskedText.startsWith("••••") || maskedText === SECRET) { + fail("Zugangsdaten in der Verbindungsliste sind nicht nachweislich maskiert: " + maskedText); + } else { + console.log("OK: Pruefung 2 (maskierte Anzeige) — gerenderter Wert: " + maskedText); + } + + // --- Pruefung 3: Feld-Mapping-Aenderung wird korrekt uebernommen --- + await page.evaluate(() => { + const btn = Array.from(document.querySelectorAll("button")).find((b) => b.textContent.trim() === "Bearbeiten"); + btn.click(); + }); + await page.waitForFunction(() => document.body.innerText.includes("Version"), { timeout: 10000 }); + + const firstFieldId = await page.evaluate(() => { + const label = Array.from(document.querySelectorAll("label")).find((l) => l.textContent.startsWith("kundennummer")); + return label ? label.getAttribute("for") : null; + }); + if (!firstFieldId) throw new Error("Mapping-Feld 'kundennummer' nicht gefunden"); + await page.type("#" + firstFieldId, "debitorennummer"); + + await page.evaluate(() => { + const btn = Array.from(document.querySelectorAll("button")).find((b) => b.textContent.includes("Mapping speichern")); + btn.click(); + }); + await page.waitForFunction(() => document.body.innerText.includes("Version 1"), { timeout: 10000 }); + + // Seite neu laden und die Mapping-Ansicht erneut oeffnen, um zu + // pruefen, dass die Aenderung tatsaechlich dauerhaft uebernommen wurde + // (nicht nur lokaler React-State). + await page.reload({ waitUntil: "networkidle0" }); + await page.evaluate(() => { + const btn = Array.from(document.querySelectorAll("button")).find((b) => b.textContent.trim() === "Bearbeiten"); + btn.click(); + }); + await page.waitForFunction(() => document.body.innerText.includes("Version"), { timeout: 10000 }); + const reloadedValue = await page.evaluate(() => { + const label = Array.from(document.querySelectorAll("label")).find((l) => l.textContent.startsWith("kundennummer")); + if (!label) return null; + const input = document.getElementById(label.getAttribute("for")); + return input ? input.value : null; + }); + if (reloadedValue !== "debitorennummer") { + fail("Mapping-Aenderung wurde nach Neuladen nicht uebernommen, gelesener Wert: " + reloadedValue); + } else { + console.log("OK: Pruefung 3 (Mapping-Aenderung uebernommen und nach Neuladen bestaetigt)"); + } + } catch (err) { + fail(err.stack || String(err)); + } finally { + await browser.close(); + } + process.exit(process.exitCode || 0); +})(); diff --git a/web/a11y-audit/connect-setup-run.sh b/web/a11y-audit/connect-setup-run.sh new file mode 100755 index 00000000..378ae2d0 --- /dev/null +++ b/web/a11y-audit/connect-setup-run.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# API-01: startet den echten setup-a11y-fixture-Dienst gegen eine +# Wegwerf-DB, baut und startet web/connect-setup dagegen, fuehrt einen +# echten Browserlauf durch, der das Einrichtungsformular tatsaechlich +# ausfuellt und absendet (connect-setup-interaction-check.js), und +# anschliessend den axe/Layout-Audit (Muster: connect-status-run.sh). +# Env: A11Y_DSN (PostgreSQL), A11Y_CHROME (Chrome-Binary) +# Ergebnis: $OUT/report.json ; Exit 1 bei Befund +set -uo pipefail +repo="$(cd "$(dirname "$0")/../.." && pwd)" +OUT="${OUT:-/tmp/api01-a11y-out}"; mkdir -p "$OUT" +: "${A11Y_DSN:?A11Y_DSN fehlt}" +if [ -z "${A11Y_CHROME:-}" ]; then + A11Y_CHROME="$(ls -d "$HOME"/.cache/puppeteer/chrome/*/chrome-linux64/chrome /root/.cache/puppeteer/chrome/*/chrome-linux64/chrome 2>/dev/null | tail -1)" + [ -n "$A11Y_CHROME" ] || A11Y_CHROME="$(command -v google-chrome || command -v chromium || true)" +fi +: "${A11Y_CHROME:?A11Y_CHROME fehlt und kein Chrome gefunden}" +export A11Y_CHROME +: "${A11Y_KEK:=$(head -c 32 /dev/urandom | base64)}" + +API_PORT=9591 +WEB_PORT=4191 +PORTS="$API_PORT $WEB_PORT" +listener_pid() { ss -ltnpH "sport = :$1" 2>/dev/null | grep -o 'pid=[0-9]*' | head -1 | cut -d= -f2; } +stop_servers() { for port in $PORTS; do pid="$(listener_pid "$port")"; [ -n "$pid" ] && kill "$pid" 2>/dev/null; done; sleep 1; } +trap stop_servers EXIT +for port in $PORTS; do + if curl -s -o /dev/null --max-time 2 "http://127.0.0.1:$port/"; then echo "Port $port ist belegt, Abbruch (kein fremder Prozess wird beendet)"; exit 2; fi +done + +(cd "$repo/connect" && go build -o "$OUT/setup-a11y-fixture" ./cmd/setup-a11y-fixture) || exit 2 +(cd "$repo/connect" && NEXARCH_CONNECT_TENANT_DSN="$A11Y_DSN" go run ./cmd/migrate) || { echo "migration fehlgeschlagen"; exit 2; } +NEXARCH_CONNECT_APP_TENANT_DSN="$A11Y_DSN" NEXARCH_CONNECT_APP_LISTEN_ADDR="127.0.0.1:$API_PORT" NEXARCH_CONNECT_APP_KEK="$A11Y_KEK" \ + setsid nohup "$OUT/setup-a11y-fixture" >"$OUT/setup-app.log" 2>&1 /dev/null 2>&1 + rm -rf node_modules/@nexarch/shl; mkdir -p node_modules/@nexarch/shl + (cd ../shl && tar cf - --exclude=node_modules --exclude=__tests__ .) | tar xf - -C node_modules/@nexarch/shl + rm -rf .next; npx next build >"$OUT/build-$d.log" 2>&1 || { echo "build $d fehlgeschlagen"; tail -20 "$OUT/build-$d.log"; exit 2; } + PORT=$WEB_PORT setsid nohup npx next start -p "$WEB_PORT" >"$OUT/serve-$d.log" 2>&1 "$OUT/pages.txt" +A11Y_PAGES="$OUT/pages.txt" A11Y_OUT="$OUT/report.json" node "$repo/web/a11y-audit/audit.js" +audit_rc=$? +echo "--- report ---" +cat "$OUT/report.json" 2>/dev/null + +if [ $interaction_rc -ne 0 ] || [ $audit_rc -ne 0 ]; then + exit 1 +fi +exit 0 diff --git a/web/connect-setup/.npmrc b/web/connect-setup/.npmrc new file mode 100644 index 00000000..c7d35173 --- /dev/null +++ b/web/connect-setup/.npmrc @@ -0,0 +1 @@ +install-links=true diff --git a/web/connect-setup/app/layout.tsx b/web/connect-setup/app/layout.tsx new file mode 100644 index 00000000..b1b9e2a8 --- /dev/null +++ b/web/connect-setup/app/layout.tsx @@ -0,0 +1,39 @@ +import Script from "next/script"; +import { ThemeProvider, I18nProvider, ToastProvider, typography, themeInitScript, focusVisibleStyles } from "@nexarch/shl"; + +export const metadata = { + title: "NEXARCH Connect einrichten", +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + {/* Muster aus UX-07/UX-08 (web/mail-search): sichtbare Fokusmarkierung + und Theme-Variablen vor dem ersten Paint, auch ohne @nexarch/shl-Shell. */} + + + + + + + {children} + + + + + ); +} diff --git a/web/connect-setup/app/page.tsx b/web/connect-setup/app/page.tsx new file mode 100644 index 00000000..e99700da --- /dev/null +++ b/web/connect-setup/app/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from "next/navigation"; + +export default function IndexPage() { + redirect("/setup"); +} diff --git a/web/connect-setup/app/setup/page.test.tsx b/web/connect-setup/app/setup/page.test.tsx new file mode 100644 index 00000000..cb57da00 --- /dev/null +++ b/web/connect-setup/app/setup/page.test.tsx @@ -0,0 +1,186 @@ +import { describe, expect, it, vi, afterEach } from "vitest"; +import { render, screen, waitFor, fireEvent } from "@testing-library/react"; +import { I18nProvider, ThemeProvider, ToastProvider } from "@nexarch/shl"; +import SetupPage from "./page"; +import { UnauthorizedError } from "../../lib/api"; + +vi.mock("../../lib/api", async () => { + const actual = await vi.importActual("../../lib/api"); + return { + ...actual, + fetchCatalog: vi.fn(), + fetchConnections: vi.fn(), + setConnection: vi.fn(), + fetchMapping: vi.fn(), + saveMapping: vi.fn(), + }; +}); + +import { fetchCatalog, fetchConnections, setConnection, fetchMapping, saveMapping } from "../../lib/api"; + +const catalog = [ + { + id: "erp", + display_name: "ERP-System", + credential_kind: "api_key" as const, + schema: [ + { nexarch_field: "kundennummer", required: true }, + { nexarch_field: "name", required: true }, + { nexarch_field: "email", required: false }, + ], + }, + { + id: "crm", + display_name: "CRM-System", + credential_kind: "oauth2" as const, + schema: [{ nexarch_field: "kontakt_id", required: true }], + }, +]; + +function renderPage() { + return render( + + + + + + + + ); +} + +describe("SetupPage", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it("zeigt eine explizite Meldung ohne Anmeldung", async () => { + vi.mocked(fetchCatalog).mockResolvedValue(catalog); + vi.mocked(fetchConnections).mockRejectedValue(new UnauthorizedError("nicht angemeldet")); + renderPage(); + await waitFor(() => { + expect(screen.getByRole("alert").textContent).toContain("Anmeldung erforderlich"); + }); + }); + + // AC1/Pruefung 1: ein Testconnector laesst sich vollstaendig ueber die + // Oberflaeche einrichten (Typ waehlen, Instanz benennen, Zugangsdaten + // eingeben, speichern) — ohne Konfigurationsdatei. + it("richtet einen Testconnector vollstaendig ueber das Formular ein", async () => { + vi.mocked(fetchCatalog).mockResolvedValue(catalog); + vi.mocked(fetchConnections).mockResolvedValueOnce([]).mockResolvedValueOnce([ + { + connector_type: "erp", + connector_type_label: "ERP-System", + connector_instance_id: "test-erp-1", + credential_kind: "api_key", + masked_credential: "••••6789", + updated_at: new Date().toISOString(), + }, + ]); + vi.mocked(setConnection).mockResolvedValue({ + connector_type: "erp", + connector_type_label: "ERP-System", + connector_instance_id: "test-erp-1", + credential_kind: "api_key", + masked_credential: "••••6789", + updated_at: new Date().toISOString(), + }); + renderPage(); + + await waitFor(() => expect(screen.getByLabelText("Instanz-Kennung")).toBeTruthy()); + fireEvent.change(screen.getByLabelText("Instanz-Kennung"), { target: { value: "test-erp-1" } }); + fireEvent.change(screen.getByLabelText("API-Schlüssel"), { target: { value: "geheimnis-123456789" } }); + fireEvent.click(screen.getByText("Verbindung speichern")); + + await waitFor(() => { + expect(setConnection).toHaveBeenCalledWith("erp", "test-erp-1", { + kind: "api_key", + api_key: "geheimnis-123456789", + access_token: undefined, + refresh_token: undefined, + }); + }); + await waitFor(() => { + expect(screen.getByText("test-erp-1")).toBeTruthy(); + }); + }); + + // AC2/Pruefung 2: Zugangsdaten werden in der Oberflaeche nur maskiert + // angezeigt, nie im Klartext. + it("zeigt Zugangsdaten in der Verbindungsliste nur maskiert", async () => { + vi.mocked(fetchCatalog).mockResolvedValue(catalog); + vi.mocked(fetchConnections).mockResolvedValue([ + { + connector_type: "erp", + connector_type_label: "ERP-System", + connector_instance_id: "acme-erp-1", + credential_kind: "api_key", + masked_credential: "••••9999", + updated_at: new Date().toISOString(), + }, + ]); + renderPage(); + + await waitFor(() => { + expect(screen.getByText("••••9999")).toBeTruthy(); + }); + expect(screen.queryByText(/geheimnis/i)).toBeNull(); + expect(document.body.innerHTML).not.toMatch(/api[_-]?key.{0,20}[a-zA-Z0-9]{10,}/i); + }); + + // AC3/Pruefung 3: eine Mapping-Aenderung ueber die Oberflaeche wird + // korrekt uebernommen. + it("uebernimmt eine Mapping-Aenderung korrekt", async () => { + vi.mocked(fetchCatalog).mockResolvedValue(catalog); + vi.mocked(fetchConnections).mockResolvedValue([ + { + connector_type: "erp", + connector_type_label: "ERP-System", + connector_instance_id: "acme-erp-1", + credential_kind: "api_key", + masked_credential: "••••9999", + updated_at: new Date().toISOString(), + }, + ]); + vi.mocked(fetchMapping).mockResolvedValue({ + schema: [ + { nexarch_field: "kundennummer", required: true }, + { nexarch_field: "name", required: true }, + ], + fields: [{ nexarch_field: "kundennummer", external_field: "debitorennummer" }], + version: 1, + }); + vi.mocked(saveMapping).mockResolvedValue({ + schema: [ + { nexarch_field: "kundennummer", required: true }, + { nexarch_field: "name", required: true }, + ], + fields: [ + { nexarch_field: "kundennummer", external_field: "debitorennummer" }, + { nexarch_field: "name", external_field: "firmenname" }, + ], + version: 2, + }); + + renderPage(); + await waitFor(() => screen.getByText("Bearbeiten")); + fireEvent.click(screen.getByText("Bearbeiten")); + + await waitFor(() => { + expect(screen.getByLabelText(/^kundennummer/)).toHaveValue("debitorennummer"); + }); + fireEvent.change(screen.getByLabelText(/^name/), { target: { value: "firmenname" } }); + fireEvent.click(screen.getByText("Mapping speichern")); + + await waitFor(() => { + expect(saveMapping).toHaveBeenCalledWith("erp", "acme-erp-1", [ + { nexarch_field: "kundennummer", external_field: "debitorennummer" }, + { nexarch_field: "name", external_field: "firmenname" }, + ]); + }); + await waitFor(() => { + expect(screen.getByText("Version 2")).toBeTruthy(); + }); + }); +}); diff --git a/web/connect-setup/app/setup/page.tsx b/web/connect-setup/app/setup/page.tsx new file mode 100644 index 00000000..537a303e --- /dev/null +++ b/web/connect-setup/app/setup/page.tsx @@ -0,0 +1,275 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { SelectField, Table, TextField, useToast, type TableColumn } from "@nexarch/shl"; +import { + ApiError, + Connection, + ConnectorType, + Entry, + MappingView, + UnauthorizedError, + fetchCatalog, + fetchConnections, + fetchMapping, + saveMapping, + setConnection, +} from "../../lib/api"; + +const CREDENTIAL_KIND_LABEL: Record = { + api_key: "API-Schlüssel", + oauth2: "OAuth2-Zugangsdaten", +}; + +export default function SetupPage() { + const { push } = useToast(); + const [connectors, setConnectors] = useState(null); + const [connections, setConnections] = useState(null); + const [unauthorized, setUnauthorized] = useState(false); + const [loadError, setLoadError] = useState(null); + + const [connectorType, setConnectorType] = useState(""); + const [instanceID, setInstanceID] = useState(""); + const [apiKey, setApiKey] = useState(""); + const [accessToken, setAccessToken] = useState(""); + const [refreshToken, setRefreshToken] = useState(""); + const [saving, setSaving] = useState(false); + + const [activeConnection, setActiveConnection] = useState<{ type: string; instance: string } | null>(null); + const [mapping, setMapping] = useState(null); + const [mappingFields, setMappingFields] = useState>({}); + const [mappingError, setMappingError] = useState(null); + const [savingMapping, setSavingMapping] = useState(false); + + function load() { + Promise.all([fetchCatalog(), fetchConnections()]) + .then(([types, conns]) => { + setConnectors(types); + setConnections(conns); + setUnauthorized(false); + if (!connectorType && types.length > 0) { + setConnectorType(types[0].id); + } + }) + .catch((err) => { + if (err instanceof UnauthorizedError) { + setUnauthorized(true); + return; + } + const message = err instanceof ApiError || err instanceof Error ? err.message : "Connectoren konnten nicht geladen werden."; + setLoadError(message); + push(message, "danger"); + }); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(load, []); + + const selectedType = useMemo(() => connectors?.find((c) => c.id === connectorType) ?? null, [connectors, connectorType]); + + function resetSecretFields() { + setApiKey(""); + setAccessToken(""); + setRefreshToken(""); + } + + async function handleCreateConnection(e: React.FormEvent) { + e.preventDefault(); + if (!selectedType || !instanceID.trim()) { + push("Connector-Typ und Instanz-Kennung sind erforderlich.", "danger"); + return; + } + setSaving(true); + try { + await setConnection(selectedType.id, instanceID.trim(), { + kind: selectedType.credential_kind, + api_key: selectedType.credential_kind === "api_key" ? apiKey : undefined, + access_token: selectedType.credential_kind === "oauth2" ? accessToken : undefined, + refresh_token: selectedType.credential_kind === "oauth2" ? refreshToken : undefined, + }); + push("Verbindung gespeichert.", "success"); + resetSecretFields(); + setInstanceID(""); + load(); + } catch (err) { + const message = err instanceof ApiError || err instanceof Error ? err.message : "Verbindung konnte nicht gespeichert werden."; + push(message, "danger"); + } finally { + setSaving(false); + } + } + + function openMapping(connection: Connection) { + setActiveConnection({ type: connection.connector_type, instance: connection.connector_instance_id }); + setMapping(null); + setMappingError(null); + fetchMapping(connection.connector_type, connection.connector_instance_id) + .then((m) => { + setMapping(m); + const next: Record = {}; + for (const f of m.schema) { + const existing = m.fields.find((e) => e.nexarch_field === f.nexarch_field); + next[f.nexarch_field] = existing?.external_field ?? ""; + } + setMappingFields(next); + }) + .catch((err) => { + const message = err instanceof ApiError || err instanceof Error ? err.message : "Mapping konnte nicht geladen werden."; + setMappingError(message); + }); + } + + async function handleSaveMapping(e: React.FormEvent) { + e.preventDefault(); + if (!activeConnection || !mapping) return; + const fields: Entry[] = mapping.schema + .map((f) => ({ nexarch_field: f.nexarch_field, external_field: (mappingFields[f.nexarch_field] ?? "").trim() })) + .filter((e) => e.external_field !== ""); + setSavingMapping(true); + setMappingError(null); + try { + const updated = await saveMapping(activeConnection.type, activeConnection.instance, fields); + setMapping(updated); + push("Feld-Mapping gespeichert.", "success"); + } catch (err) { + const message = err instanceof ApiError || err instanceof Error ? err.message : "Mapping konnte nicht gespeichert werden."; + setMappingError(message); + } finally { + setSavingMapping(false); + } + } + + if (unauthorized) { + return ( +
+

Anmeldung erforderlich, um Connectoren einzurichten.

+
+ ); + } + if (loadError) { + return ( +
+

{loadError}

+
+ ); + } + + const columns: TableColumn[] = [ + { key: "type", header: "Connector", render: (r) => r.connector_type_label }, + { key: "instance", header: "Instanz", render: (r) => r.connector_instance_id }, + { key: "kind", header: "Art", render: (r) => CREDENTIAL_KIND_LABEL[r.credential_kind] ?? r.credential_kind }, + { key: "credential", header: "Zugangsdaten", render: (r) => {r.masked_credential} }, + { key: "updated", header: "Aktualisiert", render: (r) => new Date(r.updated_at).toLocaleString("de-DE") }, + { + key: "mapping", + header: "Feld-Mapping", + render: (r) => ( + + ), + }, + ]; + + return ( +
+

Connectoren einrichten

+ +
+

Neue Verbindung

+
+ { + setConnectorType(e.target.value); + resetSecretFields(); + }} + > + {(connectors ?? []).map((c) => ( + + ))} + + setInstanceID(e.target.value)} + hint="Frei wählbarer Name dieser Verbindung, z. B. 'mandant-a-prod'." + /> + {selectedType?.credential_kind === "api_key" && ( + setApiKey(e.target.value)} + /> + )} + {selectedType?.credential_kind === "oauth2" && ( + <> + setAccessToken(e.target.value)} + /> + setRefreshToken(e.target.value)} + hint="Optional, ermöglicht automatische Erneuerung." + /> + + )} + + +
+ +
+

Eingerichtete Verbindungen

+ {connections && connections.length === 0 &&

Noch keine Connector-Verbindungen eingerichtet.

} + {connections && connections.length > 0 && ( + `${r.connector_type}/${r.connector_instance_id}`} + caption="Eingerichtete Connector-Verbindungen mit maskierten Zugangsdaten" + /> + )} + + + {activeConnection && ( +
+

+ Feld-Mapping: {activeConnection.type} / {activeConnection.instance} +

+ {mappingError &&

{mappingError}

} + {mapping && ( +
+ {mapping.schema.map((f) => ( + setMappingFields((prev) => ({ ...prev, [f.nexarch_field]: e.target.value }))} + hint="Name des entsprechenden Felds im Drittsystem" + /> + ))} +

Version {mapping.version}

+ + + )} +
+ )} + + ); +} diff --git a/web/connect-setup/lib/api.ts b/web/connect-setup/lib/api.ts new file mode 100644 index 00000000..8e73860d --- /dev/null +++ b/web/connect-setup/lib/api.ts @@ -0,0 +1,113 @@ +// Duenner Client fuer API-01 (connect/internal/connectorsetupapi) — enthaelt +// keine eigene Geschaeftslogik, nur Datenabruf/-versand. Identitaet kommt +// uebergangsweise (wie bei web/connect-status vor der zentralen +// Core-JWT-Anbindung) aus Umgebungsvariablen, die als Header durchgereicht +// werden. + +export class UnauthorizedError extends Error {} +export class ApiError extends Error {} + +function apiBase(): string { + const base = process.env.NEXT_PUBLIC_CONNECT_API_URL; + if (!base) { + throw new Error("NEXT_PUBLIC_CONNECT_API_URL ist nicht gesetzt (Umgebungsvariable erforderlich)"); + } + return base; +} + +function identityHeaders(): Record { + const userID = process.env.NEXT_PUBLIC_USER_ID; + const tenant = process.env.NEXT_PUBLIC_TENANT_SLUG; + const headers: Record = {}; + if (userID) headers["X-User-Id"] = userID; + if (tenant) headers["X-Tenant-Slug"] = tenant; + return headers; +} + +async function req(path: string, init?: RequestInit): Promise { + const res = await fetch(`${apiBase()}${path}`, { + ...init, + headers: { + ...identityHeaders(), + ...(init?.body ? { "Content-Type": "application/json" } : {}), + ...init?.headers, + }, + }); + if (res.status === 401) { + throw new UnauthorizedError("Anmeldung erforderlich, um Connectoren einzurichten."); + } + if (!res.ok) { + const body = await res.json().catch(() => ({})); + throw new ApiError(body.error ?? `Anfrage fehlgeschlagen (${res.status})`); + } + return res.json().catch(() => undefined as T); +} + +export type CredentialKind = "oauth2" | "api_key"; + +export interface FieldDef { + nexarch_field: string; + required: boolean; +} + +export interface Entry { + nexarch_field: string; + external_field: string; +} + +export interface ConnectorType { + id: string; + display_name: string; + credential_kind: CredentialKind; + schema: FieldDef[]; +} + +export interface Connection { + connector_type: string; + connector_type_label: string; + connector_instance_id: string; + credential_kind: CredentialKind; + masked_credential: string; + expires_at?: string; + updated_at: string; +} + +export interface MappingView { + schema: FieldDef[]; + fields: Entry[]; + version: number; +} + +export function fetchCatalog(): Promise { + return req("/connectors/catalog"); +} + +export function fetchConnections(): Promise { + return req("/connectors/connections"); +} + +export interface SetConnectionInput { + kind: CredentialKind; + api_key?: string; + access_token?: string; + refresh_token?: string; + expires_at?: string; +} + +export function setConnection(connectorType: string, instanceID: string, input: SetConnectionInput): Promise { + return req(`/connectors/connections/${encodeURIComponent(connectorType)}/${encodeURIComponent(instanceID)}`, { + method: "PUT", + body: JSON.stringify(input), + }); +} + +export function fetchMapping(connectorType: string, instanceID: string): Promise { + return req(`/connectors/connections/${encodeURIComponent(connectorType)}/${encodeURIComponent(instanceID)}/mapping`); +} + +export function saveMapping(connectorType: string, instanceID: string, fields: Entry[]): Promise { + return req(`/connectors/connections/${encodeURIComponent(connectorType)}/${encodeURIComponent(instanceID)}/mapping`, { + method: "PUT", + body: JSON.stringify({ fields }), + }); +} diff --git a/web/connect-setup/next.config.mjs b/web/connect-setup/next.config.mjs new file mode 100644 index 00000000..aa96725f --- /dev/null +++ b/web/connect-setup/next.config.mjs @@ -0,0 +1,5 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + transpilePackages: ["@nexarch/shl"], +}; +export default nextConfig; diff --git a/web/connect-setup/package-lock.json b/web/connect-setup/package-lock.json new file mode 100644 index 00000000..958b3da0 --- /dev/null +++ b/web/connect-setup/package-lock.json @@ -0,0 +1,3290 @@ +{ + "name": "nexarch-connect-setup", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nexarch-connect-setup", + "dependencies": { + "@nexarch/shl": "file:../shl", + "next": "16.3.5", + "react": "19.3.0", + "react-dom": "19.3.0" + }, + "devDependencies": { + "@testing-library/jest-dom": "^7.0.1", + "@testing-library/react": "16.3.3", + "@types/node": "^22.20.4", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "jsdom": "24.1.0", + "typescript": "5.5.3", + "vitest": "^5.0.1" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.10.tgz", + "integrity": "sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nexarch/shl": { + "version": "0.1.0", + "resolved": "file:../shl", + "peerDependencies": { + "react": "^19.3.0", + "react-dom": "^19.3.0" + } + }, + "node_modules/@next/env": { + "version": "16.3.5", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.3.5.tgz", + "integrity": "sha512-NWEXVDMqoEo0ktmU6u0sE2Vg0LOcsD7NnOTJNo3/fEaTfsg+F1bMIxuDmQbda4e3yTIQwVdUREF2yIuMOusKtg==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.3.5.tgz", + "integrity": "sha512-pMmGgETfKvElucLHtVaeiMRbp2zUbvKx7b1yGko0liBz3cw1mKSggWN/Rp/wPz8z+E1O82u3r4L1Co+ZS5hokQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.3.5.tgz", + "integrity": "sha512-76VaGYvf6HPa5/w12yLkE3dXTn9AfdEviI79oEL3aZoAmRLc9rWitjWqyjViVysK/ht/y9YKzFkBrUdi/wGkow==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.3.5.tgz", + "integrity": "sha512-zKDELJ5jSQMHeO/hmXUQsAzagX4bQD4OiMi3pQ5FbUj+yK506oLVHnKA2YXMlbg1EHHqJYtyePOgByIDXD1lqw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.3.5.tgz", + "integrity": "sha512-7Vql0pgzCoHagv6+FNOZoqmJqA52c6zeVbhtS/47qFozO1MSx4ms7x7GHiciY8R5CDsSMKMQjJEryoJLcsBIbA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.3.5.tgz", + "integrity": "sha512-NH/xzehyHEFWE2nlcZon7TB/0+H4shfWCi7S1zka815XCOhJDYZhoeJtOYy0dh0WVRWACVXSyGNFFytoMxUhRg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.3.5.tgz", + "integrity": "sha512-lV4+EhWMfS8jcC+EH2nn/Cm5cn6XsgbE07bU9tMH8fCo0tNAqhyzi1b5wQ/Tn6NGFTvKDY65w3ZH95EjwBRAnQ==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.3.5.tgz", + "integrity": "sha512-/wKzAREX2RF++MhicjDbg8tGn2AiBIM0+EFeTFKoUEUbW5D6amCJehd5Z5G1H5/gxNdgnwoXMcHz24H/c2tGkQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.3.5.tgz", + "integrity": "sha512-LNdCHzgLFc+UeqMS84LzXPaeBRKyqDN9OMyFAr1OrB0XrNw78IRrEVtZvvA7245W/HsaoeVOQX9jPjPk8jojwA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.154.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.154.0.tgz", + "integrity": "sha512-KYOUGQFlLWR+s4QMV2Q9ko38TZm7EdO87Qm7rvZLqk/webse/cDjtyxKhg/47l0byCbs/k46LT8qc4PKjAikzA==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.14.tgz", + "integrity": "sha512-aWR+XKH9+rW7m5fWq/5360J3mHycrx/o6xZM2DQFdJnTMdyP7/WzxPjhtLbVWdKo2y612/4U1bWqb3HQhiF7vg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.14.tgz", + "integrity": "sha512-QiVxHofl6A8lQU7WAtOUY4QMVZXohxwz7WJJoeh8NBEUyKfIkMRKhYyq8az/1RypNiIXzNTtiS6KwHz0erxYRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.14.tgz", + "integrity": "sha512-lTQOzHuBIooh3jIr5XINQkjPGEuGy+Dsy0meXDr6Sjty0b99kYgGh4uBC/8mJX1T0kC8d6S0aPP1Y+bHkh6aBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.14.tgz", + "integrity": "sha512-hyMKEGatjmBYCs/Q4sbIcL/GqpFEqsX3wc+HYVQ/85A4rxc1o7rPm4DUI4GV4k/h0ITddwFSGpqQunI14GXjTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.14.tgz", + "integrity": "sha512-u371GbXoVXrEgP20PwzoGNOdAOqAMhzo19JzzY+V4YmmnC+DTOoqdbMu+O+54emM59Mik/UdrtWgcuEDKWRPKA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.14.tgz", + "integrity": "sha512-BqwKsdZQf6EqVWr8fv2MrDYzAvOZ7+NYFSJESRGs87fkseEhs0TU9TXgRJTkZoRRkxTizyEg1LKUXfU3/78uqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.14.tgz", + "integrity": "sha512-SD9kY8M009x/7s57Z1dgUVlP5+uYBgKQK0Tkr1WIWngrtUJmRCW1RJFMY7QDla2khXhmg2i5Io7MUjcmZNZ/KQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.14.tgz", + "integrity": "sha512-BJ2CL19M6uuWesMgnqJzgxoI+N+eV7b0A9O9vLV42y+gitC41fNa8KElau1tHVzuLWX5GzEuxMPhAO0tQXQMkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.14.tgz", + "integrity": "sha512-4+Kl8daPV8NDHaD7mfCcc1pRM+Qg7ab8DyMm5FG5WoZgv4h2i3KFN1CybYBNbQqmuYm7ftJLvmMI1eHYJ22S/Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.14.tgz", + "integrity": "sha512-V/h8ePq1fKDrmtFMz6xcCN3DKvuvMRrwwD1q0PkBeeo4DpK64biaKvqWGVxqCeYKqeRk7gE7/iKPxLCn08O97A==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.14.tgz", + "integrity": "sha512-WxalXzgrLVpKC93xfpaJ3gCXUWzeCr+FRCbDVD2mUAy4L6Vo3lNVz0epUEUpxPWJkLNYUwM4OSIikv36OJ9mLg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.14.tgz", + "integrity": "sha512-3G3tnAmTpdk9jO3GQtyHXqeFD2SlecfTws/Y3M2mmj9nMBwwuLT4kur8sZKeu7ENUHSpVO3cIsm5A52LEWWwlg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.14.tgz", + "integrity": "sha512-qyr5XiNGxm2ytx17vvtcJ9nXn6v1gENETi503bJx3OGwZ1tSonj7BosxbMa/xeAfSl5iS9X6KjEWYXLu6v1L7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.14.tgz", + "integrity": "sha512-oA4o5o0S2yB199YyYXDzKUv1Z56Rc+mgOjwm7Z/vJ9Z8igiDPYvoOuDyo6g7AK/ZaF09nPhDKVnGBcYHO11GXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.14.tgz", + "integrity": "sha512-9mE94bf2GZUaR/fUSXCMpBoWkctu421Mi0ORpm4zqfbkjKep7RxzPFa5etcGs5h+2KApoWBOA/hAJa/7saGgEQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@swc/helpers": { + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.2.tgz", + "integrity": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.1.tgz", + "integrity": "sha512-oMDTC3oA+6CXSO2JZnvOI7CA6oVub6kij5ggk9ohwye5slmkwxYDXcPOVxgMw/RQlticjtO0C1RZkR97HgrWMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=22", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "@testing-library/dom": ">=10 <11", + "vitest": ">= 0.32" + }, + "peerDependenciesMeta": { + "vitest": { + "optional": true + } + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz", + "integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz", + "integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@vitest/spy": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.5.tgz", + "integrity": "sha512-lwJRExuH417257Q5wv9QYugN9IMJkoxcrKrYxGPQkBzl7wlNpTUxD/5yNX4ESKVUIShKPgkGchYPFMWOOk0wzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.29", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.29.tgz", + "integrity": "sha512-YeesxhUmR4h5y5V8KF8iBCqJb8YFrBJ++XBTeGcWIaQOYXGN19JM0FoiOjAhwuDoln8owMgXcWBmgHtYSkky2w==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001816", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001816.tgz", + "integrity": "sha512-+8Yx7BXCoqk6IF8yTicjfUR6D/ldJzdlsodlEOiOSPmhK844WIgk67/zIQaGAOxNkZyyvBlrRFnLvqKIoqbAFw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cssstyle/node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/jsdom": { + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.0.tgz", + "integrity": "sha512-6gpM7pRXCwIOKxX47cgOyvyQDN/Eh0f1MeKySBV2xGdKtqJBLj8P25eY3EVCWo2mglDDzozR2r2MW4T+JiNUZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.0.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.4", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.10", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.4", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.17.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "peer": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz", + "integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "16.3.5", + "resolved": "https://registry.npmjs.org/next/-/next-16.3.5.tgz", + "integrity": "sha512-MdtsTgzyfCPRLC6uJ1mN8ao7lyJ4BB0U6Inhnx3gta1UcCIdHK3yxLG0E8OWQteWD8/Q0qb8A5o7wJaL8M9y2w==", + "license": "MIT", + "dependencies": { + "@next/env": "16.3.5", + "@swc/helpers": "0.5.23", + "baseline-browser-mapping": "^2.9.19", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.5.23", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.3.5", + "@next/swc-darwin-x64": "16.3.5", + "@next/swc-linux-arm64-gnu": "16.3.5", + "@next/swc-linux-arm64-musl": "16.3.5", + "@next/swc-linux-x64-gnu": "16.3.5", + "@next/swc-linux-x64-musl": "16.3.5", + "@next/swc-win32-arm64-msvc": "16.3.5", + "@next/swc-win32-x64-msvc": "16.3.5", + "sharp": "^0.35.4" + }, + "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" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/nwsapi": { + "version": "2.2.28", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.28.tgz", + "integrity": "sha512-IlVB7OS7qrOsVYlpnFIkETjMwT9jwvmocJmmM+GZU/PAB3uGi9Ezd7vcWhWBUnSc0ya4ppmQITOyP1ez9gg8cg==", + "dev": true, + "license": "MIT" + }, + "node_modules/obug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/obug/-/obug-3.0.0.tgz", + "integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.0" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/rolldown": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.14.tgz", + "integrity": "sha512-eTBTTqMR6uHKLgcUqpJP8ntzDI6V4LYA8negAMBEI7gkgVvOdrx6VsvS2Ym+83KhOPdLCHWJu0w7UQFJZhHKNw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@oxc-project/types": "=0.154.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.14", + "@rolldown/binding-android-arm64": "1.2.14", + "@rolldown/binding-darwin-arm64": "1.2.14", + "@rolldown/binding-darwin-x64": "1.2.14", + "@rolldown/binding-freebsd-x64": "1.2.14", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.14", + "@rolldown/binding-linux-arm64-gnu": "1.2.14", + "@rolldown/binding-linux-arm64-musl": "1.2.14", + "@rolldown/binding-linux-ppc64-gnu": "1.2.14", + "@rolldown/binding-linux-s390x-gnu": "1.2.14", + "@rolldown/binding-linux-x64-gnu": "1.2.14", + "@rolldown/binding-linux-x64-musl": "1.2.14", + "@rolldown/binding-openharmony-arm64": "1.2.14", + "@rolldown/binding-win32-arm64-msvc": "1.2.14", + "@rolldown/binding-win32-x64-msvc": "1.2.14" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/std-env": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.2.tgz", + "integrity": "sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/vite": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.4.tgz", + "integrity": "sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.29", + "rolldown": "~1.2.12", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/postcss": { + "version": "8.5.30", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.30.tgz", + "integrity": "sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/vitest": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.5.tgz", + "integrity": "sha512-cPODOMF8YnkiiYtDdHo6n4wWmBrOBBbOIYH2d33M4l3CdNn72h+qMmO6uXffUN6A5AZFXNND/d+krS9mgkXRfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/mocker": "5.0.5", + "chai": "^6.3.0", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.4.3", + "obug": "^3.0.0", + "picomatch": "^4.0.7", + "std-env": "^4.3.0", + "tinybench": "^6.2.2", + "tinyexec": "^1.3.1", + "tinyglobby": "^0.2.17", + "why-is-node-running": "3.2.1" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.0.5", + "@vitest/browser-preview": "5.0.5", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.0.5", + "@vitest/coverage-v8": "5.0.5", + "@vitest/ui": "5.0.5", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.5.tgz", + "integrity": "sha512-xLL3lkDegJZw+MGoGaCCsFayDZthGgpAtr3Vziyfc4p7xH89K7KRPcRBBh/kEYm0Aqkt5DtTmkYpBtRV/FrHZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.0.5", + "estree-walker": "^3.0.3", + "magic-string": "^1.4.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9 || ^3.0.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/why-is-node-running": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.1.tgz", + "integrity": "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==", + "dev": true, + "license": "MIT", + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=20.11" + } + }, + "node_modules/ws": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/web/connect-setup/package.json b/web/connect-setup/package.json new file mode 100644 index 00000000..d063ab63 --- /dev/null +++ b/web/connect-setup/package.json @@ -0,0 +1,26 @@ +{ + "name": "nexarch-connect-setup", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "test": "vitest run" + }, + "dependencies": { + "@nexarch/shl": "file:../shl", + "next": "16.3.5", + "react": "19.3.0", + "react-dom": "19.3.0" + }, + "devDependencies": { + "@testing-library/jest-dom": "^7.0.1", + "@testing-library/react": "16.3.3", + "@types/node": "^22.20.4", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "jsdom": "24.1.0", + "typescript": "5.5.3", + "vitest": "^5.0.1" + } +} diff --git a/web/connect-setup/tsconfig.json b/web/connect-setup/tsconfig.json new file mode 100644 index 00000000..1c9cb74f --- /dev/null +++ b/web/connect-setup/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": false, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [{ "name": "next" }], + "paths": { "@/*": ["./*"] } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/web/connect-setup/vitest.config.ts b/web/connect-setup/vitest.config.ts new file mode 100644 index 00000000..262024c2 --- /dev/null +++ b/web/connect-setup/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "jsdom", + setupFiles: ["./vitest.setup.ts"], + }, +}); diff --git a/web/connect-setup/vitest.setup.ts b/web/connect-setup/vitest.setup.ts new file mode 100644 index 00000000..1a3d75c7 --- /dev/null +++ b/web/connect-setup/vitest.setup.ts @@ -0,0 +1,7 @@ +import "@testing-library/jest-dom/vitest"; +import { afterEach } from "vitest"; +import { cleanup } from "@testing-library/react"; + +// SHL-01-Erfahrung (bfa5c61): ohne afterEach(cleanup) stapeln sich +// gerenderte DOM-Bäume zwischen Tests. +afterEach(cleanup);