commit 5c9a7a0f64f858188822d60c9999a0c89b384c07 Author: Tom Peltonen Date: Fri Oct 23 23:24:54 2026 +1100 Initial commt for working copy generated diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2482e59 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +.git +.gitignore +README.md +docs +deploy +**/node_modules +**/dist +**/*.db +**/*.db-shm +**/*.db-wal +**/*.log +**/.env +backend/kereru-forms +backend/kereru-forms.exe diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..97b6937 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Dependencies +node_modules/ +frontend/dist/ +backend/kereru-forms +backend/kereru-forms.exe + +# Databases +*.db +*.db-shm +*.db-wal +*.sqlite +*.sqlite3 +data/ + +# Env files (keep examples) +.env +.env.local +!.env.example + +# Logs / OS / editors +*.log +.DS_Store +Thumbs.db +.idea/ +.vscode/ +*.swp + +# Test / coverage +coverage/ +*.out diff --git a/README.md b/README.md new file mode 100644 index 0000000..3f7a853 --- /dev/null +++ b/README.md @@ -0,0 +1,114 @@ +# Kererū Forms + +Simple, versioned surveys with a Go + OpenAPI backend and a React frontend. + +- **Backend**: Go 1.26, Gin, OpenAPI-generated server (`oapi-codegen`), SQLite via `modernc.org/sqlite` (pure Go), zerolog, OpenTelemetry. +- **Frontend**: React 18, Vite, TypeScript, Tailwind CSS, React Router, Vitest. +- **Deployment**: single container serving both the API and the built SPA, plus Docker Compose, Podman and Kubernetes manifests. + +## Quick start + +### 1. Backend + +```bash +cd backend +go mod download +go run ./cmd/kereru-forms --listen-addr 0.0.0.0 --port 8080 +``` + +The API listens on . The SQLite database (`kereru.db`) and its schema are created automatically. + +### 2. Frontend + +```bash +cd frontend +npm install +npm run dev +``` + +Open . Vite proxies `/api/*` to `http://localhost:8080/api/*`. The API is also available without the `/api` prefix for probes and direct clients. + +## Configuration + +Settings are resolved in this order (highest wins): + +1. CLI flags +2. Environment variables +3. `.env` file next to the executable, then the working directory +4. YAML/JSON file passed with `--conf` +5. Built-in defaults + +```bash +kereru-forms --conf ./config/config.example.yaml +``` + +| Flag | Environment variable | Default | +| --- | --- | --- | +| `--conf` | – | – | +| `--log-level` | `KERERU_LOG_LEVEL` | `info` | +| `--log-format` | `KERERU_LOG_FORMAT` | `console` | +| `--listen-addr` | `KERERU_LISTEN_ADDR` | `0.0.0.0` | +| `--listen-addr6` | `KERERU_LISTEN_ADDR6` | – | +| `--port` | `KERERU_PORT` | `8080` | +| `--db-driver` | `KERERU_DB_DRIVER` | `sqlite` | +| `--db-dsn` | `KERERU_DB_DSN` | `file:kereru.db?...` | +| `--static-dir` | `KERERU_STATIC_DIR` | – | +| – | `KERERU_TELEMETRY_ENABLED` | `false` | +| – | `OTEL_EXPORTER_OTLP_ENDPOINT` | `localhost:4318` | +| – | `KERERU_CORS_ORIGINS` | `http://localhost:5173` | +| – | `KERERU_ENVIRONMENT` | `development` | + +Both an IPv4 and an IPv6 listener may be configured at the same time. Set `--listen-addr ""` to disable the IPv4 listener. + +Frontend build-time setting: `VITE_API_BASE_URL` (defaults to `/api`; set to `http://localhost:8080` when the frontend is hosted separately). + +Sample files live in [`config/`](config). + +## API + +The contract is defined in [`backend/openapi/openapi.yaml`](backend/openapi/openapi.yaml) and the server stubs are generated with: + +```bash +cd backend +oapi-codegen -config oapi-codegen.yaml openapi/openapi.yaml +``` + +Interactive API documentation is available at . Swagger UI and the OpenAPI specification are bundled with the frontend, so the docs work without an internet connection. Forms support optional linked PDFs, digital signatures, external reference identifiers and offline PDF uploads. + +Key routes: + +| Method | Path | Purpose | +| --- | --- | --- | +| `GET` | `/health` | Health and version | +| `GET`/`POST` | `/forms` | List / create forms | +| `GET`/`PUT`/`DELETE` | `/forms/{formId}` | Read / update / delete a form version | +| `POST` | `/forms/{formId}/activate` | Activate a form version | +| `POST` | `/forms/{formId}/inactivate` | Inactivate a form version | +| `POST` | `/forms/{formId}/versions` | Clone into a new version | +| `GET` | `/forms/{formId}/pdf` | Printable PDF of the definition | +| `GET`/`POST` | `/forms/{formId}/submissions` | List / submit responses | +| `GET` | `/submissions/{submissionId}` | Submission details | +| `GET` | `/submissions/{submissionId}/offline-pdf` | Download an uploaded offline PDF | +| `GET`/`POST` | `/forms/{formId}/access-codes` | List / generate single-use codes | + +The specification is served at `/openapi.yaml` in development and production builds. + +## Tests + +```bash +cd backend && go test ./... +cd frontend && npm test +``` + +## Docker / Podman / Kubernetes + +See [`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md) and [`deploy/k8s.yaml`](deploy/k8s.yaml). + +## Documentation + +- [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) — components, data model, request flow, extension points. +- [`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md) — development, Docker, Podman and Kubernetes instructions. + +## Security note + +The management endpoints are intentionally unauthenticated to match the current scope. Deploy behind an authenticating reverse proxy or add an auth layer before exposing management to untrusted networks. Access codes protect individual form submissions, not administration. diff --git a/backend/cmd/kereru-forms/main.go b/backend/cmd/kereru-forms/main.go new file mode 100644 index 0000000..06b51be --- /dev/null +++ b/backend/cmd/kereru-forms/main.go @@ -0,0 +1,121 @@ +// Command kereru-forms serves the Kererū Forms HTTP API. +package main + +import ( + "context" + "errors" + "fmt" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "kereru.forms/internal/api" + "kereru.forms/internal/config" + "kereru.forms/internal/database" + "kereru.forms/internal/logging" + "kereru.forms/internal/telemetry" +) + +// version is overridden at build time with -ldflags "-X main.version=...". +var version = "1.0.0" + +func main() { + if err := run(); err != nil { + fmt.Fprintf(os.Stderr, "kereru-forms: %v\n", err) + os.Exit(1) + } +} + +func run() error { + cfg, err := config.Load(os.Args[1:]) + if err != nil { + return err + } + + logger := logging.New(cfg.LogLevel, cfg.LogFormat, cfg.Environment) + logger.Info(). + Str("version", version). + Str("environment", cfg.Environment). + Strs("listen", cfg.Addresses()). + Str("database_driver", cfg.DBDriver). + Msg("starting kereru-forms") + + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + + tel, err := telemetry.Setup(ctx, telemetry.Options{ + Enabled: cfg.TelemetryEnabled, + Endpoint: cfg.OTLPEndpoint, + ServiceName: "kereru-forms", + Environment: cfg.Environment, + Version: version, + }) + if err != nil { + return fmt.Errorf("setup telemetry: %w", err) + } + defer func() { + shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + if err := tel.Shutdown(shutdownCtx); err != nil { + logger.Warn().Err(err).Msg("telemetry shutdown") + } + }() + + store, err := database.Open(cfg.DBDriver, cfg.DBDSN) + if err != nil { + return fmt.Errorf("open database: %w", err) + } + defer func() { + if err := store.Close(); err != nil { + logger.Warn().Err(err).Msg("database close") + } + }() + + server := api.NewServer(store, logger, version) + router := api.NewRouter(server, logger, api.RouterOptions{ + CORSOrigins: cfg.CORSOrigins, + ServiceName: "kereru-forms", + StaticDir: cfg.StaticDir, + }) + + addrs := cfg.Addresses() + servers := make([]*http.Server, 0, len(addrs)) + errCh := make(chan error, len(addrs)) + for _, addr := range addrs { + srv := &http.Server{ + Addr: addr, + Handler: router, + ReadHeaderTimeout: 10 * time.Second, + ReadTimeout: 30 * time.Second, + WriteTimeout: 60 * time.Second, + IdleTimeout: 120 * time.Second, + } + servers = append(servers, srv) + go func(srv *http.Server) { + logger.Info().Str("addr", srv.Addr).Msg("http server listening") + if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + errCh <- fmt.Errorf("listen on %s: %w", srv.Addr, err) + } + }(srv) + } + + select { + case <-ctx.Done(): + logger.Info().Msg("shutdown signal received") + case err := <-errCh: + logger.Error().Err(err).Msg("http server failed") + stop() + } + + shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + for _, srv := range servers { + if err := srv.Shutdown(shutdownCtx); err != nil { + logger.Warn().Err(err).Str("addr", srv.Addr).Msg("http server shutdown") + } + } + logger.Info().Msg("kereru-forms stopped") + return nil +} diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 0000000..2410147 --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,78 @@ +module kereru.forms + +go 1.26.0 + +require ( + github.com/getkin/kin-openapi v0.152.0 + github.com/gin-gonic/gin v1.12.0 + github.com/go-pdf/fpdf v0.9.0 + github.com/google/uuid v1.6.0 + github.com/oapi-codegen/runtime v1.8.0 + github.com/rs/zerolog v1.35.1 + go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.72.0 + go.opentelemetry.io/otel v1.47.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.47.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0 + go.opentelemetry.io/otel/sdk v1.47.0 + go.opentelemetry.io/otel/sdk/metric v1.47.0 + go.opentelemetry.io/otel/trace v1.47.0 + gopkg.in/yaml.v3 v3.0.1 + modernc.org/sqlite v1.61.0 +) + +require ( + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/bytedance/gopkg v0.1.4 // indirect + github.com/bytedance/sonic v1.15.4 // indirect + github.com/bytedance/sonic/loader v0.5.2 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudwego/base64x v0.1.7 // indirect + github.com/dustin/go-humanize v1.1.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.15 // indirect + github.com/gin-contrib/sse v1.1.2 // indirect + github.com/go-logr/logr v1.4.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v1.0.0 // indirect + github.com/go-playground/locales v0.14.2 // indirect + github.com/go-playground/universal-translator v0.18.2 // indirect + github.com/go-playground/validator/v10 v10.30.5 // indirect + github.com/goccy/go-json v0.11.2 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.4.0 // indirect + github.com/leodido/go-urn v1.5.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.24 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/ncruces/go-strftime v1.1.0 // indirect + github.com/oasdiff/yaml v0.1.1 // indirect + github.com/oasdiff/yaml3 v0.0.14 // indirect + github.com/pelletier/go-toml/v2 v2.4.3 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.63.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.2 // indirect + go.mongodb.org/mongo-driver/v2 v2.9.1 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 // indirect + go.opentelemetry.io/otel/log v1.47.0 // indirect + go.opentelemetry.io/otel/metric v1.47.0 // indirect + go.opentelemetry.io/proto/otlp v1.11.0 // indirect + golang.org/x/arch v0.31.0 // indirect + golang.org/x/crypto v0.57.0 // indirect + golang.org/x/net v0.59.0 // indirect + golang.org/x/sys v0.49.0 // indirect + golang.org/x/text v0.42.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc // indirect + google.golang.org/grpc v1.83.2 // indirect + google.golang.org/protobuf v1.36.12 // indirect + modernc.org/libc v1.79.0 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.12.1 // indirect +) diff --git a/backend/go.sum b/backend/go.sum new file mode 100644 index 0000000..2d91da9 --- /dev/null +++ b/backend/go.sum @@ -0,0 +1,228 @@ +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM= +github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4= +github.com/bytedance/sonic v1.15.4 h1:FgtV/4aBHpla9AxuMpuuzVUpa/Cf3izufkxNmnEzdI8= +github.com/bytedance/sonic v1.15.4/go.mod h1:8e51yTPdY8M6t+vvGL1c2Y1xL9i+frEeIAQAEl75NUc= +github.com/bytedance/sonic/loader v0.5.2 h1:0QtP1gevc1OZ6/H8Lb9BRZiCXd1Ftjd3OKuj1T1lBIo= +github.com/bytedance/sonic/loader v0.5.2/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudwego/base64x v0.1.7 h1:NppS+Fgzg5ovhn4NkUXaDT3x9jldgH5ToMCqzBSi2zI= +github.com/cloudwego/base64x v0.1.7/go.mod h1:Cu1PV9zfrSf7ET2tIbWbbEy7jO7HHJ13q4X2SQ8aWYg= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dustin/go-humanize v1.1.0 h1:dbKTrvD0klcbBV/h4AWJdMuZogJACoMlvWIWZ5b2xWg= +github.com/dustin/go-humanize v1.1.0/go.mod h1:hc1CvRkJMsgxqjmjMQF3QNRAZBwY8AXBAzKYoSX9sFI= +github.com/gabriel-vasile/mimetype v1.4.15 h1:05iP/CYtZ/w455R/KZM6rZ5ieAdh99UPtd+d3YzLmaI= +github.com/gabriel-vasile/mimetype v1.4.15/go.mod h1:azpTcoLcDZRNgFou5j+APrqQx9HqVPWa6ijYQIIVswQ= +github.com/getkin/kin-openapi v0.152.0 h1:XRVo77lMRyOGMHf1paMu9lxX3wzuQDZeprssZJnz/x8= +github.com/getkin/kin-openapi v0.152.0/go.mod h1:1+BHDzstro+P5CKtPy1X4PfofnFgmRe6uvMy9+r9fKY= +github.com/gin-contrib/sse v1.1.2 h1:MU2fgl1RrdYTMcgJLtz2kJF+vPg3xrqaaKfUUU18tCo= +github.com/gin-contrib/sse v1.1.2/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s= +github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= +github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxgL3h1H8s= +github.com/go-openapi/jsonpointer v1.0.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y= +github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug= +github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= +github.com/go-pdf/fpdf v0.9.0 h1:PPvSaUuo1iMi9KkaAn90NuKi+P4gwMedWPHhj8YlJQw= +github.com/go-pdf/fpdf v0.9.0/go.mod h1:oO8N111TkmKb9D7VvWGLvLJlaZUQVPM+6V42pp3iV4Y= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.2 h1:d8UmcrM6Nip0hfGZKLGpAvZH37XB4TS0xzK9B56YNCY= +github.com/go-playground/locales v0.14.2/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.2 h1:LCsMLC9RzmbUMNUPVYD15dmcjwYAJhmX8mPZRW4rAVU= +github.com/go-playground/universal-translator v0.18.2/go.mod h1:67VZIMp5lQpDWlnStOct22q1bkdJGJqHghbOtmkawxk= +github.com/go-playground/validator/v10 v10.30.5 h1:YyCXvVShZbs2Sm3Mb53eNOlhRXctSOzW5QJAouCTZL4= +github.com/go-playground/validator/v10 v10.30.5/go.mod h1:wEqiaov48pXX1kjhc3Da8y0M0Dtg/BK7gurFBLgwFrQ= +github.com/goccy/go-json v0.11.2 h1:jdZv93Tt4ioR8yW1CoNsvSxrcZlCXAUU1aZXN7gpXUA= +github.com/goccy/go-json v0.11.2/go.mod h1:3NdmfEkZlB7YI5UFw/qdFKq8XN1aiWR0YyRPWZNQltY= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20261016150608-1facf8f91b50 h1:PlNWJRxu5APSV0pWqfIsZ0qt9OOGHSWfZQ4zvx+WgWs= +github.com/google/pprof v0.0.0-20261016150608-1facf8f91b50/go.mod h1:yUo1AMMpsPIDFGuwDK8nLcf8+vvAd5l2O6JDZ6hpwrM= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 h1:/Tnpcb2E0Pz/tN9s3bfEY2Q8ePCEX9iuS+cneUwncnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0/go.mod h1:zOBXOsUaBSjKgmH4OGzV1esUpR3oUSCPYVd2cUBjKYY= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw= +github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.5.0 h1:pLqT2kq1zpHW/1D18QMjMpdtX7cekxqtJJjg5ANyWw0= +github.com/leodido/go-urn v1.5.0/go.mod h1:9BORnCDhdPBJNDEX+w1bJisa8yOKYi116VeO96s4ifE= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= +github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/ncruces/go-strftime v1.1.0 h1:vtPmPWMkb539RGV9WpWT9EwJGvrOCKvMNI4obWL47Hk= +github.com/ncruces/go-strftime v1.1.0/go.mod h1:yVaq7iwCN09vHRrKBrrflXT0M9tTMG2KmZ9Fj7xeG/g= +github.com/oapi-codegen/nullable v1.1.0 h1:eAh8JVc5430VtYVnq00Hrbpag9PFRGWLjxR1/3KntMs= +github.com/oapi-codegen/nullable v1.1.0/go.mod h1:KUZ3vUzkmEKY90ksAmit2+5juDIhIZhfDl+0PwOQlFY= +github.com/oapi-codegen/runtime v1.8.0 h1:RJzEgQSJQADEPaG1cYuSCKYai/cCVRE2xnkrqhJs/3c= +github.com/oapi-codegen/runtime v1.8.0/go.mod h1:GwV7hC2hviaMzj+ITfHVRESK5J2W/GefVwIND/bMGvU= +github.com/oasdiff/yaml v0.1.1 h1:6nHx+pn9gBRM6YpBlFZFQGCCd1nuvqOBtTD3KKTgGxY= +github.com/oasdiff/yaml v0.1.1/go.mod h1:EYJNoyktvWMJ0Hmhx+6qTaqMOsalUaRGT8Sj1hNcegU= +github.com/oasdiff/yaml3 v0.0.14 h1:aLJee3hxBK2H5wdXd9iPcIXb93Nty1Ge0pT171eHtkw= +github.com/oasdiff/yaml3 v0.0.14/go.mod h1:csto2xfDjYccdUn/yw/bPjj/cYTdp6HtFA0J4TWG+gg= +github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY= +github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0= +github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.63.0 h1:LIFGHI4PFUhhw2dDD1ARHdCff143ffMHwZtbnbuJ78A= +github.com/quic-go/quic-go v0.63.0/go.mod h1:RAro2j2yN9a9EiPACLHT9IB2NXCvGQmmo/alT0yYI0w= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI= +github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.3/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.2 h1:zkEASHHyEClGeURfgNT9PJZVfAbs9oEX9QXggwWNJbc= +github.com/ugorji/go/codec v1.3.2/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +go.mongodb.org/mongo-driver/v2 v2.9.1 h1:jewiFs2m1/VOQp8qhFshX6hWZ+EAXDhZHXExAUMcOgQ= +go.mongodb.org/mongo-driver/v2 v2.9.1/go.mod h1:SHKN0IWkKmEVGHLjXnni6s4wPKX4v86FTgOeJJFuXcA= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.72.0 h1:Vgm7vrC26vvecffpDcy2UINqBvASUg5qkfZle3Yrkug= +go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.72.0/go.mod h1:zgCChxie/dA3qG7IgZGswax6voxkOR4+33msLT/RgNg= +go.opentelemetry.io/contrib/propagators/b3 v1.47.0 h1:vlKDmtjhe1R0d7kdov/+KVTKWOKeGLyohB+iXoDPRMQ= +go.opentelemetry.io/contrib/propagators/b3 v1.47.0/go.mod h1:0zRiXIfXU30PKYMsdbqYbGK8iHF8KqyPi0qdXT2wAuE= +go.opentelemetry.io/otel v1.47.0 h1:j7ALJ/zgkS7Z6aeJW09p8VC9804bC+PpeTfCD4XPnOM= +go.opentelemetry.io/otel v1.47.0/go.mod h1:8wS9O2qfXrYrzp6hIF/HOYJJf/wIhFPhR2xLuP+iXQU= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.47.0 h1:7kszRN4Ze5GeAkUnBjBSUIunVBMA+mZIG04auueqguM= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.47.0/go.mod h1:CLzakYy8jOdkBfcveCWYkeKJxujcF4xPhh2Z7IfE98U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 h1:julhjPeUH/q/7hinbSdDdqt5h7Zw9YWmRlWRhI0jd54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0/go.mod h1:Ao2mz688LH/tFf0yMAenidq6k2YNSx6SIY2q6jDACck= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0 h1:aXZXsZ012wOgVkqaiQv+Z/d8V0xKjmg70F6m4CE94lc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0/go.mod h1:ziu1gUIJhAaxM3EV1e3Ralk5AAyL1UOYz3lJJ4VSI38= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.47.0 h1:N3YQCxjxQ/bMjyc3heladfRm9t9RTksGQH8z4w6yU/0= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.47.0/go.mod h1:Mp8HOFqcaUyypCuGv9IhDdTHnJ56lSudSHMd+pVSCEA= +go.opentelemetry.io/otel/log v1.47.0 h1:cOTS1CcLbSQeZKanGJ+0JpF/+t4PELi3O3bbl2lqCcI= +go.opentelemetry.io/otel/log v1.47.0/go.mod h1:9byitSQ5pLC6PpqwGXjqdMKya6ZTswHRZh2vvXT33nw= +go.opentelemetry.io/otel/metric v1.47.0 h1:4PptaldXx3Eat1XjMZ68pPJEs5wrhlemctZE9a3UdWY= +go.opentelemetry.io/otel/metric v1.47.0/go.mod h1:ADGSXxRrXM6bjbvLo535EstVFlPpPYZm4LBKixjDHwU= +go.opentelemetry.io/otel/metric/x v0.69.0 h1:DjRLr15H83v+hCW7JA9NoJvOkYTtmq5YoDRbe9deYpM= +go.opentelemetry.io/otel/metric/x v0.69.0/go.mod h1:uVvsMPMFFyj/HUQfrUnH3JjnOQ1dwFDorgFLRBasM0k= +go.opentelemetry.io/otel/sdk v1.47.0 h1:zWXEr4j2lFefG87TU6Yg8a7ngfohIKFZHKp0Hf5hC6I= +go.opentelemetry.io/otel/sdk v1.47.0/go.mod h1:VUc24kiOeoGsxG8G9ULx3fWKvB7jMhnGE8Oi607lgR0= +go.opentelemetry.io/otel/sdk/metric v1.47.0 h1:lfISg2j93VT6yqdk9OfUaZmw/GfcZqCCV3jdXtsPnKw= +go.opentelemetry.io/otel/sdk/metric v1.47.0/go.mod h1:ypLp+mW1Nt2x+Szt3b5/i1syodyts49lMOwxpDI3VGw= +go.opentelemetry.io/otel/trace v1.47.0 h1:JOjX/Oci8K94QHddo+bbfya/Ai/nf6/dt9ZfrFNWSrM= +go.opentelemetry.io/otel/trace v1.47.0/go.mod h1:jNaSLa2PZEYFG6fRjJABAu+bw4FS08uDmPg28lTghu0= +go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk= +go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/arch v0.31.0 h1:22MlEb14/O/EPCYHFxsDdv5TuLD5dMjT5e2QeJw4ULk= +golang.org/x/arch v0.31.0/go.mod h1:KcJSod3cqT2dKcjBxqTyGfbumNikqU9p5tHJinPJnuY= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= +golang.org/x/mod v0.42.0 h1:ICeL3t2L30ljSPHHxDnR4fqVcBt/zR2zJAb9juavmQg= +golang.org/x/mod v0.42.0/go.mod h1:u5fzIrRInkL8MaO1cbRkUHT64v303YIT5KEqpNr7jRs= +golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues= +golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= +golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= +golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= +golang.org/x/tools v0.52.0 h1:ryxg4U63oaUYk7uOLTF37n43f4qgtGvqV4Ioit1BaEs= +golang.org/x/tools v0.52.0/go.mod h1:ItcP5UNaVGG+Yc4eIYmaFLNJ4soEopkx5Y2d4lcEurs= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc h1:rZHRz0ogq4Pid1IKtA/ivHi8XONVF5tqUQXA2TtgpkE= +google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:K/n0XLazCJHjD73YuMyNJsN38BPPy1kfFnKxUXaThFU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc h1:4bNTbnb44EqGVy9HaQxSY2jnafSUdgV3qHtedvNpDKg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.30.3 h1:aDq8gIX1X7+VFgO/JYYji6pLWL2H4tv3g8WatT3KCys= +modernc.org/cc/v4 v4.30.3/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= +modernc.org/ccgo/v4 v4.37.0 h1:BAuJCUtIHIxFIYaFhqXIw5YaoMBlNCUlFovejHfMip4= +modernc.org/ccgo/v4 v4.37.0/go.mod h1:/8627VhwWtAbqUXmPORxFQU5SgLrAx2jl9seLw9GBhY= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.2.0 h1:/Y3LGDMQaoYf55PebP1qsZ5FDGgAZyG1RYKEHeQzh1o= +modernc.org/gc/v3 v3.2.0/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.79.0 h1:9Dvvv++bAMSkSuBzuOFAatbTV5OIoq6Y6FohmD3e7q8= +modernc.org/libc v1.79.0/go.mod h1:uT3kR61NsbeKJSLqq83GJwSnZMVLlSjyAz4DfTaBy1M= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.12.1 h1:nFMiWrpStgZczNl6XI9GnIk/rWhYIyHGUaR04pGbp9g= +modernc.org/memory v1.12.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= +modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.61.0 h1:sQi63oLQ9uquAUA4CzOisH35Q3pEszkPBuHM0tFL8CY= +modernc.org/sqlite v1.61.0/go.mod h1:f4gr5tbpx2umklaT/QVtbi/50hNdM+4ng+2zMo8KXJ0= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/backend/internal/api/api_test.go b/backend/internal/api/api_test.go new file mode 100644 index 0000000..9a6014b --- /dev/null +++ b/backend/internal/api/api_test.go @@ -0,0 +1,641 @@ +package api + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "path/filepath" + "strings" + "testing" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/rs/zerolog" + + "kereru.forms/internal/database" +) + +func newTestRouter(t *testing.T) (*gin.Engine, database.Store) { + t.Helper() + path := filepath.Join(t.TempDir(), "api-test.db") + store, err := database.Open("sqlite", "file:"+path+"?_pragma=foreign_keys(1)") + if err != nil { + t.Fatalf("open database: %v", err) + } + t.Cleanup(func() { _ = store.Close() }) + + server := NewServer(store, zerolog.Nop(), "test-version") + router := NewRouter(server, zerolog.Nop(), RouterOptions{CORSOrigins: []string{"http://localhost:5173"}}) + return router, store +} + +func doRequest(t *testing.T, router *gin.Engine, method, path string, body any) *httptest.ResponseRecorder { + t.Helper() + var reader io.Reader + if body != nil { + payload, err := json.Marshal(body) + if err != nil { + t.Fatalf("marshal request: %v", err) + } + reader = bytes.NewReader(payload) + } + req := httptest.NewRequest(method, path, reader) + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + req.Header.Set("User-Agent", "kereru-test-agent") + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + return w +} + +func decode[T any](t *testing.T, w *httptest.ResponseRecorder) T { + t.Helper() + var out T + if err := json.Unmarshal(w.Body.Bytes(), &out); err != nil { + t.Fatalf("decode response %q: %v", w.Body.String(), err) + } + return out +} + +func boolPtr(b bool) *bool { return &b } +func f32Ptr(f float32) *float32 { return &f } +func strPtr(s string) *string { return &s } + +func createSampleForm(t *testing.T, router *gin.Engine) Form { + t.Helper() + options := []QuestionOption{ + {Value: "red", Label: "Red"}, + {Value: "blue", Label: "Blue"}, + } + body := CreateFormRequest{ + Title: "Customer feedback", + Description: strPtr("A short survey"), + Introduction: strPtr("Thanks for helping us improve."), + RequiresAccessCode: boolPtr(true), + LogoUrl: strPtr("https://example.com/logo.png"), + Questions: []QuestionInput{ + {Key: "name", Label: "Your name", Type: Text, Required: true}, + {Key: "happy", Label: "Are you happy?", Type: Boolean, Required: true}, + {Key: "score", Label: "Score", Type: Number, MinValue: f32Ptr(1), MaxValue: f32Ptr(10)}, + {Key: "colour", Label: "Favourite colour", Type: Select, Options: &options}, + {Key: "when", Label: "Date", Type: Date}, + }, + } + + w := doRequest(t, router, http.MethodPost, "/forms", body) + if w.Code != http.StatusCreated { + t.Fatalf("create form: status %d body %s", w.Code, w.Body.String()) + } + return decode[Form](t, w) +} + +func TestHealth(t *testing.T) { + router, _ := newTestRouter(t) + for _, path := range []string{"/health", "/api/health"} { + w := doRequest(t, router, http.MethodGet, path, nil) + if w.Code != http.StatusOK { + t.Fatalf("health %s: status %d", path, w.Code) + } + health := decode[Health](t, w) + if health.Status != "ok" || health.Version != "test-version" { + t.Fatalf("unexpected health: %+v", health) + } + if health.UptimeSeconds == nil { + t.Fatal("uptimeSeconds should be present") + } + } +} + +func TestFormLifecycle(t *testing.T) { + router, _ := newTestRouter(t) + form := createSampleForm(t, router) + + if form.Version != 1 || !form.IsActive || len(form.Questions) != 5 { + t.Fatalf("unexpected form: %+v", form) + } + + w := doRequest(t, router, http.MethodGet, "/forms/"+form.Id.String(), nil) + if w.Code != http.StatusOK { + t.Fatalf("get form: %d %s", w.Code, w.Body.String()) + } + + w = doRequest(t, router, http.MethodGet, "/forms", nil) + if w.Code != http.StatusOK { + t.Fatalf("list forms: %d", w.Code) + } + forms := decode[[]FormSummary](t, w) + if len(forms) != 1 || forms[0].QuestionCount != 5 { + t.Fatalf("unexpected forms: %+v", forms) + } + if forms[0].LogoUrl == nil || *forms[0].LogoUrl != "https://example.com/logo.png" { + t.Fatalf("logo url missing from summary: %+v", forms[0]) + } + + w = doRequest(t, router, http.MethodPost, "/forms/"+form.Id.String()+"/access-codes", + CreateAccessCodesRequest{Count: 2, Prefix: strPtr("demo")}) + if w.Code != http.StatusCreated { + t.Fatalf("create codes: %d %s", w.Code, w.Body.String()) + } + codes := decode[[]AccessCode](t, w) + if len(codes) != 2 || !strings.HasPrefix(codes[0].Code, "DEMO-") { + t.Fatalf("unexpected codes: %+v", codes) + } + + w = doRequest(t, router, http.MethodPost, "/forms/"+form.Id.String()+"/inactivate", nil) + if w.Code != http.StatusOK { + t.Fatalf("inactivate: %d %s", w.Code, w.Body.String()) + } + if inactive := decode[Form](t, w); inactive.IsActive { + t.Fatal("form should be inactive") + } + + w = doRequest(t, router, http.MethodPost, "/forms/"+form.Id.String()+"/activate", nil) + if w.Code != http.StatusOK { + t.Fatalf("activate: %d", w.Code) + } + + w = doRequest(t, router, http.MethodPost, "/forms/"+form.Id.String()+"/versions", nil) + if w.Code != http.StatusCreated { + t.Fatalf("create version: %d %s", w.Code, w.Body.String()) + } + v2 := decode[Form](t, w) + if v2.Version != 2 || v2.FormGroupId != form.FormGroupId { + t.Fatalf("unexpected version: %+v", v2) + } + if v2.LogoUrl == nil || *v2.LogoUrl != "https://example.com/logo.png" { + t.Fatalf("new version should keep the logo: %+v", v2) + } + + w = doRequest(t, router, http.MethodGet, "/forms/"+form.Id.String(), nil) + old := decode[Form](t, w) + if old.IsActive { + t.Fatal("previous version should be inactive") + } +} + +func TestSubmitFlow(t *testing.T) { + router, _ := newTestRouter(t) + form := createSampleForm(t, router) + + byKey := map[string]Question{} + for _, q := range form.Questions { + byKey[q.Key] = q + } + answers := []Answer{ + {QuestionId: byKey["name"].Id, Value: "Ada"}, + {QuestionId: byKey["happy"].Id, Value: true}, + {QuestionId: byKey["score"].Id, Value: 8}, + {QuestionId: byKey["colour"].Id, Value: "blue"}, + {QuestionId: byKey["when"].Id, Value: "2026-10-23"}, + } + + w := doRequest(t, router, http.MethodPost, "/forms/"+form.Id.String()+"/access-codes", + CreateAccessCodesRequest{Count: 1}) + if w.Code != http.StatusCreated { + t.Fatalf("create code: %d", w.Code) + } + code := decode[[]AccessCode](t, w)[0].Code + + submit := SubmitFormRequest{ + UserId: "ada@example.com", + UserIdType: Email, + Answers: answers, + Tracking: &ClientTracking{Locale: strPtr("en-NZ")}, + } + + w = doRequest(t, router, http.MethodPost, "/forms/"+form.Id.String()+"/submissions", submit) + if w.Code != http.StatusBadRequest { + t.Fatalf("missing access code should be 400, got %d %s", w.Code, w.Body.String()) + } + + submit.AccessCode = &code + w = doRequest(t, router, http.MethodPost, "/forms/"+form.Id.String()+"/submissions", submit) + if w.Code != http.StatusCreated { + t.Fatalf("submit: %d %s", w.Code, w.Body.String()) + } + submission := decode[Submission](t, w) + if submission.FormVersion != 1 || len(submission.Answers) != 5 { + t.Fatalf("unexpected submission: %+v", submission) + } + var storedDate string + for _, answer := range submission.Answers { + if answer.QuestionKey == "when" { + storedDate, _ = answer.Value.(string) + } + } + if storedDate != "2026-10-23" { + t.Fatalf("date answer not stored correctly: %+v", submission.Answers) + } + if submission.Tracking == nil || submission.Tracking.UserAgent == nil || + *submission.Tracking.UserAgent != "kereru-test-agent" { + t.Fatalf("server-side tracking missing: %+v", submission.Tracking) + } + + w = doRequest(t, router, http.MethodPost, "/forms/"+form.Id.String()+"/submissions", submit) + if w.Code != http.StatusConflict { + t.Fatalf("reused code should be 409, got %d %s", w.Code, w.Body.String()) + } + + w = doRequest(t, router, http.MethodGet, "/forms/"+form.Id.String()+"/submissions", nil) + if w.Code != http.StatusOK { + t.Fatalf("list submissions: %d", w.Code) + } + subs := decode[[]SubmissionSummary](t, w) + if len(subs) != 1 || subs[0].Id != submission.Id { + t.Fatalf("unexpected submissions: %+v", subs) + } + + w = doRequest(t, router, http.MethodGet, "/submissions/"+submission.Id.String(), nil) + if w.Code != http.StatusOK { + t.Fatalf("get submission: %d", w.Code) + } + + w = doRequest(t, router, http.MethodPost, "/forms/"+form.Id.String()+"/inactivate", nil) + if w.Code != http.StatusOK { + t.Fatalf("inactivate: %d", w.Code) + } + second := SubmitFormRequest{ + UserId: "ada@example.com", + UserIdType: Email, + Answers: answers, + } + w = doRequest(t, router, http.MethodPost, "/forms/"+form.Id.String()+"/submissions", second) + if w.Code != http.StatusGone { + t.Fatalf("inactive form should be 410, got %d %s", w.Code, w.Body.String()) + } +} + +func TestSubmitValidation(t *testing.T) { + router, _ := newTestRouter(t) + form := createSampleForm(t, router) + byKey := map[string]Question{} + for _, q := range form.Questions { + byKey[q.Key] = q + } + + cases := []struct { + name string + answers []Answer + }{ + { + name: "missing required", + answers: []Answer{{QuestionId: byKey["score"].Id, Value: 5}}, + }, + { + name: "number out of range", + answers: []Answer{ + {QuestionId: byKey["name"].Id, Value: "Ada"}, + {QuestionId: byKey["happy"].Id, Value: true}, + {QuestionId: byKey["score"].Id, Value: 42}, + }, + }, + { + name: "bad select option", + answers: []Answer{ + {QuestionId: byKey["name"].Id, Value: "Ada"}, + {QuestionId: byKey["happy"].Id, Value: true}, + {QuestionId: byKey["colour"].Id, Value: "green"}, + }, + }, + { + name: "bad date", + answers: []Answer{ + {QuestionId: byKey["name"].Id, Value: "Ada"}, + {QuestionId: byKey["happy"].Id, Value: true}, + {QuestionId: byKey["when"].Id, Value: "23/10/2026"}, + }, + }, + { + name: "wrong type", + answers: []Answer{ + {QuestionId: byKey["name"].Id, Value: "Ada"}, + {QuestionId: byKey["happy"].Id, Value: "yes"}, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + w := doRequest(t, router, http.MethodPost, "/forms/"+form.Id.String()+"/submissions", + SubmitFormRequest{UserId: "ada@example.com", UserIdType: Email, Answers: tc.answers}) + if w.Code != http.StatusBadRequest { + t.Fatalf("expected 400, got %d %s", w.Code, w.Body.String()) + } + }) + } +} + +func TestCreateFormValidation(t *testing.T) { + router, _ := newTestRouter(t) + options := []QuestionOption{{Value: "a", Label: "A"}} + + cases := []CreateFormRequest{ + {Title: "", Questions: []QuestionInput{{Key: "a", Label: "A", Type: Text}}}, + {Title: "Test", Questions: []QuestionInput{{Key: "1bad", Label: "A", Type: Text}}}, + {Title: "Test", Questions: []QuestionInput{{Key: "dup", Label: "A", Type: Text}, {Key: "dup", Label: "B", Type: Text}}}, + {Title: "Test", Questions: []QuestionInput{{Key: "a", Label: "A", Type: "datetime"}}}, + {Title: "Test", Questions: []QuestionInput{{Key: "a", Label: "A", Type: Select}}}, + {Title: "Test", Questions: []QuestionInput{{Key: "a", Label: "A", Type: Number, MinValue: f32Ptr(10), MaxValue: f32Ptr(1)}}}, + {Title: "Test", Questions: []QuestionInput{{Key: "a", Label: "A", Type: Select, Options: &options}, {Key: "a", Label: "B", Type: Text}}}, + {Title: "Test", Questions: []QuestionInput{{Key: "a", Label: "A", Type: Text}}, LogoUrl: strPtr("javascript:alert(1)")}, + {Title: "Test", Questions: []QuestionInput{{Key: "a", Label: "A", Type: Text}}, LogoUrl: strPtr("not a url")}, + } + + for i, body := range cases { + w := doRequest(t, router, http.MethodPost, "/forms", body) + if w.Code != http.StatusBadRequest { + t.Fatalf("case %d: expected 400, got %d %s", i, w.Code, w.Body.String()) + } + } +} + +func TestPDFAndCORS(t *testing.T) { + router, _ := newTestRouter(t) + form := createSampleForm(t, router) + + w := doRequest(t, router, http.MethodGet, "/forms/"+form.Id.String()+"/pdf", nil) + if w.Code != http.StatusOK { + t.Fatalf("pdf: %d %s", w.Code, w.Body.String()) + } + if ct := w.Header().Get("Content-Type"); ct != "application/pdf" { + t.Fatalf("content type = %q", ct) + } + if !bytes.HasPrefix(w.Body.Bytes(), []byte("%PDF")) { + t.Fatal("response is not a PDF") + } + + req := httptest.NewRequest(http.MethodOptions, "/forms", nil) + req.Header.Set("Origin", "http://localhost:5173") + req.Header.Set("Access-Control-Request-Method", "POST") + corsW := httptest.NewRecorder() + router.ServeHTTP(corsW, req) + if corsW.Code != http.StatusNoContent { + t.Fatalf("preflight status = %d", corsW.Code) + } + if got := corsW.Header().Get("Access-Control-Allow-Origin"); got != "http://localhost:5173" { + t.Fatalf("allow origin = %q", got) + } +} + +func TestNotFound(t *testing.T) { + router, _ := newTestRouter(t) + missing := uuid.NewString() + + for _, tc := range []struct { + method string + path string + }{ + {http.MethodGet, "/forms/" + missing}, + {http.MethodDelete, "/forms/" + missing}, + {http.MethodPost, "/forms/" + missing + "/inactivate"}, + {http.MethodGet, "/forms/" + missing + "/submissions"}, + {http.MethodGet, "/submissions/" + missing}, + {http.MethodGet, "/submissions/" + missing + "/offline-pdf"}, + } { + w := doRequest(t, router, tc.method, tc.path, nil) + if w.Code != http.StatusNotFound { + t.Fatalf("%s %s: expected 404, got %d", tc.method, tc.path, w.Code) + } + } +} + +func createSignatureForm(t *testing.T, router *gin.Engine) Form { + t.Helper() + body := CreateFormRequest{ + Title: "Signed declaration", + SignatureRequired: boolPtr(true), + Questions: []QuestionInput{ + {Key: "name", Label: "Your name", Type: Text, Required: true}, + }, + } + w := doRequest(t, router, http.MethodPost, "/forms", body) + if w.Code != http.StatusCreated { + t.Fatalf("create signature form: %d %s", w.Code, w.Body.String()) + } + return decode[Form](t, w) +} + +func pngDataURL() string { + data := []byte{0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a} + return "data:image/png;base64," + base64.StdEncoding.EncodeToString(data) +} + +func pdfDataURL(data []byte) string { + return "data:application/pdf;base64," + base64.StdEncoding.EncodeToString(data) +} + +func TestUpdateFormCreatesVersion(t *testing.T) { + router, _ := newTestRouter(t) + form := createSampleForm(t, router) + + update := UpdateFormRequest{ + Title: "Customer feedback v2", + PdfUrl: strPtr("https://example.com/prebuilt.pdf"), + ExternalReferenceRequired: boolPtr(true), + Questions: []QuestionInput{ + {Key: "name", Label: "Your name", Type: Text, Required: true}, + }, + } + w := doRequest(t, router, http.MethodPut, "/forms/"+form.Id.String(), update) + if w.Code != http.StatusOK { + t.Fatalf("update form: %d %s", w.Code, w.Body.String()) + } + updated := decode[Form](t, w) + if updated.Id == form.Id { + t.Fatal("editing should create a new form id") + } + if updated.Version != 2 || updated.FormGroupId != form.FormGroupId { + t.Fatalf("unexpected versioned form: %+v", updated) + } + if updated.PdfUrl == nil || *updated.PdfUrl != "https://example.com/prebuilt.pdf" { + t.Fatalf("pdf url not saved: %+v", updated.PdfUrl) + } + if updated.SignatureRequired == nil || *updated.SignatureRequired { + t.Fatalf("signature flag should preserve false: %+v", updated.SignatureRequired) + } + if updated.ExternalReferenceRequired == nil || !*updated.ExternalReferenceRequired || + updated.ExternalReferenceLabel == nil || *updated.ExternalReferenceLabel != "External reference" { + t.Fatalf("external reference was not enabled with a default label: %+v", updated) + } + if len(updated.Questions) != 1 || updated.Questions[0].Id.String() == "" { + t.Fatalf("expected a fresh question: %+v", updated.Questions) + } + + w = doRequest(t, router, http.MethodGet, "/forms/"+form.Id.String(), nil) + if w.Code != http.StatusOK { + t.Fatalf("get old form: %d", w.Code) + } + old := decode[Form](t, w) + if old.IsActive { + t.Fatal("previous version should be inactive") + } + + w = doRequest(t, router, http.MethodGet, "/forms?includeInactive=true", nil) + if w.Code != http.StatusOK { + t.Fatalf("list forms: %d", w.Code) + } + forms := decode[[]FormSummary](t, w) + if len(forms) != 2 { + t.Fatalf("expected two versions, got %+v", forms) + } +} + +func TestExternalReference(t *testing.T) { + router, _ := newTestRouter(t) + body := CreateFormRequest{ + Title: "Invoice approval", + ExternalReferenceRequired: boolPtr(true), + ExternalReferenceLabel: strPtr("Invoice number"), + Questions: []QuestionInput{ + {Key: "note", Label: "Note", Type: Text, Required: true}, + }, + } + w := doRequest(t, router, http.MethodPost, "/forms", body) + if w.Code != http.StatusCreated { + t.Fatalf("create form: %d %s", w.Code, w.Body.String()) + } + form := decode[Form](t, w) + if form.ExternalReferenceRequired == nil || !*form.ExternalReferenceRequired || + form.ExternalReferenceLabel == nil || *form.ExternalReferenceLabel != "Invoice number" { + t.Fatalf("unexpected external reference config: %+v", form) + } + + submit := SubmitFormRequest{ + UserId: "ada@example.com", + UserIdType: Email, + Answers: []Answer{{ + QuestionId: form.Questions[0].Id, + Value: "Approved", + }}, + } + w = doRequest(t, router, http.MethodPost, "/forms/"+form.Id.String()+"/submissions", submit) + if w.Code != http.StatusBadRequest { + t.Fatalf("missing external reference should be 400, got %d %s", w.Code, w.Body.String()) + } + + submit.ExternalReference = strPtr("INV-42") + w = doRequest(t, router, http.MethodPost, "/forms/"+form.Id.String()+"/submissions", submit) + if w.Code != http.StatusCreated { + t.Fatalf("submit with reference: %d %s", w.Code, w.Body.String()) + } + submission := decode[Submission](t, w) + if submission.ExternalReference == nil || *submission.ExternalReference != "INV-42" { + t.Fatalf("external reference not returned: %+v", submission.ExternalReference) + } + + w = doRequest(t, router, http.MethodGet, "/submissions/"+submission.Id.String(), nil) + if w.Code != http.StatusOK { + t.Fatalf("get submission: %d %s", w.Code, w.Body.String()) + } + stored := decode[Submission](t, w) + if stored.ExternalReference == nil || *stored.ExternalReference != "INV-42" { + t.Fatalf("external reference not persisted: %+v", stored.ExternalReference) + } + + w = doRequest(t, router, http.MethodGet, "/forms/"+form.Id.String()+"/submissions", nil) + if w.Code != http.StatusOK { + t.Fatalf("list submissions: %d %s", w.Code, w.Body.String()) + } + summaries := decode[[]SubmissionSummary](t, w) + if len(summaries) != 1 || summaries[0].ExternalReference == nil || + *summaries[0].ExternalReference != "INV-42" { + t.Fatalf("reference missing from summary: %+v", summaries) + } +} + +func TestSignatureSubmission(t *testing.T) { + router, _ := newTestRouter(t) + form := createSignatureForm(t, router) + questionID := form.Questions[0].Id + + base := SubmitFormRequest{ + UserId: "ada@example.com", + UserIdType: Email, + Answers: []Answer{{QuestionId: questionID, Value: "Ada"}}, + } + + w := doRequest(t, router, http.MethodPost, "/forms/"+form.Id.String()+"/submissions", base) + if w.Code != http.StatusBadRequest { + t.Fatalf("missing signature should be 400, got %d %s", w.Code, w.Body.String()) + } + + base.Signature = &SignatureInput{ + Type: Drawn, + DataUrl: pngDataURL(), + Name: "Ada Lovelace", + Title: strPtr("Analyst"), + } + w = doRequest(t, router, http.MethodPost, "/forms/"+form.Id.String()+"/submissions", base) + if w.Code != http.StatusCreated { + t.Fatalf("signed submission: %d %s", w.Code, w.Body.String()) + } + submission := decode[Submission](t, w) + if submission.Signature == nil || submission.Signature.Name != "Ada Lovelace" || + submission.Signature.Title == nil || *submission.Signature.Title != "Analyst" || + submission.Signature.SignedAt.IsZero() { + t.Fatalf("signature not returned: %+v", submission.Signature) + } + + w = doRequest(t, router, http.MethodGet, "/submissions/"+submission.Id.String(), nil) + if w.Code != http.StatusOK { + t.Fatalf("get submission: %d %s", w.Code, w.Body.String()) + } + stored := decode[Submission](t, w) + if stored.Signature == nil || stored.Signature.DataUrl != pngDataURL() { + t.Fatalf("signature not persisted: %+v", stored.Signature) + } + + base.Signature = &SignatureInput{Type: Drawn, DataUrl: pngDataURL(), Name: ""} + w = doRequest(t, router, http.MethodPost, "/forms/"+form.Id.String()+"/submissions", base) + if w.Code != http.StatusBadRequest { + t.Fatalf("empty signature name should be 400, got %d %s", w.Code, w.Body.String()) + } +} + +func TestOfflinePDFSubmission(t *testing.T) { + router, _ := newTestRouter(t) + form := createSignatureForm(t, router) + pdf := []byte("%PDF-1.4\n1 0 obj\n<<>>\nendobj\ntrailer\n<<>>\n%%EOF\n") + + submit := SubmitFormRequest{ + UserId: "ada@example.com", + UserIdType: Email, + OfflinePdf: strPtr(pdfDataURL(pdf)), + OfflinePdfName: strPtr("signed-declaration.pdf"), + } + + invalid := submit + invalid.OfflinePdf = strPtr(pdfDataURL([]byte("not a pdf"))) + w := doRequest(t, router, http.MethodPost, "/forms/"+form.Id.String()+"/submissions", invalid) + if w.Code != http.StatusBadRequest { + t.Fatalf("invalid pdf should be 400, got %d %s", w.Code, w.Body.String()) + } + + w = doRequest(t, router, http.MethodPost, "/forms/"+form.Id.String()+"/submissions", submit) + if w.Code != http.StatusCreated { + t.Fatalf("offline submission: %d %s", w.Code, w.Body.String()) + } + submission := decode[Submission](t, w) + if len(submission.Answers) != 0 || submission.OfflinePdfName == nil || + *submission.OfflinePdfName != "signed-declaration.pdf" || submission.Signature != nil { + t.Fatalf("unexpected offline submission: %+v", submission) + } + + w = doRequest(t, router, http.MethodGet, "/submissions/"+submission.Id.String()+"/offline-pdf", nil) + if w.Code != http.StatusOK { + t.Fatalf("download offline pdf: %d %s", w.Code, w.Body.String()) + } + if got := w.Header().Get("Content-Type"); got != "application/pdf" { + t.Fatalf("content type = %q", got) + } + if got := w.Header().Get("Content-Disposition"); !strings.Contains(got, "signed-declaration.pdf") { + t.Fatalf("content disposition = %q", got) + } + if !bytes.Equal(w.Body.Bytes(), pdf) { + t.Fatalf("downloaded pdf differs from upload") + } +} diff --git a/backend/internal/api/generated.go b/backend/internal/api/generated.go new file mode 100644 index 0000000..cc8f5f0 --- /dev/null +++ b/backend/internal/api/generated.go @@ -0,0 +1,2123 @@ +// Package api provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.4.1 DO NOT EDIT. +package api + +import ( + "bytes" + "compress/gzip" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "path" + "strings" + "time" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/gin-gonic/gin" + "github.com/oapi-codegen/runtime" + strictgin "github.com/oapi-codegen/runtime/strictmiddleware/gin" + openapi_types "github.com/oapi-codegen/runtime/types" +) + +// Defines values for QuestionType. +const ( + Boolean QuestionType = "boolean" + Date QuestionType = "date" + Number QuestionType = "number" + Select QuestionType = "select" + Text QuestionType = "text" +) + +// Defines values for SignatureType. +const ( + Drawn SignatureType = "drawn" + Image SignatureType = "image" +) + +// Defines values for UserIdType. +const ( + Email UserIdType = "email" + Guid UserIdType = "guid" + Phone UserIdType = "phone" + Username UserIdType = "username" +) + +// AccessCode defines model for AccessCode. +type AccessCode struct { + Code string `json:"code"` + CreatedAt time.Time `json:"createdAt"` + FormId openapi_types.UUID `json:"formId"` + Id openapi_types.UUID `json:"id"` + Used bool `json:"used"` + UsedAt *time.Time `json:"usedAt,omitempty"` +} + +// Answer defines model for Answer. +type Answer struct { + QuestionId openapi_types.UUID `json:"questionId"` + + // Value Answer value. String, number, boolean or list of strings depending on the question type. + Value interface{} `json:"value"` +} + +// AnswerRecord defines model for AnswerRecord. +type AnswerRecord struct { + QuestionId openapi_types.UUID `json:"questionId"` + QuestionKey string `json:"questionKey"` + + // Value The captured value + Value interface{} `json:"value"` +} + +// ClientTracking Client supplied tracking details. IP and user agent are captured server-side. +type ClientTracking struct { + Locale *string `json:"locale,omitempty"` + Place *string `json:"place,omitempty"` + Referer *string `json:"referer,omitempty"` + Timezone *string `json:"timezone,omitempty"` +} + +// CreateAccessCodesRequest defines model for CreateAccessCodesRequest. +type CreateAccessCodesRequest struct { + Count int `json:"count"` + Prefix *string `json:"prefix,omitempty"` +} + +// CreateFormRequest defines model for CreateFormRequest. +type CreateFormRequest struct { + CompanyName *string `json:"companyName,omitempty"` + ContactEmail *string `json:"contactEmail,omitempty"` + ContactPhone *string `json:"contactPhone,omitempty"` + ContactUrl *string `json:"contactUrl,omitempty"` + Description *string `json:"description,omitempty"` + ExternalReferenceLabel *string `json:"externalReferenceLabel,omitempty"` + ExternalReferenceRequired *bool `json:"externalReferenceRequired,omitempty"` + Introduction *string `json:"introduction,omitempty"` + LogoUrl *string `json:"logoUrl,omitempty"` + PdfUrl *string `json:"pdfUrl,omitempty"` + Questions []QuestionInput `json:"questions"` + RequiresAccessCode *bool `json:"requiresAccessCode,omitempty"` + SignatureRequired *bool `json:"signatureRequired,omitempty"` + Title string `json:"title"` +} + +// Error defines model for Error. +type Error struct { + Code *string `json:"code,omitempty"` + Error string `json:"error"` +} + +// Form defines model for Form. +type Form struct { + CompanyName *string `json:"companyName,omitempty"` + ContactEmail *string `json:"contactEmail,omitempty"` + ContactPhone *string `json:"contactPhone,omitempty"` + ContactUrl *string `json:"contactUrl,omitempty"` + CreatedAt time.Time `json:"createdAt"` + Description *string `json:"description,omitempty"` + + // ExternalReferenceLabel Label shown for the external reference, for example Invoice number + ExternalReferenceLabel *string `json:"externalReferenceLabel,omitempty"` + + // ExternalReferenceRequired Whether a reference identifier is required before the form can be submitted + ExternalReferenceRequired *bool `json:"externalReferenceRequired,omitempty"` + FormGroupId openapi_types.UUID `json:"formGroupId"` + Id openapi_types.UUID `json:"id"` + + // Introduction Introduction section explaining purpose, background and support details + Introduction *string `json:"introduction,omitempty"` + IsActive bool `json:"isActive"` + LogoUrl *string `json:"logoUrl,omitempty"` + + // PdfUrl Optional absolute URL of a prebuilt PDF form linked to this definition + PdfUrl *string `json:"pdfUrl,omitempty"` + Questions []Question `json:"questions"` + RequiresAccessCode bool `json:"requiresAccessCode"` + + // SignatureRequired Whether a digital signature is captured at the end of the form + SignatureRequired *bool `json:"signatureRequired,omitempty"` + Title string `json:"title"` + UpdatedAt time.Time `json:"updatedAt"` + Version int `json:"version"` +} + +// FormSummary defines model for FormSummary. +type FormSummary struct { + CreatedAt time.Time `json:"createdAt"` + Description *string `json:"description,omitempty"` + + // ExternalReferenceLabel Label shown for the external reference, for example Invoice number + ExternalReferenceLabel *string `json:"externalReferenceLabel,omitempty"` + + // ExternalReferenceRequired Whether a reference identifier is required before the form can be submitted + ExternalReferenceRequired *bool `json:"externalReferenceRequired,omitempty"` + + // FormGroupId Groups all versions of the same logical form + FormGroupId openapi_types.UUID `json:"formGroupId"` + Id openapi_types.UUID `json:"id"` + IsActive bool `json:"isActive"` + LogoUrl *string `json:"logoUrl,omitempty"` + + // PdfUrl Optional absolute URL of a prebuilt PDF form linked to this definition + PdfUrl *string `json:"pdfUrl,omitempty"` + QuestionCount int `json:"questionCount"` + Title string `json:"title"` + UpdatedAt time.Time `json:"updatedAt"` + Version int `json:"version"` +} + +// Health defines model for Health. +type Health struct { + Status string `json:"status"` + UptimeSeconds *int64 `json:"uptimeSeconds,omitempty"` + Version string `json:"version"` +} + +// Question defines model for Question. +type Question struct { + Description *string `json:"description,omitempty"` + Id openapi_types.UUID `json:"id"` + + // Key Stable machine key for this question within the form + Key string `json:"key"` + + // Label The question text (may be multiline, no formatting directives) + Label string `json:"label"` + + // MaxValue Maximum allowed value for `number` questions + MaxValue *float32 `json:"maxValue,omitempty"` + + // MinValue Minimum allowed value for `number` questions + MinValue *float32 `json:"minValue,omitempty"` + + // Multiline Whether a `text` answer may span multiple lines + Multiline *bool `json:"multiline,omitempty"` + + // Options Present for `select` questions + Options *[]QuestionOption `json:"options,omitempty"` + Placeholder *string `json:"placeholder,omitempty"` + Position int `json:"position"` + Required bool `json:"required"` + + // Type * `boolean` – true/false + // * `number` – numeric answer + // * `select` – one option from a configured list + // * `text` – free-form text (single or multiline) + // * `date` - calendar date in `YYYY-MM-DD` form + Type QuestionType `json:"type"` +} + +// QuestionInput defines model for QuestionInput. +type QuestionInput struct { + Description *string `json:"description,omitempty"` + Key string `json:"key"` + Label string `json:"label"` + MaxValue *float32 `json:"maxValue,omitempty"` + MinValue *float32 `json:"minValue,omitempty"` + Multiline *bool `json:"multiline,omitempty"` + Options *[]QuestionOption `json:"options,omitempty"` + Placeholder *string `json:"placeholder,omitempty"` + Required bool `json:"required"` + + // Type * `boolean` – true/false + // * `number` – numeric answer + // * `select` – one option from a configured list + // * `text` – free-form text (single or multiline) + // * `date` - calendar date in `YYYY-MM-DD` form + Type QuestionType `json:"type"` +} + +// QuestionOption defines model for QuestionOption. +type QuestionOption struct { + Label string `json:"label"` + Value string `json:"value"` +} + +// QuestionType * `boolean` – true/false +// * `number` – numeric answer +// * `select` – one option from a configured list +// * `text` – free-form text (single or multiline) +// * `date` - calendar date in `YYYY-MM-DD` form +type QuestionType string + +// Signature defines model for Signature. +type Signature struct { + DataUrl string `json:"dataUrl"` + Name string `json:"name"` + SignedAt time.Time `json:"signedAt"` + Title *string `json:"title,omitempty"` + + // Type * `drawn` - signature drawn in the browser + // * `image` - uploaded image containing a signature + Type SignatureType `json:"type"` +} + +// SignatureInput defines model for SignatureInput. +type SignatureInput struct { + // DataUrl PNG or JPEG image data URL (maximum decoded size 2 MB) + DataUrl string `json:"dataUrl"` + + // Name Name of the person signing + Name string `json:"name"` + + // Title Optional title of the person signing + Title *string `json:"title,omitempty"` + + // Type * `drawn` - signature drawn in the browser + // * `image` - uploaded image containing a signature + Type SignatureType `json:"type"` +} + +// SignatureType * `drawn` - signature drawn in the browser +// * `image` - uploaded image containing a signature +type SignatureType string + +// Submission defines model for Submission. +type Submission struct { + Answers []AnswerRecord `json:"answers"` + ExternalReference *string `json:"externalReference,omitempty"` + FormId openapi_types.UUID `json:"formId"` + FormVersion int `json:"formVersion"` + Id openapi_types.UUID `json:"id"` + + // OfflinePdfName Present when the respondent uploaded a completed offline PDF + OfflinePdfName *string `json:"offlinePdfName,omitempty"` + Signature *Signature `json:"signature,omitempty"` + SubmittedAt time.Time `json:"submittedAt"` + Tracking *Tracking `json:"tracking,omitempty"` + UserId string `json:"userId"` + UserIdType UserIdType `json:"userIdType"` +} + +// SubmissionSummary defines model for SubmissionSummary. +type SubmissionSummary struct { + ExternalReference *string `json:"externalReference,omitempty"` + FormId openapi_types.UUID `json:"formId"` + FormVersion int `json:"formVersion"` + Id openapi_types.UUID `json:"id"` + SubmittedAt time.Time `json:"submittedAt"` + UserId string `json:"userId"` + UserIdType UserIdType `json:"userIdType"` +} + +// SubmitFormRequest defines model for SubmitFormRequest. +type SubmitFormRequest struct { + // AccessCode Required when the form has `requiresAccessCode = true` + AccessCode *string `json:"accessCode,omitempty"` + Answers []Answer `json:"answers"` + + // ExternalReference External reference identifier used to link this response to a record in + // another system, for example an invoice, account or policy number. + ExternalReference *string `json:"externalReference,omitempty"` + + // OfflinePdf Base64 data URL of a PDF completed offline (for example + // `data:application/pdf;base64,...`). Maximum decoded size is 10 MB. + OfflinePdf *string `json:"offlinePdf,omitempty"` + + // OfflinePdfName Original filename of the uploaded offline PDF + OfflinePdfName *string `json:"offlinePdfName,omitempty"` + Signature *SignatureInput `json:"signature,omitempty"` + + // Tracking Client supplied tracking details. IP and user agent are captured server-side. + Tracking *ClientTracking `json:"tracking,omitempty"` + + // UserId Respondent identifier captured at the start (email, username, phone or GUID) + UserId string `json:"userId"` + UserIdType UserIdType `json:"userIdType"` +} + +// Tracking defines model for Tracking. +type Tracking struct { + IpAddress *string `json:"ipAddress,omitempty"` + Locale *string `json:"locale,omitempty"` + Place *string `json:"place,omitempty"` + Referer *string `json:"referer,omitempty"` + Timezone *string `json:"timezone,omitempty"` + UserAgent *string `json:"userAgent,omitempty"` +} + +// UpdateFormRequest defines model for UpdateFormRequest. +type UpdateFormRequest struct { + CompanyName *string `json:"companyName,omitempty"` + ContactEmail *string `json:"contactEmail,omitempty"` + ContactPhone *string `json:"contactPhone,omitempty"` + ContactUrl *string `json:"contactUrl,omitempty"` + Description *string `json:"description,omitempty"` + ExternalReferenceLabel *string `json:"externalReferenceLabel,omitempty"` + ExternalReferenceRequired *bool `json:"externalReferenceRequired,omitempty"` + Introduction *string `json:"introduction,omitempty"` + LogoUrl *string `json:"logoUrl,omitempty"` + PdfUrl *string `json:"pdfUrl,omitempty"` + Questions []QuestionInput `json:"questions"` + RequiresAccessCode *bool `json:"requiresAccessCode,omitempty"` + SignatureRequired *bool `json:"signatureRequired,omitempty"` + Title string `json:"title"` +} + +// UserIdType defines model for UserIdType. +type UserIdType string + +// FormId defines model for FormId. +type FormId = openapi_types.UUID + +// ListFormsParams defines parameters for ListForms. +type ListFormsParams struct { + IncludeInactive *bool `form:"includeInactive,omitempty" json:"includeInactive,omitempty"` + ActiveOnly *bool `form:"activeOnly,omitempty" json:"activeOnly,omitempty"` +} + +// CreateFormJSONRequestBody defines body for CreateForm for application/json ContentType. +type CreateFormJSONRequestBody = CreateFormRequest + +// UpdateFormJSONRequestBody defines body for UpdateForm for application/json ContentType. +type UpdateFormJSONRequestBody = UpdateFormRequest + +// CreateAccessCodesJSONRequestBody defines body for CreateAccessCodes for application/json ContentType. +type CreateAccessCodesJSONRequestBody = CreateAccessCodesRequest + +// SubmitFormJSONRequestBody defines body for SubmitForm for application/json ContentType. +type SubmitFormJSONRequestBody = SubmitFormRequest + +// ServerInterface represents all server handlers. +type ServerInterface interface { + // List forms + // (GET /forms) + ListForms(c *gin.Context, params ListFormsParams) + // Create a form + // (POST /forms) + CreateForm(c *gin.Context) + // Delete a form definition + // (DELETE /forms/{formId}) + DeleteForm(c *gin.Context, formId FormId) + // Get a form by id + // (GET /forms/{formId}) + GetForm(c *gin.Context, formId FormId) + // Update a form definition + // (PUT /forms/{formId}) + UpdateForm(c *gin.Context, formId FormId) + // List access codes for a form + // (GET /forms/{formId}/access-codes) + ListAccessCodes(c *gin.Context, formId FormId) + // Create one or more access codes for a form + // (POST /forms/{formId}/access-codes) + CreateAccessCodes(c *gin.Context, formId FormId) + // Activate a form + // (POST /forms/{formId}/activate) + ActivateForm(c *gin.Context, formId FormId) + // Inactivate a form + // (POST /forms/{formId}/inactivate) + InactivateForm(c *gin.Context, formId FormId) + // Download a PDF rendering of the form definition + // (GET /forms/{formId}/pdf) + GetFormPdf(c *gin.Context, formId FormId) + // List submissions for a form (management) + // (GET /forms/{formId}/submissions) + ListFormSubmissions(c *gin.Context, formId FormId) + // Submit an answer set for a form + // (POST /forms/{formId}/submissions) + SubmitForm(c *gin.Context, formId FormId) + // Create a new version of a form + // (POST /forms/{formId}/versions) + CreateFormVersion(c *gin.Context, formId FormId) + // Service health + // (GET /health) + GetHealth(c *gin.Context) + // Get a submission by id (management) + // (GET /submissions/{submissionId}) + GetSubmission(c *gin.Context, submissionId openapi_types.UUID) + // Download the offline PDF uploaded with a submission + // (GET /submissions/{submissionId}/offline-pdf) + GetSubmissionOfflinePdf(c *gin.Context, submissionId openapi_types.UUID) +} + +// ServerInterfaceWrapper converts contexts to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface + HandlerMiddlewares []MiddlewareFunc + ErrorHandler func(*gin.Context, error, int) +} + +type MiddlewareFunc func(c *gin.Context) + +// ListForms operation middleware +func (siw *ServerInterfaceWrapper) ListForms(c *gin.Context) { + + var err error + + // Parameter object where we will unmarshal all parameters from the context + var params ListFormsParams + + // ------------- Optional query parameter "includeInactive" ------------- + + err = runtime.BindQueryParameter("form", true, false, "includeInactive", c.Request.URL.Query(), ¶ms.IncludeInactive) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter includeInactive: %w", err), http.StatusBadRequest) + return + } + + // ------------- Optional query parameter "activeOnly" ------------- + + err = runtime.BindQueryParameter("form", true, false, "activeOnly", c.Request.URL.Query(), ¶ms.ActiveOnly) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter activeOnly: %w", err), http.StatusBadRequest) + return + } + + for _, middleware := range siw.HandlerMiddlewares { + middleware(c) + if c.IsAborted() { + return + } + } + + siw.Handler.ListForms(c, params) +} + +// CreateForm operation middleware +func (siw *ServerInterfaceWrapper) CreateForm(c *gin.Context) { + + for _, middleware := range siw.HandlerMiddlewares { + middleware(c) + if c.IsAborted() { + return + } + } + + siw.Handler.CreateForm(c) +} + +// DeleteForm operation middleware +func (siw *ServerInterfaceWrapper) DeleteForm(c *gin.Context) { + + var err error + + // ------------- Path parameter "formId" ------------- + var formId FormId + + err = runtime.BindStyledParameterWithOptions("simple", "formId", c.Param("formId"), &formId, runtime.BindStyledParameterOptions{Explode: false, Required: true}) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter formId: %w", err), http.StatusBadRequest) + return + } + + for _, middleware := range siw.HandlerMiddlewares { + middleware(c) + if c.IsAborted() { + return + } + } + + siw.Handler.DeleteForm(c, formId) +} + +// GetForm operation middleware +func (siw *ServerInterfaceWrapper) GetForm(c *gin.Context) { + + var err error + + // ------------- Path parameter "formId" ------------- + var formId FormId + + err = runtime.BindStyledParameterWithOptions("simple", "formId", c.Param("formId"), &formId, runtime.BindStyledParameterOptions{Explode: false, Required: true}) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter formId: %w", err), http.StatusBadRequest) + return + } + + for _, middleware := range siw.HandlerMiddlewares { + middleware(c) + if c.IsAborted() { + return + } + } + + siw.Handler.GetForm(c, formId) +} + +// UpdateForm operation middleware +func (siw *ServerInterfaceWrapper) UpdateForm(c *gin.Context) { + + var err error + + // ------------- Path parameter "formId" ------------- + var formId FormId + + err = runtime.BindStyledParameterWithOptions("simple", "formId", c.Param("formId"), &formId, runtime.BindStyledParameterOptions{Explode: false, Required: true}) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter formId: %w", err), http.StatusBadRequest) + return + } + + for _, middleware := range siw.HandlerMiddlewares { + middleware(c) + if c.IsAborted() { + return + } + } + + siw.Handler.UpdateForm(c, formId) +} + +// ListAccessCodes operation middleware +func (siw *ServerInterfaceWrapper) ListAccessCodes(c *gin.Context) { + + var err error + + // ------------- Path parameter "formId" ------------- + var formId FormId + + err = runtime.BindStyledParameterWithOptions("simple", "formId", c.Param("formId"), &formId, runtime.BindStyledParameterOptions{Explode: false, Required: true}) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter formId: %w", err), http.StatusBadRequest) + return + } + + for _, middleware := range siw.HandlerMiddlewares { + middleware(c) + if c.IsAborted() { + return + } + } + + siw.Handler.ListAccessCodes(c, formId) +} + +// CreateAccessCodes operation middleware +func (siw *ServerInterfaceWrapper) CreateAccessCodes(c *gin.Context) { + + var err error + + // ------------- Path parameter "formId" ------------- + var formId FormId + + err = runtime.BindStyledParameterWithOptions("simple", "formId", c.Param("formId"), &formId, runtime.BindStyledParameterOptions{Explode: false, Required: true}) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter formId: %w", err), http.StatusBadRequest) + return + } + + for _, middleware := range siw.HandlerMiddlewares { + middleware(c) + if c.IsAborted() { + return + } + } + + siw.Handler.CreateAccessCodes(c, formId) +} + +// ActivateForm operation middleware +func (siw *ServerInterfaceWrapper) ActivateForm(c *gin.Context) { + + var err error + + // ------------- Path parameter "formId" ------------- + var formId FormId + + err = runtime.BindStyledParameterWithOptions("simple", "formId", c.Param("formId"), &formId, runtime.BindStyledParameterOptions{Explode: false, Required: true}) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter formId: %w", err), http.StatusBadRequest) + return + } + + for _, middleware := range siw.HandlerMiddlewares { + middleware(c) + if c.IsAborted() { + return + } + } + + siw.Handler.ActivateForm(c, formId) +} + +// InactivateForm operation middleware +func (siw *ServerInterfaceWrapper) InactivateForm(c *gin.Context) { + + var err error + + // ------------- Path parameter "formId" ------------- + var formId FormId + + err = runtime.BindStyledParameterWithOptions("simple", "formId", c.Param("formId"), &formId, runtime.BindStyledParameterOptions{Explode: false, Required: true}) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter formId: %w", err), http.StatusBadRequest) + return + } + + for _, middleware := range siw.HandlerMiddlewares { + middleware(c) + if c.IsAborted() { + return + } + } + + siw.Handler.InactivateForm(c, formId) +} + +// GetFormPdf operation middleware +func (siw *ServerInterfaceWrapper) GetFormPdf(c *gin.Context) { + + var err error + + // ------------- Path parameter "formId" ------------- + var formId FormId + + err = runtime.BindStyledParameterWithOptions("simple", "formId", c.Param("formId"), &formId, runtime.BindStyledParameterOptions{Explode: false, Required: true}) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter formId: %w", err), http.StatusBadRequest) + return + } + + for _, middleware := range siw.HandlerMiddlewares { + middleware(c) + if c.IsAborted() { + return + } + } + + siw.Handler.GetFormPdf(c, formId) +} + +// ListFormSubmissions operation middleware +func (siw *ServerInterfaceWrapper) ListFormSubmissions(c *gin.Context) { + + var err error + + // ------------- Path parameter "formId" ------------- + var formId FormId + + err = runtime.BindStyledParameterWithOptions("simple", "formId", c.Param("formId"), &formId, runtime.BindStyledParameterOptions{Explode: false, Required: true}) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter formId: %w", err), http.StatusBadRequest) + return + } + + for _, middleware := range siw.HandlerMiddlewares { + middleware(c) + if c.IsAborted() { + return + } + } + + siw.Handler.ListFormSubmissions(c, formId) +} + +// SubmitForm operation middleware +func (siw *ServerInterfaceWrapper) SubmitForm(c *gin.Context) { + + var err error + + // ------------- Path parameter "formId" ------------- + var formId FormId + + err = runtime.BindStyledParameterWithOptions("simple", "formId", c.Param("formId"), &formId, runtime.BindStyledParameterOptions{Explode: false, Required: true}) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter formId: %w", err), http.StatusBadRequest) + return + } + + for _, middleware := range siw.HandlerMiddlewares { + middleware(c) + if c.IsAborted() { + return + } + } + + siw.Handler.SubmitForm(c, formId) +} + +// CreateFormVersion operation middleware +func (siw *ServerInterfaceWrapper) CreateFormVersion(c *gin.Context) { + + var err error + + // ------------- Path parameter "formId" ------------- + var formId FormId + + err = runtime.BindStyledParameterWithOptions("simple", "formId", c.Param("formId"), &formId, runtime.BindStyledParameterOptions{Explode: false, Required: true}) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter formId: %w", err), http.StatusBadRequest) + return + } + + for _, middleware := range siw.HandlerMiddlewares { + middleware(c) + if c.IsAborted() { + return + } + } + + siw.Handler.CreateFormVersion(c, formId) +} + +// GetHealth operation middleware +func (siw *ServerInterfaceWrapper) GetHealth(c *gin.Context) { + + for _, middleware := range siw.HandlerMiddlewares { + middleware(c) + if c.IsAborted() { + return + } + } + + siw.Handler.GetHealth(c) +} + +// GetSubmission operation middleware +func (siw *ServerInterfaceWrapper) GetSubmission(c *gin.Context) { + + var err error + + // ------------- Path parameter "submissionId" ------------- + var submissionId openapi_types.UUID + + err = runtime.BindStyledParameterWithOptions("simple", "submissionId", c.Param("submissionId"), &submissionId, runtime.BindStyledParameterOptions{Explode: false, Required: true}) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter submissionId: %w", err), http.StatusBadRequest) + return + } + + for _, middleware := range siw.HandlerMiddlewares { + middleware(c) + if c.IsAborted() { + return + } + } + + siw.Handler.GetSubmission(c, submissionId) +} + +// GetSubmissionOfflinePdf operation middleware +func (siw *ServerInterfaceWrapper) GetSubmissionOfflinePdf(c *gin.Context) { + + var err error + + // ------------- Path parameter "submissionId" ------------- + var submissionId openapi_types.UUID + + err = runtime.BindStyledParameterWithOptions("simple", "submissionId", c.Param("submissionId"), &submissionId, runtime.BindStyledParameterOptions{Explode: false, Required: true}) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter submissionId: %w", err), http.StatusBadRequest) + return + } + + for _, middleware := range siw.HandlerMiddlewares { + middleware(c) + if c.IsAborted() { + return + } + } + + siw.Handler.GetSubmissionOfflinePdf(c, submissionId) +} + +// GinServerOptions provides options for the Gin server. +type GinServerOptions struct { + BaseURL string + Middlewares []MiddlewareFunc + ErrorHandler func(*gin.Context, error, int) +} + +// RegisterHandlers creates http.Handler with routing matching OpenAPI spec. +func RegisterHandlers(router gin.IRouter, si ServerInterface) { + RegisterHandlersWithOptions(router, si, GinServerOptions{}) +} + +// RegisterHandlersWithOptions creates http.Handler with additional options +func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options GinServerOptions) { + errorHandler := options.ErrorHandler + if errorHandler == nil { + errorHandler = func(c *gin.Context, err error, statusCode int) { + c.JSON(statusCode, gin.H{"msg": err.Error()}) + } + } + + wrapper := ServerInterfaceWrapper{ + Handler: si, + HandlerMiddlewares: options.Middlewares, + ErrorHandler: errorHandler, + } + + router.GET(options.BaseURL+"/forms", wrapper.ListForms) + router.POST(options.BaseURL+"/forms", wrapper.CreateForm) + router.DELETE(options.BaseURL+"/forms/:formId", wrapper.DeleteForm) + router.GET(options.BaseURL+"/forms/:formId", wrapper.GetForm) + router.PUT(options.BaseURL+"/forms/:formId", wrapper.UpdateForm) + router.GET(options.BaseURL+"/forms/:formId/access-codes", wrapper.ListAccessCodes) + router.POST(options.BaseURL+"/forms/:formId/access-codes", wrapper.CreateAccessCodes) + router.POST(options.BaseURL+"/forms/:formId/activate", wrapper.ActivateForm) + router.POST(options.BaseURL+"/forms/:formId/inactivate", wrapper.InactivateForm) + router.GET(options.BaseURL+"/forms/:formId/pdf", wrapper.GetFormPdf) + router.GET(options.BaseURL+"/forms/:formId/submissions", wrapper.ListFormSubmissions) + router.POST(options.BaseURL+"/forms/:formId/submissions", wrapper.SubmitForm) + router.POST(options.BaseURL+"/forms/:formId/versions", wrapper.CreateFormVersion) + router.GET(options.BaseURL+"/health", wrapper.GetHealth) + router.GET(options.BaseURL+"/submissions/:submissionId", wrapper.GetSubmission) + router.GET(options.BaseURL+"/submissions/:submissionId/offline-pdf", wrapper.GetSubmissionOfflinePdf) +} + +type ErrorJSONResponse Error + +type ListFormsRequestObject struct { + Params ListFormsParams +} + +type ListFormsResponseObject interface { + VisitListFormsResponse(w http.ResponseWriter) error +} + +type ListForms200JSONResponse []FormSummary + +func (response ListForms200JSONResponse) VisitListFormsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type ListForms500JSONResponse struct{ ErrorJSONResponse } + +func (response ListForms500JSONResponse) VisitListFormsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type CreateFormRequestObject struct { + Body *CreateFormJSONRequestBody +} + +type CreateFormResponseObject interface { + VisitCreateFormResponse(w http.ResponseWriter) error +} + +type CreateForm201JSONResponse Form + +func (response CreateForm201JSONResponse) VisitCreateFormResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(201) + + return json.NewEncoder(w).Encode(response) +} + +type CreateForm400JSONResponse struct{ ErrorJSONResponse } + +func (response CreateForm400JSONResponse) VisitCreateFormResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type CreateForm500JSONResponse Error + +func (response CreateForm500JSONResponse) VisitCreateFormResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type DeleteFormRequestObject struct { + FormId FormId `json:"formId"` +} + +type DeleteFormResponseObject interface { + VisitDeleteFormResponse(w http.ResponseWriter) error +} + +type DeleteForm204Response struct { +} + +func (response DeleteForm204Response) VisitDeleteFormResponse(w http.ResponseWriter) error { + w.WriteHeader(204) + return nil +} + +type DeleteForm404JSONResponse struct{ ErrorJSONResponse } + +func (response DeleteForm404JSONResponse) VisitDeleteFormResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type DeleteForm500JSONResponse Error + +func (response DeleteForm500JSONResponse) VisitDeleteFormResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type GetFormRequestObject struct { + FormId FormId `json:"formId"` +} + +type GetFormResponseObject interface { + VisitGetFormResponse(w http.ResponseWriter) error +} + +type GetForm200JSONResponse Form + +func (response GetForm200JSONResponse) VisitGetFormResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetForm404JSONResponse struct{ ErrorJSONResponse } + +func (response GetForm404JSONResponse) VisitGetFormResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type GetForm500JSONResponse Error + +func (response GetForm500JSONResponse) VisitGetFormResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type UpdateFormRequestObject struct { + FormId FormId `json:"formId"` + Body *UpdateFormJSONRequestBody +} + +type UpdateFormResponseObject interface { + VisitUpdateFormResponse(w http.ResponseWriter) error +} + +type UpdateForm200JSONResponse Form + +func (response UpdateForm200JSONResponse) VisitUpdateFormResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type UpdateForm400JSONResponse struct{ ErrorJSONResponse } + +func (response UpdateForm400JSONResponse) VisitUpdateFormResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type UpdateForm404JSONResponse Error + +func (response UpdateForm404JSONResponse) VisitUpdateFormResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type UpdateForm500JSONResponse Error + +func (response UpdateForm500JSONResponse) VisitUpdateFormResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type ListAccessCodesRequestObject struct { + FormId FormId `json:"formId"` +} + +type ListAccessCodesResponseObject interface { + VisitListAccessCodesResponse(w http.ResponseWriter) error +} + +type ListAccessCodes200JSONResponse []AccessCode + +func (response ListAccessCodes200JSONResponse) VisitListAccessCodesResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type ListAccessCodes404JSONResponse struct{ ErrorJSONResponse } + +func (response ListAccessCodes404JSONResponse) VisitListAccessCodesResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type ListAccessCodes500JSONResponse Error + +func (response ListAccessCodes500JSONResponse) VisitListAccessCodesResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type CreateAccessCodesRequestObject struct { + FormId FormId `json:"formId"` + Body *CreateAccessCodesJSONRequestBody +} + +type CreateAccessCodesResponseObject interface { + VisitCreateAccessCodesResponse(w http.ResponseWriter) error +} + +type CreateAccessCodes201JSONResponse []AccessCode + +func (response CreateAccessCodes201JSONResponse) VisitCreateAccessCodesResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(201) + + return json.NewEncoder(w).Encode(response) +} + +type CreateAccessCodes400JSONResponse struct{ ErrorJSONResponse } + +func (response CreateAccessCodes400JSONResponse) VisitCreateAccessCodesResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type CreateAccessCodes404JSONResponse Error + +func (response CreateAccessCodes404JSONResponse) VisitCreateAccessCodesResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type CreateAccessCodes500JSONResponse Error + +func (response CreateAccessCodes500JSONResponse) VisitCreateAccessCodesResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type ActivateFormRequestObject struct { + FormId FormId `json:"formId"` +} + +type ActivateFormResponseObject interface { + VisitActivateFormResponse(w http.ResponseWriter) error +} + +type ActivateForm200JSONResponse Form + +func (response ActivateForm200JSONResponse) VisitActivateFormResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type ActivateForm404JSONResponse struct{ ErrorJSONResponse } + +func (response ActivateForm404JSONResponse) VisitActivateFormResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type ActivateForm500JSONResponse Error + +func (response ActivateForm500JSONResponse) VisitActivateFormResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type InactivateFormRequestObject struct { + FormId FormId `json:"formId"` +} + +type InactivateFormResponseObject interface { + VisitInactivateFormResponse(w http.ResponseWriter) error +} + +type InactivateForm200JSONResponse Form + +func (response InactivateForm200JSONResponse) VisitInactivateFormResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type InactivateForm404JSONResponse struct{ ErrorJSONResponse } + +func (response InactivateForm404JSONResponse) VisitInactivateFormResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type InactivateForm500JSONResponse Error + +func (response InactivateForm500JSONResponse) VisitInactivateFormResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type GetFormPdfRequestObject struct { + FormId FormId `json:"formId"` +} + +type GetFormPdfResponseObject interface { + VisitGetFormPdfResponse(w http.ResponseWriter) error +} + +type GetFormPdf200ApplicationpdfResponse struct { + Body io.Reader + ContentLength int64 +} + +func (response GetFormPdf200ApplicationpdfResponse) VisitGetFormPdfResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/pdf") + if response.ContentLength != 0 { + w.Header().Set("Content-Length", fmt.Sprint(response.ContentLength)) + } + w.WriteHeader(200) + + if closer, ok := response.Body.(io.ReadCloser); ok { + defer closer.Close() + } + _, err := io.Copy(w, response.Body) + return err +} + +type GetFormPdf404JSONResponse struct{ ErrorJSONResponse } + +func (response GetFormPdf404JSONResponse) VisitGetFormPdfResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type GetFormPdf500JSONResponse Error + +func (response GetFormPdf500JSONResponse) VisitGetFormPdfResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type ListFormSubmissionsRequestObject struct { + FormId FormId `json:"formId"` +} + +type ListFormSubmissionsResponseObject interface { + VisitListFormSubmissionsResponse(w http.ResponseWriter) error +} + +type ListFormSubmissions200JSONResponse []SubmissionSummary + +func (response ListFormSubmissions200JSONResponse) VisitListFormSubmissionsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type ListFormSubmissions404JSONResponse struct{ ErrorJSONResponse } + +func (response ListFormSubmissions404JSONResponse) VisitListFormSubmissionsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type ListFormSubmissions500JSONResponse Error + +func (response ListFormSubmissions500JSONResponse) VisitListFormSubmissionsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type SubmitFormRequestObject struct { + FormId FormId `json:"formId"` + Body *SubmitFormJSONRequestBody +} + +type SubmitFormResponseObject interface { + VisitSubmitFormResponse(w http.ResponseWriter) error +} + +type SubmitForm201JSONResponse Submission + +func (response SubmitForm201JSONResponse) VisitSubmitFormResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(201) + + return json.NewEncoder(w).Encode(response) +} + +type SubmitForm400JSONResponse struct{ ErrorJSONResponse } + +func (response SubmitForm400JSONResponse) VisitSubmitFormResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type SubmitForm404JSONResponse Error + +func (response SubmitForm404JSONResponse) VisitSubmitFormResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type SubmitForm409JSONResponse Error + +func (response SubmitForm409JSONResponse) VisitSubmitFormResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(409) + + return json.NewEncoder(w).Encode(response) +} + +type SubmitForm410JSONResponse Error + +func (response SubmitForm410JSONResponse) VisitSubmitFormResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(410) + + return json.NewEncoder(w).Encode(response) +} + +type SubmitForm500JSONResponse Error + +func (response SubmitForm500JSONResponse) VisitSubmitFormResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type CreateFormVersionRequestObject struct { + FormId FormId `json:"formId"` +} + +type CreateFormVersionResponseObject interface { + VisitCreateFormVersionResponse(w http.ResponseWriter) error +} + +type CreateFormVersion201JSONResponse Form + +func (response CreateFormVersion201JSONResponse) VisitCreateFormVersionResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(201) + + return json.NewEncoder(w).Encode(response) +} + +type CreateFormVersion404JSONResponse struct{ ErrorJSONResponse } + +func (response CreateFormVersion404JSONResponse) VisitCreateFormVersionResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type CreateFormVersion500JSONResponse Error + +func (response CreateFormVersion500JSONResponse) VisitCreateFormVersionResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type GetHealthRequestObject struct { +} + +type GetHealthResponseObject interface { + VisitGetHealthResponse(w http.ResponseWriter) error +} + +type GetHealth200JSONResponse Health + +func (response GetHealth200JSONResponse) VisitGetHealthResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetSubmissionRequestObject struct { + SubmissionId openapi_types.UUID `json:"submissionId"` +} + +type GetSubmissionResponseObject interface { + VisitGetSubmissionResponse(w http.ResponseWriter) error +} + +type GetSubmission200JSONResponse Submission + +func (response GetSubmission200JSONResponse) VisitGetSubmissionResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetSubmission404JSONResponse struct{ ErrorJSONResponse } + +func (response GetSubmission404JSONResponse) VisitGetSubmissionResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type GetSubmission500JSONResponse Error + +func (response GetSubmission500JSONResponse) VisitGetSubmissionResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type GetSubmissionOfflinePdfRequestObject struct { + SubmissionId openapi_types.UUID `json:"submissionId"` +} + +type GetSubmissionOfflinePdfResponseObject interface { + VisitGetSubmissionOfflinePdfResponse(w http.ResponseWriter) error +} + +type GetSubmissionOfflinePdf200ApplicationpdfResponse struct { + Body io.Reader + ContentLength int64 +} + +func (response GetSubmissionOfflinePdf200ApplicationpdfResponse) VisitGetSubmissionOfflinePdfResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/pdf") + if response.ContentLength != 0 { + w.Header().Set("Content-Length", fmt.Sprint(response.ContentLength)) + } + w.WriteHeader(200) + + if closer, ok := response.Body.(io.ReadCloser); ok { + defer closer.Close() + } + _, err := io.Copy(w, response.Body) + return err +} + +type GetSubmissionOfflinePdf404JSONResponse struct{ ErrorJSONResponse } + +func (response GetSubmissionOfflinePdf404JSONResponse) VisitGetSubmissionOfflinePdfResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type GetSubmissionOfflinePdf500JSONResponse Error + +func (response GetSubmissionOfflinePdf500JSONResponse) VisitGetSubmissionOfflinePdfResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +// StrictServerInterface represents all server handlers. +type StrictServerInterface interface { + // List forms + // (GET /forms) + ListForms(ctx context.Context, request ListFormsRequestObject) (ListFormsResponseObject, error) + // Create a form + // (POST /forms) + CreateForm(ctx context.Context, request CreateFormRequestObject) (CreateFormResponseObject, error) + // Delete a form definition + // (DELETE /forms/{formId}) + DeleteForm(ctx context.Context, request DeleteFormRequestObject) (DeleteFormResponseObject, error) + // Get a form by id + // (GET /forms/{formId}) + GetForm(ctx context.Context, request GetFormRequestObject) (GetFormResponseObject, error) + // Update a form definition + // (PUT /forms/{formId}) + UpdateForm(ctx context.Context, request UpdateFormRequestObject) (UpdateFormResponseObject, error) + // List access codes for a form + // (GET /forms/{formId}/access-codes) + ListAccessCodes(ctx context.Context, request ListAccessCodesRequestObject) (ListAccessCodesResponseObject, error) + // Create one or more access codes for a form + // (POST /forms/{formId}/access-codes) + CreateAccessCodes(ctx context.Context, request CreateAccessCodesRequestObject) (CreateAccessCodesResponseObject, error) + // Activate a form + // (POST /forms/{formId}/activate) + ActivateForm(ctx context.Context, request ActivateFormRequestObject) (ActivateFormResponseObject, error) + // Inactivate a form + // (POST /forms/{formId}/inactivate) + InactivateForm(ctx context.Context, request InactivateFormRequestObject) (InactivateFormResponseObject, error) + // Download a PDF rendering of the form definition + // (GET /forms/{formId}/pdf) + GetFormPdf(ctx context.Context, request GetFormPdfRequestObject) (GetFormPdfResponseObject, error) + // List submissions for a form (management) + // (GET /forms/{formId}/submissions) + ListFormSubmissions(ctx context.Context, request ListFormSubmissionsRequestObject) (ListFormSubmissionsResponseObject, error) + // Submit an answer set for a form + // (POST /forms/{formId}/submissions) + SubmitForm(ctx context.Context, request SubmitFormRequestObject) (SubmitFormResponseObject, error) + // Create a new version of a form + // (POST /forms/{formId}/versions) + CreateFormVersion(ctx context.Context, request CreateFormVersionRequestObject) (CreateFormVersionResponseObject, error) + // Service health + // (GET /health) + GetHealth(ctx context.Context, request GetHealthRequestObject) (GetHealthResponseObject, error) + // Get a submission by id (management) + // (GET /submissions/{submissionId}) + GetSubmission(ctx context.Context, request GetSubmissionRequestObject) (GetSubmissionResponseObject, error) + // Download the offline PDF uploaded with a submission + // (GET /submissions/{submissionId}/offline-pdf) + GetSubmissionOfflinePdf(ctx context.Context, request GetSubmissionOfflinePdfRequestObject) (GetSubmissionOfflinePdfResponseObject, error) +} + +type StrictHandlerFunc = strictgin.StrictGinHandlerFunc +type StrictMiddlewareFunc = strictgin.StrictGinMiddlewareFunc + +func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface { + return &strictHandler{ssi: ssi, middlewares: middlewares} +} + +type strictHandler struct { + ssi StrictServerInterface + middlewares []StrictMiddlewareFunc +} + +// ListForms operation middleware +func (sh *strictHandler) ListForms(ctx *gin.Context, params ListFormsParams) { + var request ListFormsRequestObject + + request.Params = params + + handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { + return sh.ssi.ListForms(ctx, request.(ListFormsRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "ListForms") + } + + response, err := handler(ctx, request) + + if err != nil { + ctx.Error(err) + ctx.Status(http.StatusInternalServerError) + } else if validResponse, ok := response.(ListFormsResponseObject); ok { + if err := validResponse.VisitListFormsResponse(ctx.Writer); err != nil { + ctx.Error(err) + } + } else if response != nil { + ctx.Error(fmt.Errorf("unexpected response type: %T", response)) + } +} + +// CreateForm operation middleware +func (sh *strictHandler) CreateForm(ctx *gin.Context) { + var request CreateFormRequestObject + + var body CreateFormJSONRequestBody + if err := ctx.ShouldBindJSON(&body); err != nil { + ctx.Status(http.StatusBadRequest) + ctx.Error(err) + return + } + request.Body = &body + + handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { + return sh.ssi.CreateForm(ctx, request.(CreateFormRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "CreateForm") + } + + response, err := handler(ctx, request) + + if err != nil { + ctx.Error(err) + ctx.Status(http.StatusInternalServerError) + } else if validResponse, ok := response.(CreateFormResponseObject); ok { + if err := validResponse.VisitCreateFormResponse(ctx.Writer); err != nil { + ctx.Error(err) + } + } else if response != nil { + ctx.Error(fmt.Errorf("unexpected response type: %T", response)) + } +} + +// DeleteForm operation middleware +func (sh *strictHandler) DeleteForm(ctx *gin.Context, formId FormId) { + var request DeleteFormRequestObject + + request.FormId = formId + + handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { + return sh.ssi.DeleteForm(ctx, request.(DeleteFormRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "DeleteForm") + } + + response, err := handler(ctx, request) + + if err != nil { + ctx.Error(err) + ctx.Status(http.StatusInternalServerError) + } else if validResponse, ok := response.(DeleteFormResponseObject); ok { + if err := validResponse.VisitDeleteFormResponse(ctx.Writer); err != nil { + ctx.Error(err) + } + } else if response != nil { + ctx.Error(fmt.Errorf("unexpected response type: %T", response)) + } +} + +// GetForm operation middleware +func (sh *strictHandler) GetForm(ctx *gin.Context, formId FormId) { + var request GetFormRequestObject + + request.FormId = formId + + handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { + return sh.ssi.GetForm(ctx, request.(GetFormRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetForm") + } + + response, err := handler(ctx, request) + + if err != nil { + ctx.Error(err) + ctx.Status(http.StatusInternalServerError) + } else if validResponse, ok := response.(GetFormResponseObject); ok { + if err := validResponse.VisitGetFormResponse(ctx.Writer); err != nil { + ctx.Error(err) + } + } else if response != nil { + ctx.Error(fmt.Errorf("unexpected response type: %T", response)) + } +} + +// UpdateForm operation middleware +func (sh *strictHandler) UpdateForm(ctx *gin.Context, formId FormId) { + var request UpdateFormRequestObject + + request.FormId = formId + + var body UpdateFormJSONRequestBody + if err := ctx.ShouldBindJSON(&body); err != nil { + ctx.Status(http.StatusBadRequest) + ctx.Error(err) + return + } + request.Body = &body + + handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { + return sh.ssi.UpdateForm(ctx, request.(UpdateFormRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "UpdateForm") + } + + response, err := handler(ctx, request) + + if err != nil { + ctx.Error(err) + ctx.Status(http.StatusInternalServerError) + } else if validResponse, ok := response.(UpdateFormResponseObject); ok { + if err := validResponse.VisitUpdateFormResponse(ctx.Writer); err != nil { + ctx.Error(err) + } + } else if response != nil { + ctx.Error(fmt.Errorf("unexpected response type: %T", response)) + } +} + +// ListAccessCodes operation middleware +func (sh *strictHandler) ListAccessCodes(ctx *gin.Context, formId FormId) { + var request ListAccessCodesRequestObject + + request.FormId = formId + + handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { + return sh.ssi.ListAccessCodes(ctx, request.(ListAccessCodesRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "ListAccessCodes") + } + + response, err := handler(ctx, request) + + if err != nil { + ctx.Error(err) + ctx.Status(http.StatusInternalServerError) + } else if validResponse, ok := response.(ListAccessCodesResponseObject); ok { + if err := validResponse.VisitListAccessCodesResponse(ctx.Writer); err != nil { + ctx.Error(err) + } + } else if response != nil { + ctx.Error(fmt.Errorf("unexpected response type: %T", response)) + } +} + +// CreateAccessCodes operation middleware +func (sh *strictHandler) CreateAccessCodes(ctx *gin.Context, formId FormId) { + var request CreateAccessCodesRequestObject + + request.FormId = formId + + var body CreateAccessCodesJSONRequestBody + if err := ctx.ShouldBindJSON(&body); err != nil { + ctx.Status(http.StatusBadRequest) + ctx.Error(err) + return + } + request.Body = &body + + handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { + return sh.ssi.CreateAccessCodes(ctx, request.(CreateAccessCodesRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "CreateAccessCodes") + } + + response, err := handler(ctx, request) + + if err != nil { + ctx.Error(err) + ctx.Status(http.StatusInternalServerError) + } else if validResponse, ok := response.(CreateAccessCodesResponseObject); ok { + if err := validResponse.VisitCreateAccessCodesResponse(ctx.Writer); err != nil { + ctx.Error(err) + } + } else if response != nil { + ctx.Error(fmt.Errorf("unexpected response type: %T", response)) + } +} + +// ActivateForm operation middleware +func (sh *strictHandler) ActivateForm(ctx *gin.Context, formId FormId) { + var request ActivateFormRequestObject + + request.FormId = formId + + handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { + return sh.ssi.ActivateForm(ctx, request.(ActivateFormRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "ActivateForm") + } + + response, err := handler(ctx, request) + + if err != nil { + ctx.Error(err) + ctx.Status(http.StatusInternalServerError) + } else if validResponse, ok := response.(ActivateFormResponseObject); ok { + if err := validResponse.VisitActivateFormResponse(ctx.Writer); err != nil { + ctx.Error(err) + } + } else if response != nil { + ctx.Error(fmt.Errorf("unexpected response type: %T", response)) + } +} + +// InactivateForm operation middleware +func (sh *strictHandler) InactivateForm(ctx *gin.Context, formId FormId) { + var request InactivateFormRequestObject + + request.FormId = formId + + handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { + return sh.ssi.InactivateForm(ctx, request.(InactivateFormRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "InactivateForm") + } + + response, err := handler(ctx, request) + + if err != nil { + ctx.Error(err) + ctx.Status(http.StatusInternalServerError) + } else if validResponse, ok := response.(InactivateFormResponseObject); ok { + if err := validResponse.VisitInactivateFormResponse(ctx.Writer); err != nil { + ctx.Error(err) + } + } else if response != nil { + ctx.Error(fmt.Errorf("unexpected response type: %T", response)) + } +} + +// GetFormPdf operation middleware +func (sh *strictHandler) GetFormPdf(ctx *gin.Context, formId FormId) { + var request GetFormPdfRequestObject + + request.FormId = formId + + handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { + return sh.ssi.GetFormPdf(ctx, request.(GetFormPdfRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetFormPdf") + } + + response, err := handler(ctx, request) + + if err != nil { + ctx.Error(err) + ctx.Status(http.StatusInternalServerError) + } else if validResponse, ok := response.(GetFormPdfResponseObject); ok { + if err := validResponse.VisitGetFormPdfResponse(ctx.Writer); err != nil { + ctx.Error(err) + } + } else if response != nil { + ctx.Error(fmt.Errorf("unexpected response type: %T", response)) + } +} + +// ListFormSubmissions operation middleware +func (sh *strictHandler) ListFormSubmissions(ctx *gin.Context, formId FormId) { + var request ListFormSubmissionsRequestObject + + request.FormId = formId + + handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { + return sh.ssi.ListFormSubmissions(ctx, request.(ListFormSubmissionsRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "ListFormSubmissions") + } + + response, err := handler(ctx, request) + + if err != nil { + ctx.Error(err) + ctx.Status(http.StatusInternalServerError) + } else if validResponse, ok := response.(ListFormSubmissionsResponseObject); ok { + if err := validResponse.VisitListFormSubmissionsResponse(ctx.Writer); err != nil { + ctx.Error(err) + } + } else if response != nil { + ctx.Error(fmt.Errorf("unexpected response type: %T", response)) + } +} + +// SubmitForm operation middleware +func (sh *strictHandler) SubmitForm(ctx *gin.Context, formId FormId) { + var request SubmitFormRequestObject + + request.FormId = formId + + var body SubmitFormJSONRequestBody + if err := ctx.ShouldBindJSON(&body); err != nil { + ctx.Status(http.StatusBadRequest) + ctx.Error(err) + return + } + request.Body = &body + + handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { + return sh.ssi.SubmitForm(ctx, request.(SubmitFormRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "SubmitForm") + } + + response, err := handler(ctx, request) + + if err != nil { + ctx.Error(err) + ctx.Status(http.StatusInternalServerError) + } else if validResponse, ok := response.(SubmitFormResponseObject); ok { + if err := validResponse.VisitSubmitFormResponse(ctx.Writer); err != nil { + ctx.Error(err) + } + } else if response != nil { + ctx.Error(fmt.Errorf("unexpected response type: %T", response)) + } +} + +// CreateFormVersion operation middleware +func (sh *strictHandler) CreateFormVersion(ctx *gin.Context, formId FormId) { + var request CreateFormVersionRequestObject + + request.FormId = formId + + handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { + return sh.ssi.CreateFormVersion(ctx, request.(CreateFormVersionRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "CreateFormVersion") + } + + response, err := handler(ctx, request) + + if err != nil { + ctx.Error(err) + ctx.Status(http.StatusInternalServerError) + } else if validResponse, ok := response.(CreateFormVersionResponseObject); ok { + if err := validResponse.VisitCreateFormVersionResponse(ctx.Writer); err != nil { + ctx.Error(err) + } + } else if response != nil { + ctx.Error(fmt.Errorf("unexpected response type: %T", response)) + } +} + +// GetHealth operation middleware +func (sh *strictHandler) GetHealth(ctx *gin.Context) { + var request GetHealthRequestObject + + handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { + return sh.ssi.GetHealth(ctx, request.(GetHealthRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetHealth") + } + + response, err := handler(ctx, request) + + if err != nil { + ctx.Error(err) + ctx.Status(http.StatusInternalServerError) + } else if validResponse, ok := response.(GetHealthResponseObject); ok { + if err := validResponse.VisitGetHealthResponse(ctx.Writer); err != nil { + ctx.Error(err) + } + } else if response != nil { + ctx.Error(fmt.Errorf("unexpected response type: %T", response)) + } +} + +// GetSubmission operation middleware +func (sh *strictHandler) GetSubmission(ctx *gin.Context, submissionId openapi_types.UUID) { + var request GetSubmissionRequestObject + + request.SubmissionId = submissionId + + handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { + return sh.ssi.GetSubmission(ctx, request.(GetSubmissionRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetSubmission") + } + + response, err := handler(ctx, request) + + if err != nil { + ctx.Error(err) + ctx.Status(http.StatusInternalServerError) + } else if validResponse, ok := response.(GetSubmissionResponseObject); ok { + if err := validResponse.VisitGetSubmissionResponse(ctx.Writer); err != nil { + ctx.Error(err) + } + } else if response != nil { + ctx.Error(fmt.Errorf("unexpected response type: %T", response)) + } +} + +// GetSubmissionOfflinePdf operation middleware +func (sh *strictHandler) GetSubmissionOfflinePdf(ctx *gin.Context, submissionId openapi_types.UUID) { + var request GetSubmissionOfflinePdfRequestObject + + request.SubmissionId = submissionId + + handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { + return sh.ssi.GetSubmissionOfflinePdf(ctx, request.(GetSubmissionOfflinePdfRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetSubmissionOfflinePdf") + } + + response, err := handler(ctx, request) + + if err != nil { + ctx.Error(err) + ctx.Status(http.StatusInternalServerError) + } else if validResponse, ok := response.(GetSubmissionOfflinePdfResponseObject); ok { + if err := validResponse.VisitGetSubmissionOfflinePdfResponse(ctx.Writer); err != nil { + ctx.Error(err) + } + } else if response != nil { + ctx.Error(fmt.Errorf("unexpected response type: %T", response)) + } +} + +// Base64 encoded, gzipped, json marshaled Swagger object +var swaggerSpec = []string{ + + "H4sIAAAAAAAC/+xc627cRpZ+lQPu/rAMqltOvEG2gwVWtiKvNrGtseUMjMhAV5Onuysiq5iqYksdQ8C8", + "wzzGPMe8yDzJoC4ki80i3S1LHQfIL0lkXU6dy3cudaiPUcLzgjNkSkaTj1FBBMlRoTB/nXKRn6X6N8qi", + "SVQQtYziiJEco0k0ty/jSOCvJRWYRhMlSowjmSwxJ3qWHkJUNInKkuqRal3omVIJyhbR7e2tniwLziSa", + "/b4Xggv9S8KZQqb0r6QoMpoQRTkb/yI508+aHf5T4DyaRP8xbo4xtm/l2K5mdklRJoIWepFoYreBaudI", + "D3Bz9JLHSYJSPucpGoYIXqBQ1BKYuKcbB4mjRCBRmB6r1rFTovBQ0Ry7Z48r/n2aTXFEtxtWSkw96mac", + "Z0hY9WZ72m59mf4cmc1qaRsWuL38c3+ol+GzXzBRettjJq9RdNn4a4lSy2LL469IVhq2t8VoVwfzdgRv", + "zfAYWJnPUMTgTg9cQEalAj4Hu6KEFAtkKWUL4AzUEqGiB/TWow4DPHIrYvpP+wYTLtLPPnM1/AdcBxWu", + "hycXS4SEFKoUmFrODJ/G32bobM8zikxdCJJcaQI6+9r3IEttrZiCciMhRUVoJkdwdg6EpVBKFEAWejAR", + "Hq0SxQrFoaQpjqJ4g3cZT0gWNrwiI0n4jcA5Cqt9nXda73/jLDTxNnR6o+UNMMg3aBgXAojSwlZObmhe", + "5tHkydFRHOWUub/q1SlTuEBhziBwTm/CtPiSs4t/6CVQw/UAZXlB2PqVAe8QgnGmSKK+zwnNhgacL8N8", + "qwe8E+H5LYUJvMcbhYKR7I2RG0vwRzLDbLuhb2ouhdCPMiV4Wia9W2d8wfvILtJ536vKeAx/qcJcfsol", + "/aWyPVaURnBuTSIEWUeNuGXbCXWPJOmCEW05wydXVGX4acWyw/wDhZSs9s5b+kSsxg9vboeFNtT6/AUq", + "8h18/d11vw2z5jHIJb9mMOfCOK9qLohqcmze4Q3JiwzhjK04TdA5xhB1g+bUJuCvS1RLjeDNbkBTZIrO", + "KQqgEirRwgznXKAhUTMJEsJghiDLWU6VQs/reQqrR74QvCzuNzLaRID2oc68tyDR/sSbIiOUaR9WlKLg", + "EmOYkeRqIXjJUuPLtLfjQlU+LrixPE4UXfVY8Xa40yb2tfmFZEBmkmelQnj35kcd3RAoBM5Kmik4Pzm1", + "PM8ou9LemINaUh34zCmjZqH4PuHs3pGsT+lSuqCKZFBP0hpXxxBEWYNgqeZHpXhBPesDxjgqi3RX616h", + "kG3Lrp17Xyhd6XgzOa5RuNaZIBN9OflY5FPeB6ZvyzwnYh3A1D8hbV+Q1t7cvJBAsgycIshKdyXJETK+", + "oAnJKkW+Nzz8A8LS8yq07gbQfxBrbp9kF+P9PySZWnbtViqiSvObMw096ypYGyj0ed9iwlkqW8ygTH3z", + "NArlJR4nmuWfjI5GR5+sGTjCmjVCp6rdR+dcnwKXLdX8yibPbU19q8gsQ8hJsqQM4QrXDneobAoB11Qt", + "KQv4EC9pCOPZRauegDcKHuVkrWEiLzNFM8owBsbBEq9MjkwFGh2RB6GNcnLzUzjbf2mTTA0f/LpK+M1p", + "phYap+A7C7eyQ81bk5b2rWwT1ruvXJ11CG+nmj1TILaOo7kkC8IsnzTM6/kyiKm8qAOV9uLnAiUyZSmV", + "mGGi2pTuFNdYUAtFN6bssORZ2lNgKLikqgdFfEsJJm3myXYUXuixQVyqKbBmUOmrW96bMGSZNk/d2Tyv", + "eopWWW9G7+v4oJoOa9qgpuxJ9A8s3LsL83URBtt+qaw2uN4D9nZYRdQQCRfu9G2jfQxTx6Ep/Otvfwcl", + "ShzPSSbxkj1uAEe/YmWOgiYOM8zrysr1a84QrMRhLngOBBLO5nRh0oOMSmVmWNjR4+cC8dCEJBapJWWL", + "DIGLBqwPzBTtnKdwCAnJkKVEgH4AlMH0/fv37w9fvjw8OZkaTL/UFoeszDVnKsHHUR2qWnK16PBG/9AL", + "eTxrmP+2ynEC9kcU6QvQWF9dROdMu0VC/WHVNlpc0x9UY6e41Ukc3R6VITWql+zDpYYvG27h1Qst1P8/", + "//4F0JwsUMuPmBD1kavUQooJTzEFSX9D+ApePgs644q97fVf6Vjdxe0FCsmZSVH1nCG+9oTQ5v0Oyz2Q", + "MAYl0GvJqSDXTJtKk6ObR+DCqZng19KZrhGFHlsWGSea+VY4pgBnay+kWadlWWZN7c/1hLD96KxMyiDk", + "WfjY3iu0rnYCPqGTbgatZocbPz3gp/5EZOsImM/nGsTO0/mroOJW8dL1Eq147K2oTn8boWgU1dG/whTc", + "gjqXC+0nfdDaSiHNrCqB3gmdvMuooZ3qSyt7DyrO0nCmaF5dbGFL75qRw1elvhDrvVs7xbUmtpkQNL1a", + "n3vLOF+sGt5JwF+esLYSkRq8hiOtcmjbGKsaVWONJjRZEgnTbiUQ/scEStMQ6+6Gb1sj20Y7Rada59fP", + "SmkrPRllVzbJrhov9FMCwoAqUHbJCOMmM5RrqTBvl/yIdiCm6hcDScxNqPboBc9osnaFwNEwAnYpf0Yk", + "fvO0iQVMuer85DSAd488Yi6ZjgfJxG9NKdL5dzOzXDwajaYHI3gZiiuohCdH8PLZ6JLdBa5fC7qgmtVz", + "miHzgo4aq+8dn5uryi3xdqNVoGXIm/pe+xpPYTbL+VIRoeAR5oRmsekf0AePoViaaF/Ai3dnJwf9APKZ", + "KDEM2yEU8Lsk2sZPi+M0FShlzy30/roc7HGOF9gqqQ71QLwzBco/Wwz+bDF4wBaDdy2LrWJ9Y/rOAl2a", + "aIw/iqOFDjg+BDsLKZvzqpeQJEZd7UKTyF3c/u+VNp1ypL2sbBobf9BP//kPODWPO92Dx+dnxjM9flwN", + "NPMfP45NsmK8lSzFCtdjd0lk8Mx5tdElu2RmZdP/5MrjaC+U3X0SZSRRdEUUpiM4dldNNiWSYFraUrQ9", + "Uwr4/JLVPIXrJZfoiiN2A4OR84lXU4mrIoqmNxW8OEz5NQNbl6CcxZeMi43KyAjeVH2aZtUarlOYrYHY", + "rq5+DL9kFsS9W9n6YDlZu5INybJ1dddmWMkWGR6W+kBGVUH7Ues4M5ogk0ZJnNBenl142tiWIRyfn3lX", + "EdUdhqkOIiMFjSbR16Oj0ddasYhaGhsbW62YfIwWqEK+S5WCSXsGaQJyinIE7yTClLIkK1M8s3LEqY50", + "3DNfuFZx4FFOGFlgrv3giuL1gY5jNK6Sql8w+pFKdeq01G/R/dl15v5aolg3Gryxf+T35KY4J2WmoolV", + "ho5F38Z6TCEw0SRWPb2hXezir1m2bm3QWfDDRpfvV0dHO/X4bgV7/vV2B/S6LcDHdVPovLLy/7JkhTap", + "ya97inUu4JIwIxyoMESRhZZLZP/+YK8CzCHbEm069lz5FqV6xtP1vXU/d1sCb9uYrAV72xHNk3sjwJwt", + "wPpTA2f26lPz/ekOfP8MKVl+AKnv8zYEdRs7kx9/tDnhrbUWnQZ0xXdintfia7HwaRct7HB33Kf7OK7d", + "0R1344a9o6IO4dpHfIEqfL6jB1eRi+radX/8eoGqYtZsDbaK0TXlNvSGdmmGjN3XExr/XLW6zeAmon4g", + "COiG7FtBwMPL1xKWejLeBQL2pBGWyK0sqIsdYxuyHOqQxQ8ium7d6yqP9uEovbB/Gz/ZRF5yj7w3LtWL", + "+kyc1cXuFpM/z0AHnPSmhB7KVwe+L3gAl/0ASmLJT1sC+2LN2sUBrnaUc4G7K1rQ3m1Qv/kR3T1o4bFb", + "+vfyxsetfGWPoqo23iVoGzf51b2IYrNTfDN5E5gTymBFJZ1lpiPAy+ZmpdI5PeMWzAoFDK9tu6i5yJHd", + "TK/Z4fcS9+YZ9yjwZuudRF7Y2v5QFHueznfjpVsz8C3rjDJi0uBAzWnjcvXkFFKelEYXFsg0XZqpgufN", + "7Y4XWOwxO+DXLOMkddcdAlmKwnwQOQ9Sdo/BcEB+nkEMhko2x2/G7iNc6t66buEQfSL3HDR5vPRcmV9j", + "OvCk6XP+XuKnjcDAlgFlq4PAVv90ije1VypTeBS+8/GU8eCS1eVFudGoMIIzT2ur8jYQ5jv2GAisSEbT", + "+JKVzFxKTpu72CnkpVS2l999wUpYaj4u4UyWOabAGcjSTLAlyLZ6Nje/DxQidq+W91zO8dppBjXeebq7", + "1HZ2s5OnR/+9y+gne6kyWTEZ3bNdzRJVOKRsm14AFavvQR4gjHmecebMyNiM0XXVtL9LoMxczutwxdFh", + "WuLNlOqBvXQHyhJhgAVTa9b+JCrBlom/s81sAleUl9J/7992dOyqqWI2vRl7r1i+wmvLp+oGYe/ZSlsS", + "pk1hIDxa1l+L9IVE7nuSB4wu3Q4hqECxoolphbCErjdtyA1YVkTWJmNu0NwhPfsZf2z+cHXbvoN7KPaA", + "h98WK/de32z4ZKucd4wLAv8NxpfAZ/1PmA/Dwh27BpfDTwT9DZNfNw1Ae88ALvzWnPOT098jxNe463UF", + "NfQYQPd14ktRAH0O8+9A7GYb34vyhGSQ4gozXmi9jeKoFFk0iZZKFZPx2LTRLLlUk2+Pvj2K7H3m4Arw", + "aPUEZkTiwfBaY1LQ8eqJcayOUZtrW9gzDjVHRQBZWnDKlNde4FCsS9dpO+myi9Tm2f7HS7Jvvh//txfQ", + "bqOOs1OiSEh2oXXfBi/ipX8R7FXHbj/c/jsAAP//u4Ta0lVKAAA=", +} + +// GetSwagger returns the content of the embedded swagger specification file +// or error if failed to decode +func decodeSpec() ([]byte, error) { + zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, "")) + if err != nil { + return nil, fmt.Errorf("error base64 decoding spec: %w", err) + } + zr, err := gzip.NewReader(bytes.NewReader(zipped)) + if err != nil { + return nil, fmt.Errorf("error decompressing spec: %w", err) + } + var buf bytes.Buffer + _, err = buf.ReadFrom(zr) + if err != nil { + return nil, fmt.Errorf("error decompressing spec: %w", err) + } + + return buf.Bytes(), nil +} + +var rawSpec = decodeSpecCached() + +// a naive cached of a decoded swagger spec +func decodeSpecCached() func() ([]byte, error) { + data, err := decodeSpec() + return func() ([]byte, error) { + return data, err + } +} + +// Constructs a synthetic filesystem for resolving external references when loading openapi specifications. +func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) { + res := make(map[string]func() ([]byte, error)) + if len(pathToFile) > 0 { + res[pathToFile] = rawSpec + } + + return res +} + +// GetSwagger returns the Swagger specification corresponding to the generated code +// in this file. The external references of Swagger specification are resolved. +// The logic of resolving external references is tightly connected to "import-mapping" feature. +// Externally referenced files must be embedded in the corresponding golang packages. +// Urls can be supported but this task was out of the scope. +func GetSwagger() (swagger *openapi3.T, err error) { + resolvePath := PathToRawSpec("") + + loader := openapi3.NewLoader() + loader.IsExternalRefsAllowed = true + loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) { + pathToFile := url.String() + pathToFile = path.Clean(pathToFile) + getSpec, ok := resolvePath[pathToFile] + if !ok { + err1 := fmt.Errorf("path not found: %s", pathToFile) + return nil, err1 + } + return getSpec() + } + var specData []byte + specData, err = rawSpec() + if err != nil { + return + } + swagger, err = loader.LoadFromData(specData) + if err != nil { + return + } + return +} diff --git a/backend/internal/api/mapping.go b/backend/internal/api/mapping.go new file mode 100644 index 0000000..b2b7028 --- /dev/null +++ b/backend/internal/api/mapping.go @@ -0,0 +1,202 @@ +package api + +import ( + "encoding/json" + + "github.com/google/uuid" + + "kereru.forms/internal/database" +) + +func toAPIForm(f *database.Form) Form { + questions := make([]Question, 0, len(f.Questions)) + for _, q := range f.Questions { + questions = append(questions, toAPIQuestion(q)) + } + return Form{ + Id: uuid.MustParse(f.ID), + FormGroupId: uuid.MustParse(f.FormGroupID), + Version: f.Version, + Title: f.Title, + Description: f.Description, + Introduction: f.Introduction, + IsActive: f.IsActive, + RequiresAccessCode: f.RequiresAccessCode, + SignatureRequired: boolPtrIfSet(f.SignatureRequired, true), + ExternalReferenceRequired: boolPtrIfSet(f.ExternalReferenceRequired, true), + ExternalReferenceLabel: f.ExternalReferenceLabel, + LogoUrl: f.LogoURL, + PdfUrl: f.PDFURL, + CompanyName: f.CompanyName, + ContactEmail: f.ContactEmail, + ContactPhone: f.ContactPhone, + ContactUrl: f.ContactURL, + CreatedAt: f.CreatedAt, + UpdatedAt: f.UpdatedAt, + Questions: questions, + } +} + +func toAPIQuestion(q database.Question) Question { + out := Question{ + Id: uuid.MustParse(q.ID), + Key: q.Key, + Label: q.Label, + Description: q.Description, + Type: QuestionType(q.Type), + Required: q.Required, + Placeholder: q.Placeholder, + Multiline: boolPtrIfSet(q.Multiline, QuestionType(q.Type) == Text), + } + if q.MinValue != nil { + v := float32(*q.MinValue) + out.MinValue = &v + } + if q.MaxValue != nil { + v := float32(*q.MaxValue) + out.MaxValue = &v + } + if len(q.Options) > 0 { + opts := make([]QuestionOption, 0, len(q.Options)) + for _, o := range q.Options { + opts = append(opts, QuestionOption{Value: o.Value, Label: o.Label}) + } + out.Options = &opts + } + return out +} + +func boolPtrIfSet(v bool, include bool) *bool { + if !include && !v { + return nil + } + b := v + return &b +} + +func toAPIFormSummary(f database.FormSummary) FormSummary { + return FormSummary{ + Id: uuid.MustParse(f.ID), + FormGroupId: uuid.MustParse(f.FormGroupID), + Version: f.Version, + Title: f.Title, + Description: f.Description, + LogoUrl: f.LogoURL, + PdfUrl: f.PDFURL, + ExternalReferenceRequired: boolPtrIfSet(f.ExternalReferenceRequired, true), + ExternalReferenceLabel: f.ExternalReferenceLabel, + IsActive: f.IsActive, + QuestionCount: f.QuestionCount, + CreatedAt: f.CreatedAt, + UpdatedAt: f.UpdatedAt, + } +} + +func toAPIAnswerRecord(a database.Answer) AnswerRecord { + var value interface{} + _ = json.Unmarshal(a.Value, &value) + return AnswerRecord{ + QuestionId: uuid.MustParse(a.QuestionID), + QuestionKey: a.QuestionKey, + Value: value, + } +} + +func toAPISubmission(s *database.Submission) Submission { + answers := make([]AnswerRecord, 0, len(s.Answers)) + for _, a := range s.Answers { + answers = append(answers, toAPIAnswerRecord(a)) + } + out := Submission{ + Id: uuid.MustParse(s.ID), + FormId: uuid.MustParse(s.FormID), + FormVersion: s.FormVersion, + UserId: s.UserID, + UserIdType: UserIdType(s.UserIDType), + SubmittedAt: s.SubmittedAt, + Answers: answers, + ExternalReference: s.ExternalReference, + OfflinePdfName: s.OfflinePDFName, + } + if s.Signature != nil { + out.Signature = &Signature{ + Type: SignatureType(s.Signature.Type), + DataUrl: s.Signature.DataURL, + Name: s.Signature.Name, + Title: s.Signature.Title, + SignedAt: s.Signature.SignedAt, + } + } + if hasTracking(s) { + out.Tracking = &Tracking{ + IpAddress: s.IPAddress, + UserAgent: s.UserAgent, + Locale: s.Locale, + Timezone: s.Timezone, + Place: s.Place, + Referer: s.Referer, + } + } + return out +} + +func hasTracking(s *database.Submission) bool { + return s.IPAddress != nil || s.UserAgent != nil || s.Locale != nil || + s.Timezone != nil || s.Place != nil || s.Referer != nil +} + +func toAPISubmissionSummary(s database.SubmissionSummary) SubmissionSummary { + return SubmissionSummary{ + Id: uuid.MustParse(s.ID), + FormId: uuid.MustParse(s.FormID), + FormVersion: s.FormVersion, + UserId: s.UserID, + UserIdType: UserIdType(s.UserIDType), + ExternalReference: s.ExternalReference, + SubmittedAt: s.SubmittedAt, + } +} + +func toAPIAccessCode(c database.AccessCode) AccessCode { + return AccessCode{ + Id: uuid.MustParse(c.ID), + FormId: uuid.MustParse(c.FormID), + Code: c.Code, + Used: c.Used, + UsedAt: c.UsedAt, + CreatedAt: c.CreatedAt, + } +} + +func questionInputsToRecords(inputs []QuestionInput) []database.Question { + out := make([]database.Question, 0, len(inputs)) + for i, q := range inputs { + rec := database.Question{ + Position: i, + Key: q.Key, + Label: q.Label, + Description: q.Description, + Type: string(q.Type), + Required: q.Required, + Placeholder: q.Placeholder, + } + if q.MinValue != nil { + v := float64(*q.MinValue) + rec.MinValue = &v + } + if q.MaxValue != nil { + v := float64(*q.MaxValue) + rec.MaxValue = &v + } + if q.Multiline != nil { + rec.Multiline = *q.Multiline + } + if q.Options != nil { + for _, o := range *q.Options { + rec.Options = append(rec.Options, database.QuestionOption{Value: o.Value, Label: o.Label}) + } + } + out = append(out, rec) + } + return out +} diff --git a/backend/internal/api/router.go b/backend/internal/api/router.go new file mode 100644 index 0000000..8ee986c --- /dev/null +++ b/backend/internal/api/router.go @@ -0,0 +1,162 @@ +package api + +import ( + "context" + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/rs/zerolog" + "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin" + + "kereru.forms/internal/logging" +) + +// RouterOptions configures cross-cutting HTTP behaviour. +type RouterOptions struct { + CORSOrigins []string + ServiceName string + // StaticDir, when set, serves a built frontend with SPA fallback. + StaticDir string +} + +// NewRouter builds the Gin engine with all middleware and API routes. +func NewRouter(server *Server, logger zerolog.Logger, opts RouterOptions) *gin.Engine { + gin.SetMode(gin.ReleaseMode) + + serviceName := opts.ServiceName + if serviceName == "" { + serviceName = "kereru-forms" + } + + r := gin.New() + r.RedirectTrailingSlash = false + // Ensure *gin.Context behaves as the request context for derived values. + r.ContextWithFallback = true + + r.Use(otelgin.Middleware(serviceName)) + r.Use(gin.Recovery()) + r.Use(requestIDMiddleware()) + r.Use(withRequestMetadata()) + r.Use(logging.GinMiddleware(logger)) + r.Use(corsMiddleware(opts.CORSOrigins)) + + strict := NewStrictHandler(server, nil) + // The API is exposed at the root (for probes and direct clients) and under + // /api so the SPA can use a stable same-origin prefix in every deployment. + RegisterHandlers(r, strict) + RegisterHandlers(r.Group("/api"), strict) + + if opts.StaticDir != "" { + serveStatic(r, opts.StaticDir, logger) + } + + return r +} + +var apiPrefixes = []string{"/health", "/forms", "/submissions", "/api"} + +func isAPIPath(path string) bool { + for _, prefix := range apiPrefixes { + if path == prefix || strings.HasPrefix(path, prefix+"/") { + return true + } + } + return false +} + +// serveStatic serves files from dir and falls back to index.html for client +// side routes so the SPA can be hosted by the backend binary. +func serveStatic(r *gin.Engine, dir string, logger zerolog.Logger) { + index := filepath.Join(dir, "index.html") + r.NoRoute(func(c *gin.Context) { + if isAPIPath(c.Request.URL.Path) { + c.JSON(http.StatusNotFound, gin.H{"error": "not found", "code": "not_found"}) + return + } + clean := filepath.Clean(strings.TrimPrefix(c.Request.URL.Path, "/")) + candidate := filepath.Join(dir, clean) + if clean != "." && !strings.HasPrefix(candidate, filepath.Clean(dir)) { + c.Status(http.StatusNotFound) + return + } + if info, err := os.Stat(candidate); err == nil && !info.IsDir() { + c.File(candidate) + return + } + if _, err := os.Stat(index); err != nil { + logger.Warn().Err(err).Str("dir", dir).Msg("static frontend not found") + c.Status(http.StatusNotFound) + return + } + c.File(index) + }) +} + +func requestIDMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + id := strings.TrimSpace(c.GetHeader("X-Request-ID")) + if id == "" { + id = uuid.NewString() + } + c.Header("X-Request-ID", id) + logging.SetRequestID(c, id) + c.Next() + } +} + +func withRequestMetadata() gin.HandlerFunc { + return func(c *gin.Context) { + meta := requestMetadata{} + if ip := strings.TrimSpace(c.ClientIP()); ip != "" { + meta.ip = &ip + } + if ua := strings.TrimSpace(c.Request.UserAgent()); ua != "" { + meta.userAgent = &ua + } + c.Set(metaKey, meta) + ctx := context.WithValue(c.Request.Context(), metaKey, meta) + c.Request = c.Request.WithContext(ctx) + c.Next() + } +} + +func corsMiddleware(origins []string) gin.HandlerFunc { + allowAll := false + allowed := make(map[string]struct{}, len(origins)) + for _, o := range origins { + o = strings.TrimSpace(o) + if o == "*" { + allowAll = true + continue + } + if o != "" { + allowed[o] = struct{}{} + } + } + + return func(c *gin.Context) { + origin := c.GetHeader("Origin") + if origin != "" { + if allowAll { + c.Header("Access-Control-Allow-Origin", "*") + } else if _, ok := allowed[origin]; ok { + c.Header("Access-Control-Allow-Origin", origin) + c.Header("Vary", "Origin") + } + c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Request-ID") + c.Header("Access-Control-Expose-Headers", "X-Request-ID") + c.Header("Access-Control-Max-Age", "600") + } + + if c.Request.Method == http.MethodOptions { + c.AbortWithStatus(http.StatusNoContent) + return + } + c.Next() + } +} diff --git a/backend/internal/api/server.go b/backend/internal/api/server.go new file mode 100644 index 0000000..c4721d8 --- /dev/null +++ b/backend/internal/api/server.go @@ -0,0 +1,1068 @@ +package api + +import ( + "bytes" + "context" + "crypto/rand" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "math/big" + "net/mail" + "net/url" + "path" + "regexp" + "strings" + "time" + + "github.com/google/uuid" + "github.com/rs/zerolog" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + + "kereru.forms/internal/database" + "kereru.forms/internal/pdf" +) + +// Server implements the generated StrictServerInterface. +type Server struct { + store database.Store + logger zerolog.Logger + version string + startedAt time.Time + tracer trace.Tracer +} + +// NewServer constructs the API implementation. +func NewServer(store database.Store, logger zerolog.Logger, version string) *Server { + return &Server{ + store: store, + logger: logger, + version: version, + startedAt: time.Now(), + tracer: otel.Tracer("kereru.forms/api"), + } +} + +func errJSON(message, code string) ErrorJSONResponse { + e := Error{Error: message} + if code != "" { + c := code + e.Code = &c + } + return ErrorJSONResponse(e) +} + +func (s *Server) GetHealth(ctx context.Context, _ GetHealthRequestObject) (GetHealthResponseObject, error) { + _, span := s.tracer.Start(ctx, "GetHealth") + defer span.End() + + uptime := int64(time.Since(s.startedAt).Seconds()) + return GetHealth200JSONResponse(Health{ + Status: "ok", + Version: s.version, + UptimeSeconds: &uptime, + }), nil +} + +func (s *Server) ListForms(ctx context.Context, req ListFormsRequestObject) (ListFormsResponseObject, error) { + ctx, span := s.tracer.Start(ctx, "ListForms") + defer span.End() + + includeInactive := req.Params.IncludeInactive != nil && *req.Params.IncludeInactive + if req.Params.ActiveOnly != nil && *req.Params.ActiveOnly { + includeInactive = false + } + + forms, err := s.store.ListForms(ctx, includeInactive) + if err != nil { + s.logger.Error().Err(err).Msg("list forms") + return ListForms500JSONResponse{ErrorJSONResponse: errJSON("could not list forms", "internal_error")}, nil + } + + out := make([]FormSummary, 0, len(forms)) + for _, f := range forms { + out = append(out, toAPIFormSummary(f)) + } + return ListForms200JSONResponse(out), nil +} + +func (s *Server) CreateForm(ctx context.Context, req CreateFormRequestObject) (CreateFormResponseObject, error) { + ctx, span := s.tracer.Start(ctx, "CreateForm") + defer span.End() + + if req.Body == nil { + return CreateForm400JSONResponse{ErrorJSONResponse: errJSON("request body is required", "validation_error")}, nil + } + if err := validateForm(req.Body.Title, req.Body.Questions); err != nil { + return CreateForm400JSONResponse{ErrorJSONResponse: errJSON(err.Error(), "validation_error")}, nil + } + if err := validateLogoURL(req.Body.LogoUrl); err != nil { + return CreateForm400JSONResponse{ErrorJSONResponse: errJSON(err.Error(), "validation_error")}, nil + } + if err := validatePDFURL(req.Body.PdfUrl); err != nil { + return CreateForm400JSONResponse{ErrorJSONResponse: errJSON(err.Error(), "validation_error")}, nil + } + externalReferenceRequired := req.Body.ExternalReferenceRequired != nil && *req.Body.ExternalReferenceRequired + externalReferenceLabel, err := normalizeExternalReferenceLabel(req.Body.ExternalReferenceLabel, externalReferenceRequired) + if err != nil { + return CreateForm400JSONResponse{ErrorJSONResponse: errJSON(err.Error(), "validation_error")}, nil + } + + now := time.Now().UTC() + form := &database.Form{ + ID: uuid.NewString(), + FormGroupID: uuid.NewString(), + Version: 1, + Title: strings.TrimSpace(req.Body.Title), + Description: req.Body.Description, + Introduction: req.Body.Introduction, + IsActive: true, + RequiresAccessCode: req.Body.RequiresAccessCode != nil && *req.Body.RequiresAccessCode, + SignatureRequired: req.Body.SignatureRequired != nil && *req.Body.SignatureRequired, + ExternalReferenceRequired: externalReferenceRequired, + ExternalReferenceLabel: externalReferenceLabel, + LogoURL: req.Body.LogoUrl, + PDFURL: req.Body.PdfUrl, + CompanyName: req.Body.CompanyName, + ContactEmail: req.Body.ContactEmail, + ContactPhone: req.Body.ContactPhone, + ContactURL: req.Body.ContactUrl, + CreatedAt: now, + UpdatedAt: now, + Questions: questionInputsToRecords(req.Body.Questions), + } + for i := range form.Questions { + form.Questions[i].ID = uuid.NewString() + form.Questions[i].FormID = form.ID + } + + if err := s.store.CreateForm(ctx, form); err != nil { + if errors.Is(err, database.ErrConflict) { + return CreateForm400JSONResponse{ErrorJSONResponse: errJSON("a form with this key already exists", "conflict")}, nil + } + s.logger.Error().Err(err).Msg("create form") + return CreateForm500JSONResponse(errJSON("could not create form", "internal_error")), nil + } + + span.SetAttributes(attribute.String("form.id", form.ID), attribute.String("form.group", form.FormGroupID)) + return CreateForm201JSONResponse(toAPIForm(form)), nil +} + +func (s *Server) GetForm(ctx context.Context, req GetFormRequestObject) (GetFormResponseObject, error) { + ctx, span := s.tracer.Start(ctx, "GetForm") + defer span.End() + span.SetAttributes(attribute.String("form.id", req.FormId.String())) + + form, err := s.store.GetForm(ctx, req.FormId.String()) + if errors.Is(err, database.ErrNotFound) { + return GetForm404JSONResponse{ErrorJSONResponse: errJSON("form not found", "not_found")}, nil + } + if err != nil { + s.logger.Error().Err(err).Msg("get form") + return GetForm500JSONResponse(errJSON("could not load form", "internal_error")), nil + } + return GetForm200JSONResponse(toAPIForm(form)), nil +} + +func (s *Server) UpdateForm(ctx context.Context, req UpdateFormRequestObject) (UpdateFormResponseObject, error) { + ctx, span := s.tracer.Start(ctx, "UpdateForm") + defer span.End() + span.SetAttributes(attribute.String("form.id", req.FormId.String())) + + if req.Body == nil { + return UpdateForm400JSONResponse{ErrorJSONResponse: errJSON("request body is required", "validation_error")}, nil + } + if err := validateForm(req.Body.Title, req.Body.Questions); err != nil { + return UpdateForm400JSONResponse{ErrorJSONResponse: errJSON(err.Error(), "validation_error")}, nil + } + if err := validateLogoURL(req.Body.LogoUrl); err != nil { + return UpdateForm400JSONResponse{ErrorJSONResponse: errJSON(err.Error(), "validation_error")}, nil + } + if err := validatePDFURL(req.Body.PdfUrl); err != nil { + return UpdateForm400JSONResponse{ErrorJSONResponse: errJSON(err.Error(), "validation_error")}, nil + } + + existing, err := s.store.GetForm(ctx, req.FormId.String()) + if errors.Is(err, database.ErrNotFound) { + return UpdateForm404JSONResponse(errJSON("form not found", "not_found")), nil + } + if err != nil { + s.logger.Error().Err(err).Msg("load form for update") + return UpdateForm500JSONResponse(errJSON("could not load form", "internal_error")), nil + } + + // Editing a definition creates a new version so historical submissions keep + // pointing at the exact definition they answered. + externalReferenceRequired := existing.ExternalReferenceRequired + if req.Body.ExternalReferenceRequired != nil { + externalReferenceRequired = *req.Body.ExternalReferenceRequired + } + externalReferenceLabel := existing.ExternalReferenceLabel + if req.Body.ExternalReferenceLabel != nil { + label, err := normalizeExternalReferenceLabel(req.Body.ExternalReferenceLabel, externalReferenceRequired) + if err != nil { + return UpdateForm400JSONResponse{ErrorJSONResponse: errJSON(err.Error(), "validation_error")}, nil + } + externalReferenceLabel = label + } else if externalReferenceRequired && (externalReferenceLabel == nil || *externalReferenceLabel == "") { + label := defaultExternalReferenceLabel + externalReferenceLabel = &label + } + now := time.Now().UTC() + form := &database.Form{ + ID: uuid.NewString(), + FormGroupID: existing.FormGroupID, + Version: existing.Version + 1, + Title: strings.TrimSpace(req.Body.Title), + Description: req.Body.Description, + Introduction: req.Body.Introduction, + IsActive: true, + RequiresAccessCode: existing.RequiresAccessCode, + SignatureRequired: existing.SignatureRequired, + ExternalReferenceRequired: externalReferenceRequired, + ExternalReferenceLabel: externalReferenceLabel, + LogoURL: req.Body.LogoUrl, + PDFURL: req.Body.PdfUrl, + CompanyName: req.Body.CompanyName, + ContactEmail: req.Body.ContactEmail, + ContactPhone: req.Body.ContactPhone, + ContactURL: req.Body.ContactUrl, + CreatedAt: now, + UpdatedAt: now, + Questions: questionInputsToRecords(req.Body.Questions), + } + if req.Body.RequiresAccessCode != nil { + form.RequiresAccessCode = *req.Body.RequiresAccessCode + } + if req.Body.SignatureRequired != nil { + form.SignatureRequired = *req.Body.SignatureRequired + } + for i := range form.Questions { + form.Questions[i].ID = uuid.NewString() + form.Questions[i].FormID = form.ID + } + + if err := s.store.CreateForm(ctx, form); err != nil { + if errors.Is(err, database.ErrConflict) { + return UpdateForm400JSONResponse{ErrorJSONResponse: errJSON("a newer version of this form already exists", "conflict")}, nil + } + s.logger.Error().Err(err).Msg("update form") + return UpdateForm500JSONResponse(errJSON("could not update form", "internal_error")), nil + } + if err := s.store.SetFormActive(ctx, existing.ID, false); err != nil { + s.logger.Warn().Err(err).Str("form.id", existing.ID).Msg("could not inactivate previous form version") + } + span.SetAttributes(attribute.Int("form.version", form.Version)) + return UpdateForm200JSONResponse(toAPIForm(form)), nil +} + +func (s *Server) DeleteForm(ctx context.Context, req DeleteFormRequestObject) (DeleteFormResponseObject, error) { + ctx, span := s.tracer.Start(ctx, "DeleteForm") + defer span.End() + span.SetAttributes(attribute.String("form.id", req.FormId.String())) + + err := s.store.DeleteForm(ctx, req.FormId.String()) + if errors.Is(err, database.ErrNotFound) { + return DeleteForm404JSONResponse{ErrorJSONResponse: errJSON("form not found", "not_found")}, nil + } + if err != nil { + s.logger.Error().Err(err).Msg("delete form") + return DeleteForm500JSONResponse(errJSON("could not delete form", "internal_error")), nil + } + return DeleteForm204Response{}, nil +} + +func (s *Server) InactivateForm(ctx context.Context, req InactivateFormRequestObject) (InactivateFormResponseObject, error) { + form, err := s.setFormActive(ctx, req.FormId.String(), false) + if errors.Is(err, database.ErrNotFound) { + return InactivateForm404JSONResponse{ErrorJSONResponse: errJSON("form not found", "not_found")}, nil + } + if err != nil { + return InactivateForm500JSONResponse(errJSON(err.Error(), "internal_error")), nil + } + return InactivateForm200JSONResponse(toAPIForm(form)), nil +} + +func (s *Server) ActivateForm(ctx context.Context, req ActivateFormRequestObject) (ActivateFormResponseObject, error) { + form, err := s.setFormActive(ctx, req.FormId.String(), true) + if errors.Is(err, database.ErrNotFound) { + return ActivateForm404JSONResponse{ErrorJSONResponse: errJSON("form not found", "not_found")}, nil + } + if err != nil { + return ActivateForm500JSONResponse(errJSON(err.Error(), "internal_error")), nil + } + return ActivateForm200JSONResponse(toAPIForm(form)), nil +} + +func (s *Server) setFormActive(ctx context.Context, id string, active bool) (*database.Form, error) { + ctx, span := s.tracer.Start(ctx, "SetFormActive") + defer span.End() + span.SetAttributes(attribute.String("form.id", id), attribute.Bool("form.active", active)) + + if err := s.store.SetFormActive(ctx, id, active); err != nil { + if !errors.Is(err, database.ErrNotFound) { + s.logger.Error().Err(err).Bool("active", active).Msg("set form active") + } + return nil, err + } + + form, err := s.store.GetForm(ctx, id) + if err != nil { + s.logger.Error().Err(err).Msg("load form after activation change") + return nil, err + } + return form, nil +} + +func (s *Server) CreateFormVersion(ctx context.Context, req CreateFormVersionRequestObject) (CreateFormVersionResponseObject, error) { + ctx, span := s.tracer.Start(ctx, "CreateFormVersion") + defer span.End() + span.SetAttributes(attribute.String("form.id", req.FormId.String())) + + existing, err := s.store.GetForm(ctx, req.FormId.String()) + if errors.Is(err, database.ErrNotFound) { + return CreateFormVersion404JSONResponse{ErrorJSONResponse: errJSON("form not found", "not_found")}, nil + } + if err != nil { + s.logger.Error().Err(err).Msg("load form for new version") + return CreateFormVersion500JSONResponse(errJSON("could not load form", "internal_error")), nil + } + + now := time.Now().UTC() + version := &database.Form{ + ID: uuid.NewString(), + FormGroupID: existing.FormGroupID, + Version: existing.Version + 1, + Title: existing.Title, + Description: existing.Description, + Introduction: existing.Introduction, + IsActive: true, + RequiresAccessCode: existing.RequiresAccessCode, + SignatureRequired: existing.SignatureRequired, + ExternalReferenceRequired: existing.ExternalReferenceRequired, + ExternalReferenceLabel: existing.ExternalReferenceLabel, + LogoURL: existing.LogoURL, + CompanyName: existing.CompanyName, + ContactEmail: existing.ContactEmail, + ContactPhone: existing.ContactPhone, + ContactURL: existing.ContactURL, + CreatedAt: now, + UpdatedAt: now, + Questions: make([]database.Question, 0, len(existing.Questions)), + } + for _, q := range existing.Questions { + clone := q + clone.ID = uuid.NewString() + clone.FormID = version.ID + version.Questions = append(version.Questions, clone) + } + + if err := s.store.CreateForm(ctx, version); err != nil { + s.logger.Error().Err(err).Msg("create form version") + return CreateFormVersion500JSONResponse(errJSON("could not create form version", "internal_error")), nil + } + if err := s.store.SetFormActive(ctx, existing.ID, false); err != nil { + s.logger.Warn().Err(err).Str("form.id", existing.ID).Msg("could not inactivate previous form version") + } + return CreateFormVersion201JSONResponse(toAPIForm(version)), nil +} + +func (s *Server) GetFormPdf(ctx context.Context, req GetFormPdfRequestObject) (GetFormPdfResponseObject, error) { + ctx, span := s.tracer.Start(ctx, "GetFormPdf") + defer span.End() + span.SetAttributes(attribute.String("form.id", req.FormId.String())) + + form, err := s.store.GetForm(ctx, req.FormId.String()) + if errors.Is(err, database.ErrNotFound) { + return GetFormPdf404JSONResponse{ErrorJSONResponse: errJSON("form not found", "not_found")}, nil + } + if err != nil { + s.logger.Error().Err(err).Msg("load form for pdf") + return GetFormPdf500JSONResponse(errJSON("could not load form", "internal_error")), nil + } + + data, err := pdf.Generate(form) + if err != nil { + s.logger.Error().Err(err).Msg("generate pdf") + return GetFormPdf500JSONResponse(errJSON("could not generate pdf", "internal_error")), nil + } + return GetFormPdf200ApplicationpdfResponse{ + Body: bytes.NewReader(data), + ContentLength: int64(len(data)), + }, nil +} + +func (s *Server) ListFormSubmissions(ctx context.Context, req ListFormSubmissionsRequestObject) (ListFormSubmissionsResponseObject, error) { + ctx, span := s.tracer.Start(ctx, "ListFormSubmissions") + defer span.End() + span.SetAttributes(attribute.String("form.id", req.FormId.String())) + + if _, err := s.store.GetForm(ctx, req.FormId.String()); err != nil { + if errors.Is(err, database.ErrNotFound) { + return ListFormSubmissions404JSONResponse{ErrorJSONResponse: errJSON("form not found", "not_found")}, nil + } + s.logger.Error().Err(err).Msg("load form for submissions") + return ListFormSubmissions500JSONResponse(errJSON("could not load form", "internal_error")), nil + } + + subs, err := s.store.ListSubmissions(ctx, req.FormId.String()) + if err != nil { + s.logger.Error().Err(err).Msg("list submissions") + return ListFormSubmissions500JSONResponse(errJSON("could not list submissions", "internal_error")), nil + } + out := make([]SubmissionSummary, 0, len(subs)) + for _, sub := range subs { + out = append(out, toAPISubmissionSummary(sub)) + } + return ListFormSubmissions200JSONResponse(out), nil +} + +func (s *Server) SubmitForm(ctx context.Context, req SubmitFormRequestObject) (SubmitFormResponseObject, error) { + ctx, span := s.tracer.Start(ctx, "SubmitForm") + defer span.End() + span.SetAttributes(attribute.String("form.id", req.FormId.String())) + + form, err := s.store.GetForm(ctx, req.FormId.String()) + if errors.Is(err, database.ErrNotFound) { + return SubmitForm404JSONResponse(errJSON("form not found", "not_found")), nil + } + if err != nil { + s.logger.Error().Err(err).Msg("load form for submission") + return SubmitForm500JSONResponse(errJSON("could not load form", "internal_error")), nil + } + if !form.IsActive { + return SubmitForm410JSONResponse(errJSON("this form is no longer accepting responses", "form_inactive")), nil + } + if req.Body == nil { + return SubmitForm400JSONResponse{ErrorJSONResponse: errJSON("request body is required", "validation_error")}, nil + } + + userIdType := UserIdType(strings.ToLower(strings.TrimSpace(string(req.Body.UserIdType)))) + if !validUserIdType(userIdType) { + return SubmitForm400JSONResponse{ErrorJSONResponse: errJSON("userIdType must be one of email, username, phone or guid", "validation_error")}, nil + } + userId := strings.TrimSpace(req.Body.UserId) + if userId == "" { + return SubmitForm400JSONResponse{ErrorJSONResponse: errJSON("userId is required", "validation_error")}, nil + } + if userIdType == Email { + if _, err := mail.ParseAddress(userId); err != nil { + return SubmitForm400JSONResponse{ErrorJSONResponse: errJSON("userId is not a valid email address", "validation_error")}, nil + } + } + + externalReference := strings.TrimSpace(derefString(req.Body.ExternalReference)) + if form.ExternalReferenceRequired && externalReference == "" { + label := defaultExternalReferenceLabel + if form.ExternalReferenceLabel != nil && *form.ExternalReferenceLabel != "" { + label = *form.ExternalReferenceLabel + } + return SubmitForm400JSONResponse{ErrorJSONResponse: errJSON(label+" is required", "validation_error")}, nil + } + if len(externalReference) > maxExternalReferenceLength { + return SubmitForm400JSONResponse{ErrorJSONResponse: errJSON(fmt.Sprintf("externalReference must be %d characters or fewer", maxExternalReferenceLength), "validation_error")}, nil + } + var externalReferencePtr *string + if externalReference != "" { + externalReferencePtr = &externalReference + } + + answers, err := validateAnswers(form, req.Body.Answers, req.Body.OfflinePdf == nil) + if err != nil { + return SubmitForm400JSONResponse{ErrorJSONResponse: errJSON(err.Error(), "validation_error")}, nil + } + + signature, offlinePDF, offlinePDFName, err := validateSignatureAndOfflinePDF(form, req.Body) + if err != nil { + return SubmitForm400JSONResponse{ErrorJSONResponse: errJSON(err.Error(), "validation_error")}, nil + } + + var accessCode string + if form.RequiresAccessCode { + accessCode = strings.ToUpper(strings.TrimSpace(derefString(req.Body.AccessCode))) + if accessCode == "" { + return SubmitForm400JSONResponse{ErrorJSONResponse: errJSON("an access code is required for this form", "access_code_required")}, nil + } + } + + meta := requestMeta(ctx) + now := time.Now().UTC() + submission := &database.Submission{ + ID: uuid.NewString(), + FormID: form.ID, + FormVersion: form.Version, + UserID: userId, + UserIDType: string(userIdType), + SubmittedAt: now, + IPAddress: meta.ip, + UserAgent: meta.userAgent, + ExternalReference: externalReferencePtr, + Signature: signature, + OfflinePDFName: offlinePDFName, + OfflinePDF: offlinePDF, + Answers: answers, + } + if req.Body.Tracking != nil { + submission.Locale = req.Body.Tracking.Locale + submission.Timezone = req.Body.Tracking.Timezone + submission.Place = req.Body.Tracking.Place + submission.Referer = req.Body.Tracking.Referer + } + + if err := s.store.CreateSubmission(ctx, submission, accessCode); err != nil { + switch { + case errors.Is(err, database.ErrAccessCode): + return SubmitForm409JSONResponse(errJSON("the access code is not valid for this form", "invalid_access_code")), nil + case errors.Is(err, database.ErrCodeAlreadyIn): + return SubmitForm409JSONResponse(errJSON("the access code has already been used", "access_code_used")), nil + default: + s.logger.Error().Err(err).Msg("create submission") + return SubmitForm500JSONResponse(errJSON("could not save submission", "internal_error")), nil + } + } + + span.SetAttributes(attribute.String("submission.id", submission.ID)) + return SubmitForm201JSONResponse(toAPISubmission(submission)), nil +} + +func (s *Server) GetSubmission(ctx context.Context, req GetSubmissionRequestObject) (GetSubmissionResponseObject, error) { + ctx, span := s.tracer.Start(ctx, "GetSubmission") + defer span.End() + span.SetAttributes(attribute.String("submission.id", req.SubmissionId.String())) + + sub, err := s.store.GetSubmission(ctx, req.SubmissionId.String()) + if errors.Is(err, database.ErrNotFound) { + return GetSubmission404JSONResponse{ErrorJSONResponse: errJSON("submission not found", "not_found")}, nil + } + if err != nil { + s.logger.Error().Err(err).Msg("get submission") + return GetSubmission500JSONResponse(errJSON("could not load submission", "internal_error")), nil + } + return GetSubmission200JSONResponse(toAPISubmission(sub)), nil +} + +func (s *Server) GetSubmissionOfflinePdf(ctx context.Context, req GetSubmissionOfflinePdfRequestObject) (GetSubmissionOfflinePdfResponseObject, error) { + headerCtx := ctx + ctx, span := s.tracer.Start(ctx, "GetSubmissionOfflinePdf") + defer span.End() + span.SetAttributes(attribute.String("submission.id", req.SubmissionId.String())) + + data, name, err := s.store.GetSubmissionOfflinePDF(ctx, req.SubmissionId.String()) + if errors.Is(err, database.ErrNotFound) { + return GetSubmissionOfflinePdf404JSONResponse{ErrorJSONResponse: errJSON("offline PDF not found", "not_found")}, nil + } + if err != nil { + s.logger.Error().Err(err).Msg("get submission offline pdf") + return GetSubmissionOfflinePdf500JSONResponse(errJSON("could not load offline PDF", "internal_error")), nil + } + if c, ok := headerCtx.(interface{ Header(string, string) }); ok { + c.Header("Content-Disposition", attachmentDisposition(name)) + } + return GetSubmissionOfflinePdf200ApplicationpdfResponse{ + Body: bytes.NewReader(data), + ContentLength: int64(len(data)), + }, nil +} + +func attachmentDisposition(name string) string { + clean := strings.Map(func(r rune) rune { + if r < ' ' || r == 127 || r == '"' || r == '\\' { + return '-' + } + return r + }, name) + if strings.TrimSpace(clean) == "" { + clean = "submission.pdf" + } + return fmt.Sprintf("attachment; filename=%q", clean) +} + +func (s *Server) ListAccessCodes(ctx context.Context, req ListAccessCodesRequestObject) (ListAccessCodesResponseObject, error) { + ctx, span := s.tracer.Start(ctx, "ListAccessCodes") + defer span.End() + span.SetAttributes(attribute.String("form.id", req.FormId.String())) + + if _, err := s.store.GetForm(ctx, req.FormId.String()); err != nil { + if errors.Is(err, database.ErrNotFound) { + return ListAccessCodes404JSONResponse{ErrorJSONResponse: errJSON("form not found", "not_found")}, nil + } + s.logger.Error().Err(err).Msg("load form for access codes") + return ListAccessCodes500JSONResponse(errJSON("could not load form", "internal_error")), nil + } + + codes, err := s.store.ListAccessCodes(ctx, req.FormId.String()) + if err != nil { + s.logger.Error().Err(err).Msg("list access codes") + return ListAccessCodes500JSONResponse(errJSON("could not list access codes", "internal_error")), nil + } + out := make([]AccessCode, 0, len(codes)) + for _, c := range codes { + out = append(out, toAPIAccessCode(c)) + } + return ListAccessCodes200JSONResponse(out), nil +} + +func (s *Server) CreateAccessCodes(ctx context.Context, req CreateAccessCodesRequestObject) (CreateAccessCodesResponseObject, error) { + ctx, span := s.tracer.Start(ctx, "CreateAccessCodes") + defer span.End() + span.SetAttributes(attribute.String("form.id", req.FormId.String())) + + if req.Body == nil { + return CreateAccessCodes400JSONResponse{ErrorJSONResponse: errJSON("request body is required", "validation_error")}, nil + } + if req.Body.Count < 1 || req.Body.Count > 1000 { + return CreateAccessCodes400JSONResponse{ErrorJSONResponse: errJSON("count must be between 1 and 1000", "validation_error")}, nil + } + if _, err := s.store.GetForm(ctx, req.FormId.String()); err != nil { + if errors.Is(err, database.ErrNotFound) { + return CreateAccessCodes404JSONResponse(errJSON("form not found", "not_found")), nil + } + s.logger.Error().Err(err).Msg("load form for access code creation") + return CreateAccessCodes500JSONResponse(errJSON("could not load form", "internal_error")), nil + } + + prefix := "" + if req.Body.Prefix != nil { + prefix = strings.ToUpper(strings.TrimSpace(*req.Body.Prefix)) + if prefix != "" && !codePrefixPattern.MatchString(prefix) { + return CreateAccessCodes400JSONResponse{ErrorJSONResponse: errJSON("prefix may only contain letters, digits and hyphens", "validation_error")}, nil + } + } + + now := time.Now().UTC() + codes := make([]database.AccessCode, 0, req.Body.Count) + for i := 0; i < req.Body.Count; i++ { + code, err := generateCode(prefix) + if err != nil { + s.logger.Error().Err(err).Msg("generate access code") + return CreateAccessCodes500JSONResponse(errJSON("could not generate access codes", "internal_error")), nil + } + codes = append(codes, database.AccessCode{ + ID: uuid.NewString(), + FormID: req.FormId.String(), + Code: code, + CreatedAt: now, + }) + } + + if err := s.store.CreateAccessCodes(ctx, codes); err != nil { + s.logger.Error().Err(err).Msg("create access codes") + return CreateAccessCodes500JSONResponse(errJSON("could not create access codes", "internal_error")), nil + } + out := make([]AccessCode, 0, len(codes)) + for _, c := range codes { + out = append(out, toAPIAccessCode(c)) + } + return CreateAccessCodes201JSONResponse(out), nil +} + +var ( + questionKeyPattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_-]{0,63}$`) + codePrefixPattern = regexp.MustCompile(`^[A-Z0-9-]{1,16}$`) + codeAlphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" +) + +func generateCode(prefix string) (string, error) { + var b strings.Builder + for i := 0; i < 8; i++ { + if i == 4 { + b.WriteByte('-') + } + n, err := rand.Int(rand.Reader, big.NewInt(int64(len(codeAlphabet)))) + if err != nil { + return "", err + } + b.WriteByte(codeAlphabet[n.Int64()]) + } + code := b.String() + if prefix != "" { + code = prefix + "-" + code + } + return code, nil +} + +func validateLogoURL(raw *string) error { + if raw == nil || strings.TrimSpace(*raw) == "" { + return nil + } + u, err := url.Parse(strings.TrimSpace(*raw)) + if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" { + return errors.New("logoUrl must be an absolute http or https URL") + } + return nil +} + +func validatePDFURL(raw *string) error { + if raw == nil || strings.TrimSpace(*raw) == "" { + return nil + } + u, err := url.Parse(strings.TrimSpace(*raw)) + if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" { + return errors.New("pdfUrl must be an absolute http or https URL") + } + return nil +} + +const ( + maxSignatureBytes = 2 << 20 + maxOfflinePDFSize = 10 << 20 +) + +const ( + defaultExternalReferenceLabel = "External reference" + maxExternalReferenceLabel = 100 + maxExternalReferenceLength = 200 +) + +func normalizeExternalReferenceLabel(raw *string, required bool) (*string, error) { + label := strings.TrimSpace(derefString(raw)) + if len(label) > maxExternalReferenceLabel { + return nil, fmt.Errorf("externalReferenceLabel must be %d characters or fewer", maxExternalReferenceLabel) + } + if label == "" && required { + label = defaultExternalReferenceLabel + } + if label == "" { + return nil, nil + } + return &label, nil +} + +func validateSignatureAndOfflinePDF(form *database.Form, req *SubmitFormRequest) (*database.Signature, []byte, *string, error) { + var ( + offline []byte + offlineName *string + ) + if req.OfflinePdf != nil { + data, err := decodeDataURL(*req.OfflinePdf, "application/pdf", maxOfflinePDFSize) + if err != nil { + return nil, nil, nil, fmt.Errorf("offlinePdf: %w", err) + } + if !bytes.HasPrefix(data, []byte("%PDF")) { + return nil, nil, nil, errors.New("offlinePdf: file does not look like a PDF") + } + name := "offline-submission.pdf" + if req.OfflinePdfName != nil { + if trimmed := strings.TrimSpace(*req.OfflinePdfName); trimmed != "" { + name = path.Base(strings.ReplaceAll(trimmed, "\\", "/")) + } + } + if len(name) > 255 { + return nil, nil, nil, errors.New("offlinePdfName must be 255 characters or fewer") + } + offline = data + offlineName = &name + } + + var signature *database.Signature + if req.Signature != nil { + kind := strings.ToLower(strings.TrimSpace(string(req.Signature.Type))) + if kind != "drawn" && kind != "image" { + return nil, nil, nil, errors.New("signature.type must be drawn or image") + } + _, mediaType, err := decodeSignatureImage(req.Signature.DataUrl) + if err != nil { + return nil, nil, nil, fmt.Errorf("signature.dataUrl: %w", err) + } + if kind == "drawn" && mediaType != "image/png" { + return nil, nil, nil, errors.New("signature.dataUrl must be a PNG for drawn signatures") + } + name := strings.TrimSpace(req.Signature.Name) + if name == "" { + return nil, nil, nil, errors.New("signature.name is required") + } + if len(name) > 200 { + return nil, nil, nil, errors.New("signature.name must be 200 characters or fewer") + } + var title *string + if req.Signature.Title != nil { + trimmed := strings.TrimSpace(*req.Signature.Title) + if len(trimmed) > 200 { + return nil, nil, nil, errors.New("signature.title must be 200 characters or fewer") + } + if trimmed != "" { + title = &trimmed + } + } + signature = &database.Signature{ + Type: kind, + DataURL: req.Signature.DataUrl, + Name: name, + Title: title, + SignedAt: time.Now().UTC(), + } + } else if form.SignatureRequired && len(offline) == 0 { + return nil, nil, nil, errors.New("a signature is required for this form") + } + + return signature, offline, offlineName, nil +} + +func decodeSignatureImage(raw string) ([]byte, string, error) { + data, mediaType, err := decodeDataURLWithMedia(raw, maxSignatureBytes) + if err != nil { + return nil, "", err + } + switch mediaType { + case "image/png": + if !bytes.HasPrefix(data, []byte{0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a}) { + return nil, "", errors.New("file does not look like a PNG image") + } + case "image/jpeg": + if !bytes.HasPrefix(data, []byte{0xff, 0xd8, 0xff}) { + return nil, "", errors.New("file does not look like a JPEG image") + } + default: + return nil, "", errors.New("must be a PNG or JPEG data URL") + } + return data, mediaType, nil +} + +func decodeDataURL(raw, expectedMedia string, maxBytes int) ([]byte, error) { + data, mediaType, err := decodeDataURLWithMedia(raw, maxBytes) + if err != nil { + return nil, err + } + if mediaType != expectedMedia { + return nil, fmt.Errorf("must be a %s data URL", expectedMedia) + } + return data, nil +} + +func decodeDataURLWithMedia(raw string, maxBytes int) ([]byte, string, error) { + if !strings.HasPrefix(raw, "data:") { + return nil, "", errors.New("must be a base64 data URL") + } + header, payload, ok := strings.Cut(raw[len("data:"):], ",") + if !ok { + return nil, "", errors.New("must be a base64 data URL") + } + mediaType, encoding, ok := strings.Cut(header, ";") + if !ok || !strings.EqualFold(strings.TrimSpace(encoding), "base64") { + return nil, "", errors.New("must be a base64 data URL") + } + mediaType = strings.ToLower(strings.TrimSpace(mediaType)) + data, err := base64.StdEncoding.DecodeString(payload) + if err != nil { + return nil, "", errors.New("contains invalid base64 data") + } + if len(data) == 0 { + return nil, "", errors.New("file is empty") + } + if len(data) > maxBytes { + return nil, "", fmt.Errorf("file exceeds the %d MB limit", maxBytes/(1<<20)) + } + return data, mediaType, nil +} + +func validateForm(title string, questions []QuestionInput) error { + if strings.TrimSpace(title) == "" { + return errors.New("title is required") + } + seenKeys := make(map[string]struct{}, len(questions)) + for i, q := range questions { + label := fmt.Sprintf("question %d", i+1) + key := strings.TrimSpace(q.Key) + if key == "" { + return fmt.Errorf("%s: key is required", label) + } + if !questionKeyPattern.MatchString(key) { + return fmt.Errorf("%s: key must start with a letter and contain only letters, digits, underscores or hyphens", label) + } + if _, exists := seenKeys[key]; exists { + return fmt.Errorf("%s: duplicate key %q", label, key) + } + seenKeys[key] = struct{}{} + + if strings.TrimSpace(q.Label) == "" { + return fmt.Errorf("%s: label is required", label) + } + switch q.Type { + case Boolean, Number, Select, Text, Date: + case "": + return fmt.Errorf("%s: type is required", label) + default: + return fmt.Errorf("%s: unsupported type %q", label, q.Type) + } + if q.Type == Select { + if q.Options == nil || len(*q.Options) == 0 { + return fmt.Errorf("%s: select questions require at least one option", label) + } + seenValues := make(map[string]struct{}, len(*q.Options)) + for _, opt := range *q.Options { + value := strings.TrimSpace(opt.Value) + if value == "" { + return fmt.Errorf("%s: option value is required", label) + } + if strings.TrimSpace(opt.Label) == "" { + return fmt.Errorf("%s: option label is required", label) + } + if _, exists := seenValues[value]; exists { + return fmt.Errorf("%s: duplicate option value %q", label, value) + } + seenValues[value] = struct{}{} + } + } + if q.Type == Number && q.MinValue != nil && q.MaxValue != nil && *q.MinValue > *q.MaxValue { + return fmt.Errorf("%s: minValue must be less than or equal to maxValue", label) + } + } + return nil +} + +func validateAnswers(form *database.Form, answers []Answer, requireRequired bool) ([]database.Answer, error) { + byID := make(map[string]database.Question, len(form.Questions)) + for _, q := range form.Questions { + byID[q.ID] = q + } + + seen := make(map[string]struct{}, len(answers)) + out := make([]database.Answer, 0, len(answers)) + for _, ans := range answers { + id := ans.QuestionId.String() + q, ok := byID[id] + if !ok { + return nil, fmt.Errorf("question %s does not belong to this form", id) + } + if _, dup := seen[id]; dup { + return nil, fmt.Errorf("duplicate answer for question %q", q.Key) + } + seen[id] = struct{}{} + + if err := validateAnswerValue(q, ans.Value); err != nil { + return nil, err + } + raw, err := json.Marshal(ans.Value) + if err != nil { + return nil, fmt.Errorf("question %q: could not encode answer", q.Key) + } + out = append(out, database.Answer{ + QuestionID: id, + QuestionKey: q.Key, + Value: raw, + }) + } + + for _, q := range form.Questions { + if !requireRequired || !q.Required { + continue + } + if _, ok := seen[q.ID]; !ok { + return nil, fmt.Errorf("question %q requires an answer", q.Key) + } + } + return out, nil +} + +func validateAnswerValue(q database.Question, value interface{}) error { + if value == nil { + return fmt.Errorf("question %q: answer value is required", q.Key) + } + switch q.Type { + case "boolean": + if _, ok := value.(bool); !ok { + return fmt.Errorf("question %q: expected a true/false value", q.Key) + } + case "number": + num, ok := toFloat(value) + if !ok { + return fmt.Errorf("question %q: expected a numeric value", q.Key) + } + if q.MinValue != nil && num < *q.MinValue { + return fmt.Errorf("question %q: value must be at least %g", q.Key, *q.MinValue) + } + if q.MaxValue != nil && num > *q.MaxValue { + return fmt.Errorf("question %q: value must be at most %g", q.Key, *q.MaxValue) + } + case "select": + str, ok := value.(string) + if !ok { + return fmt.Errorf("question %q: expected one of the configured options", q.Key) + } + found := false + for _, opt := range q.Options { + if opt.Value == str { + found = true + break + } + } + if !found { + return fmt.Errorf("question %q: %q is not a configured option", q.Key, str) + } + case "date": + str, ok := value.(string) + if !ok { + return fmt.Errorf("question %q: expected a date in YYYY-MM-DD form", q.Key) + } + date, err := time.Parse("2006-01-02", strings.TrimSpace(str)) + if err != nil || date.Format("2006-01-02") != strings.TrimSpace(str) { + return fmt.Errorf("question %q: expected a date in YYYY-MM-DD form", q.Key) + } + case "text": + str, ok := value.(string) + if !ok { + return fmt.Errorf("question %q: expected a text value", q.Key) + } + if len(str) > 10000 { + return fmt.Errorf("question %q: answer is too long", q.Key) + } + if !q.Multiline && strings.ContainsAny(str, "\r\n") { + return fmt.Errorf("question %q: this answer must be a single line", q.Key) + } + default: + return fmt.Errorf("question %q: unsupported type %q", q.Key, q.Type) + } + return nil +} + +func toFloat(v interface{}) (float64, bool) { + switch n := v.(type) { + case float64: + return n, true + case float32: + return float64(n), true + case int: + return float64(n), true + case int64: + return float64(n), true + case json.Number: + f, err := n.Float64() + return f, err == nil + default: + return 0, false + } +} + +func validUserIdType(t UserIdType) bool { + switch t { + case Email, Username, Phone, Guid: + return true + default: + return false + } +} + +func derefString(v *string) string { + if v == nil { + return "" + } + return *v +} + +const metaKey = "kereru.request_meta" + +type requestMetadata struct { + ip *string + userAgent *string +} + +// requestMeta returns the server-side captured request metadata. +func requestMeta(ctx context.Context) requestMetadata { + if v, ok := ctx.Value(metaKey).(requestMetadata); ok { + return v + } + return requestMetadata{} +} diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go new file mode 100644 index 0000000..cd34550 --- /dev/null +++ b/backend/internal/config/config.go @@ -0,0 +1,279 @@ +// Package config loads application configuration from defaults, a YAML/JSON +// document, environment variables (optionally via a .env file) and CLI flags. +// +// Precedence, highest first: +// 1. CLI flags (--conf, --log-level, --listen-addr, --listen-addr6, --port) +// 2. Environment variables +// 3. .env file located next to the executable or in the working directory +// 4. Configuration file supplied with --conf +// 5. Built-in defaults +package config + +import ( + "errors" + "flag" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + + "gopkg.in/yaml.v3" +) + +// Config holds every runtime setting supported by kereru-forms. +type Config struct { + // LogLevel is the minimum log level (trace, debug, info, warn, error). + LogLevel string `yaml:"logLevel" json:"logLevel"` + // LogFormat is "json" or "console". + LogFormat string `yaml:"logFormat" json:"logFormat"` + // Environment is a free-form deployment label (development, staging, production). + Environment string `yaml:"environment" json:"environment"` + + // HTTPAddr is the IPv4 address to listen on. Empty disables that listener. + HTTPAddr string `yaml:"listenAddr" json:"listenAddr"` + // HTTPAddr6 is the IPv6 address to listen on. Empty disables that listener. + HTTPAddr6 string `yaml:"listenAddr6" json:"listenAddr6"` + // Port is the TCP port shared by both listeners. + Port string `yaml:"port" json:"port"` + + // DBDriver selects the storage backend. Currently "sqlite" is implemented. + DBDriver string `yaml:"dbDriver" json:"dbDriver"` + // DBDSN is the driver-specific connection string. + DBDSN string `yaml:"dbDsn" json:"dbDsn"` + + // TelemetryEnabled turns on OpenTelemetry tracing/metrics export. + TelemetryEnabled bool `yaml:"telemetryEnabled" json:"telemetryEnabled"` + // OTLPEndpoint is the OTLP/HTTP collector endpoint (host:port or URL). + OTLPEndpoint string `yaml:"otlpEndpoint" json:"otlpEndpoint"` + + // CORSOrigins is the list of allowed browser origins ("*" allows all). + CORSOrigins []string `yaml:"corsOrigins" json:"corsOrigins"` + + // StaticDir optionally serves a built frontend from this directory. + StaticDir string `yaml:"staticDir" json:"staticDir"` +} + +// Default returns the built-in configuration. +func Default() *Config { + return &Config{ + LogLevel: "info", + LogFormat: "console", + Environment: "development", + HTTPAddr: "0.0.0.0", + HTTPAddr6: "", + Port: "8080", + DBDriver: "sqlite", + DBDSN: "file:kereru.db?_pragma=busy_timeout(5000)&_pragma=foreign_keys(1)", + TelemetryEnabled: false, + OTLPEndpoint: "localhost:4318", + CORSOrigins: []string{"http://localhost:5173"}, + StaticDir: "", + } +} + +// Load builds the configuration from args (usually os.Args[1:]). +func Load(args []string) (*Config, error) { + cfg := Default() + + dotEnvPath := LoadDotEnv() + _ = dotEnvPath + + confPath := scanConf(args) + if confPath != "" { + if err := loadFile(cfg, confPath); err != nil { + return nil, err + } + } + + applyEnv(cfg) + + fs := flag.NewFlagSet("kereru-forms", flag.ContinueOnError) + fs.SetOutput(os.Stderr) + + var ( + flagConf = fs.String("conf", confPath, "path to a YAML or JSON configuration file") + flagLevel = fs.String("log-level", cfg.LogLevel, "log level: trace, debug, info, warn, error") + flagFormat = fs.String("log-format", cfg.LogFormat, "log format: console or json") + flagAddr = fs.String("listen-addr", cfg.HTTPAddr, "IPv4 listen address (empty to disable)") + flagAddr6 = fs.String("listen-addr6", cfg.HTTPAddr6, "IPv6 listen address (empty to disable)") + flagPort = fs.String("port", cfg.Port, "TCP listen port") + flagDB = fs.String("db-dsn", cfg.DBDSN, "database connection string") + flagDriver = fs.String("db-driver", cfg.DBDriver, "database driver (sqlite)") + flagStatic = fs.String("static-dir", cfg.StaticDir, "directory containing a built frontend (optional)") + ) + fs.Usage = func() { + fmt.Fprintf(fs.Output(), "Usage: kereru-forms [flags]\n\nFlags:\n") + fs.PrintDefaults() + } + + if err := fs.Parse(args); err != nil { + return nil, err + } + _ = flagConf + + cfg.LogLevel = *flagLevel + cfg.LogFormat = *flagFormat + cfg.HTTPAddr = *flagAddr + cfg.HTTPAddr6 = *flagAddr6 + cfg.Port = *flagPort + cfg.DBDSN = *flagDB + cfg.DBDriver = *flagDriver + cfg.StaticDir = *flagStatic + + if err := cfg.Validate(); err != nil { + return nil, err + } + return cfg, nil +} + +// Validate checks that the configuration is usable. +func (c *Config) Validate() error { + if strings.TrimSpace(c.Port) == "" { + return errors.New("port must not be empty") + } + if _, err := strconv.Atoi(c.Port); err != nil { + return fmt.Errorf("port %q is not a number", c.Port) + } + if c.HTTPAddr == "" && c.HTTPAddr6 == "" { + return errors.New("at least one of listenAddr or listenAddr6 must be set") + } + if strings.TrimSpace(c.DBDriver) == "" { + return errors.New("dbDriver must not be empty") + } + if strings.TrimSpace(c.DBDSN) == "" { + return errors.New("dbDsn must not be empty") + } + return nil +} + +// Addresses returns the listen addresses (host:port) for enabled listeners. +func (c *Config) Addresses() []string { + var addrs []string + if c.HTTPAddr != "" { + addrs = append(addrs, joinHostPort(c.HTTPAddr, c.Port)) + } + if c.HTTPAddr6 != "" { + addrs = append(addrs, joinHostPort(c.HTTPAddr6, c.Port)) + } + return addrs +} + +func joinHostPort(host, port string) string { + if strings.Contains(host, ":") && !strings.HasPrefix(host, "[") { + return "[" + host + "]:" + port + } + return host + ":" + port +} + +func scanConf(args []string) string { + for i := 0; i < len(args); i++ { + arg := args[i] + if arg == "--conf" || arg == "-conf" { + if i+1 < len(args) { + return args[i+1] + } + } + if strings.HasPrefix(arg, "--conf=") { + return strings.TrimPrefix(arg, "--conf=") + } + if strings.HasPrefix(arg, "-conf=") { + return strings.TrimPrefix(arg, "-conf=") + } + } + return "" +} + +func loadFile(cfg *Config, path string) error { + data, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("read config %s: %w", path, err) + } + if strings.EqualFold(filepath.Ext(path), ".json") { + if err := yaml.Unmarshal(data, cfg); err != nil { + return fmt.Errorf("parse JSON config %s: %w", path, err) + } + return nil + } + if err := yaml.Unmarshal(data, cfg); err != nil { + return fmt.Errorf("parse YAML config %s: %w", path, err) + } + return nil +} + +func applyEnv(cfg *Config) { + setString(&cfg.LogLevel, "KERERU_LOG_LEVEL") + setString(&cfg.LogFormat, "KERERU_LOG_FORMAT") + setString(&cfg.Environment, "KERERU_ENVIRONMENT") + setString(&cfg.HTTPAddr, "KERERU_LISTEN_ADDR") + setString(&cfg.HTTPAddr6, "KERERU_LISTEN_ADDR6") + setString(&cfg.Port, "KERERU_PORT") + setString(&cfg.DBDriver, "KERERU_DB_DRIVER") + setString(&cfg.DBDSN, "KERERU_DB_DSN") + setString(&cfg.StaticDir, "KERERU_STATIC_DIR") + if v, ok := os.LookupEnv("KERERU_TELEMETRY_ENABLED"); ok { + if b, err := strconv.ParseBool(v); err == nil { + cfg.TelemetryEnabled = b + } + } + setString(&cfg.OTLPEndpoint, "OTEL_EXPORTER_OTLP_ENDPOINT") + if v, ok := os.LookupEnv("KERERU_CORS_ORIGINS"); ok { + var origins []string + for _, part := range strings.Split(v, ",") { + if p := strings.TrimSpace(part); p != "" { + origins = append(origins, p) + } + } + if len(origins) > 0 { + cfg.CORSOrigins = origins + } + } +} + +func setString(dst *string, key string) { + if v, ok := os.LookupEnv(key); ok { + *dst = v + } +} + +// LoadDotEnv reads a .env file from the directory containing the executable +// or, failing that, the working directory. Existing environment variables are never +// overwritten. It returns the path that was loaded, if any. +func LoadDotEnv() string { + candidateDirs := []string{} + if exe, err := os.Executable(); err == nil { + candidateDirs = append(candidateDirs, filepath.Dir(exe)) + } + if wd, err := os.Getwd(); err == nil { + candidateDirs = append(candidateDirs, wd) + } + + for _, dir := range candidateDirs { + path := filepath.Join(dir, ".env") + data, err := os.ReadFile(path) + if err != nil { + continue + } + for _, line := range strings.Split(string(data), "\n") { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + line = strings.TrimPrefix(line, "export ") + key, value, ok := strings.Cut(line, "=") + if !ok { + continue + } + key = strings.TrimSpace(key) + value = strings.Trim(strings.TrimSpace(value), `"'`) + if key == "" { + continue + } + if _, exists := os.LookupEnv(key); !exists { + _ = os.Setenv(key, value) + } + } + return path + } + return "" +} diff --git a/backend/internal/config/config_test.go b/backend/internal/config/config_test.go new file mode 100644 index 0000000..775f6f1 --- /dev/null +++ b/backend/internal/config/config_test.go @@ -0,0 +1,96 @@ +package config + +import ( + "os" + "path/filepath" + "testing" +) + +func TestDefaults(t *testing.T) { + cfg, err := Load(nil) + if err != nil { + t.Fatalf("Load: %v", err) + } + if cfg.Port != "8080" { + t.Fatalf("Port = %q, want 8080", cfg.Port) + } + if cfg.DBDriver != "sqlite" { + t.Fatalf("DBDriver = %q, want sqlite", cfg.DBDriver) + } + if got := cfg.Addresses(); len(got) != 1 || got[0] != "0.0.0.0:8080" { + t.Fatalf("Addresses = %v, want [0.0.0.0:8080]", got) + } +} + +func TestPrecedence(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "config.yaml") + content := ` +logLevel: debug +port: "9090" +listenAddr: "127.0.0.1" +dbDsn: "file:from-file.db" +corsOrigins: + - "https://forms.example.com" +` + if err := os.WriteFile(path, []byte(content), 0o600); err != nil { + t.Fatal(err) + } + + t.Setenv("KERERU_LOG_LEVEL", "warn") + t.Setenv("KERERU_PORT", "9091") + + cfg, err := Load([]string{"--conf", path, "--port", "9092"}) + if err != nil { + t.Fatalf("Load: %v", err) + } + + if cfg.LogLevel != "warn" { + t.Fatalf("LogLevel = %q, want env override warn", cfg.LogLevel) + } + if cfg.Port != "9092" { + t.Fatalf("Port = %q, want flag override 9092", cfg.Port) + } + if cfg.HTTPAddr != "127.0.0.1" { + t.Fatalf("HTTPAddr = %q, want file value", cfg.HTTPAddr) + } + if cfg.DBDSN != "file:from-file.db" { + t.Fatalf("DBDSN = %q, want file value", cfg.DBDSN) + } + if len(cfg.CORSOrigins) != 1 || cfg.CORSOrigins[0] != "https://forms.example.com" { + t.Fatalf("CORSOrigins = %v", cfg.CORSOrigins) + } +} + +func TestJSONConfigAndIPv6(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "config.json") + content := `{"listenAddr":"","listenAddr6":"::1","port":"8081"}` + if err := os.WriteFile(path, []byte(content), 0o600); err != nil { + t.Fatal(err) + } + + cfg, err := Load([]string{"--conf=" + path}) + if err != nil { + t.Fatalf("Load: %v", err) + } + addrs := cfg.Addresses() + if len(addrs) != 1 || addrs[0] != "[::1]:8081" { + t.Fatalf("Addresses = %v, want [[::1]:8081]", addrs) + } +} + +func TestValidateRequiresListener(t *testing.T) { + cfg := Default() + cfg.HTTPAddr = "" + cfg.HTTPAddr6 = "" + if err := cfg.Validate(); err == nil { + t.Fatal("expected validation error when no listener is configured") + } +} + +func TestInvalidPort(t *testing.T) { + if _, err := Load([]string{"--port", "not-a-port"}); err == nil { + t.Fatal("expected validation error for invalid port") + } +} diff --git a/backend/internal/database/migrations.go b/backend/internal/database/migrations.go new file mode 100644 index 0000000..90f017b --- /dev/null +++ b/backend/internal/database/migrations.go @@ -0,0 +1,161 @@ +package database + +import ( + "context" + "fmt" + + "github.com/google/uuid" +) + +func newID() string { return uuid.NewString() } + +type migration struct { + version int + name string + statements string +} + +var migrations = []migration{ + { + version: 1, + name: "initial schema", + statements: ` +CREATE TABLE IF NOT EXISTS forms ( + id TEXT PRIMARY KEY, + form_group_id TEXT NOT NULL, + version INTEGER NOT NULL DEFAULT 1, + title TEXT NOT NULL, + description TEXT, + introduction TEXT, + is_active INTEGER NOT NULL DEFAULT 1, + requires_access_code INTEGER NOT NULL DEFAULT 0, + logo_url TEXT, + company_name TEXT, + contact_email TEXT, + contact_phone TEXT, + contact_url TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + UNIQUE (form_group_id, version) +); +CREATE INDEX IF NOT EXISTS idx_forms_group ON forms (form_group_id); +CREATE INDEX IF NOT EXISTS idx_forms_active ON forms (is_active); + +CREATE TABLE IF NOT EXISTS questions ( + id TEXT PRIMARY KEY, + form_id TEXT NOT NULL REFERENCES forms (id) ON DELETE CASCADE, + position INTEGER NOT NULL, + question_key TEXT NOT NULL, + label TEXT NOT NULL, + description TEXT, + type TEXT NOT NULL, + required INTEGER NOT NULL DEFAULT 0, + options_json TEXT, + min_value REAL, + max_value REAL, + placeholder TEXT, + multiline INTEGER NOT NULL DEFAULT 0, + UNIQUE (form_id, question_key) +); +CREATE INDEX IF NOT EXISTS idx_questions_form ON questions (form_id); + +CREATE TABLE IF NOT EXISTS submissions ( + id TEXT PRIMARY KEY, + form_id TEXT NOT NULL REFERENCES forms (id) ON DELETE CASCADE, + form_version INTEGER NOT NULL, + user_id TEXT NOT NULL, + user_id_type TEXT NOT NULL, + submitted_at TEXT NOT NULL, + ip_address TEXT, + user_agent TEXT, + locale TEXT, + timezone TEXT, + place TEXT, + referer TEXT +); +CREATE INDEX IF NOT EXISTS idx_submissions_form ON submissions (form_id); +CREATE INDEX IF NOT EXISTS idx_submissions_user ON submissions (user_id); + +CREATE TABLE IF NOT EXISTS answers ( + id TEXT PRIMARY KEY, + submission_id TEXT NOT NULL REFERENCES submissions (id) ON DELETE CASCADE, + question_id TEXT NOT NULL, + question_key TEXT NOT NULL, + value_json TEXT NOT NULL +); +CREATE INDEX IF NOT EXISTS idx_answers_submission ON answers (submission_id); + +CREATE TABLE IF NOT EXISTS access_codes ( + id TEXT PRIMARY KEY, + form_id TEXT NOT NULL REFERENCES forms (id) ON DELETE CASCADE, + code TEXT NOT NULL UNIQUE, + used INTEGER NOT NULL DEFAULT 0, + used_at TEXT, + submission_id TEXT, + created_at TEXT NOT NULL +); +CREATE INDEX IF NOT EXISTS idx_codes_form ON access_codes (form_id); +`, + }, + { + version: 2, + name: "signatures and offline pdf", + statements: ` +ALTER TABLE forms ADD COLUMN pdf_url TEXT; +ALTER TABLE forms ADD COLUMN signature_required INTEGER NOT NULL DEFAULT 0; +ALTER TABLE submissions ADD COLUMN signature_json TEXT; +ALTER TABLE submissions ADD COLUMN offline_pdf_name TEXT; +ALTER TABLE submissions ADD COLUMN offline_pdf BLOB; +`, + }, + { + version: 3, + name: "external reference", + statements: ` +ALTER TABLE forms ADD COLUMN external_reference_required INTEGER NOT NULL DEFAULT 0; +ALTER TABLE forms ADD COLUMN external_reference_label TEXT; +ALTER TABLE submissions ADD COLUMN external_reference TEXT; +`, + }, +} + +func (s *sqliteStore) migrate(ctx context.Context) error { + if _, err := s.db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS schema_migrations ( + version INTEGER PRIMARY KEY, + name TEXT NOT NULL, + applied_at TEXT NOT NULL DEFAULT (datetime('now')) + )`); err != nil { + return err + } + + for _, m := range migrations { + var exists int + err := s.db.QueryRowContext(ctx, + `SELECT COUNT(*) FROM schema_migrations WHERE version = ?`, m.version).Scan(&exists) + if err != nil { + return err + } + if exists > 0 { + continue + } + + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return err + } + if _, err := tx.ExecContext(ctx, m.statements); err != nil { + _ = tx.Rollback() + return fmt.Errorf("apply migration %d (%s): %w", m.version, m.name, err) + } + if _, err := tx.ExecContext(ctx, + `INSERT INTO schema_migrations (version, name) VALUES (?, ?)`, + m.version, m.name); err != nil { + _ = tx.Rollback() + return err + } + if err := tx.Commit(); err != nil { + return err + } + } + return nil +} diff --git a/backend/internal/database/sqlite.go b/backend/internal/database/sqlite.go new file mode 100644 index 0000000..273823f --- /dev/null +++ b/backend/internal/database/sqlite.go @@ -0,0 +1,642 @@ +package database + +import ( + "context" + "database/sql" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + _ "modernc.org/sqlite" +) + +// Open creates a Store for the requested driver. SQLite is fully supported; +// the driver switch is the extension point for PostgreSQL and MariaDB. +func Open(driver, dsn string) (Store, error) { + switch strings.ToLower(strings.TrimSpace(driver)) { + case "", "sqlite", "sqlite3": + return openSQLite(dsn) + default: + return nil, fmt.Errorf("unsupported database driver %q (supported: sqlite)", driver) + } +} + +type sqliteStore struct { + db *sql.DB +} + +func openSQLite(dsn string) (*sqliteStore, error) { + if err := ensureSQLiteDir(dsn); err != nil { + return nil, err + } + db, err := sql.Open("sqlite", dsn) + if err != nil { + return nil, fmt.Errorf("open sqlite: %w", err) + } + // SQLite allows a single writer; a small pool keeps behaviour predictable. + db.SetMaxOpenConns(1) + db.SetMaxIdleConns(1) + db.SetConnMaxLifetime(0) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + if err := db.PingContext(ctx); err != nil { + _ = db.Close() + return nil, fmt.Errorf("ping sqlite: %w", err) + } + + store := &sqliteStore{db: db} + if err := store.migrate(ctx); err != nil { + _ = db.Close() + return nil, fmt.Errorf("migrate sqlite: %w", err) + } + return store, nil +} + +func ensureSQLiteDir(dsn string) error { + path := dsn + if idx := strings.Index(path, "?"); idx >= 0 { + path = path[:idx] + } + path = strings.TrimPrefix(path, "file:") + if path == "" || path == ":memory:" || strings.HasPrefix(path, ":memory:") { + return nil + } + dir := filepath.Dir(path) + if dir == "." || dir == "" { + return nil + } + if err := os.MkdirAll(dir, 0o755); err != nil { + return fmt.Errorf("create database directory %s: %w", dir, err) + } + return nil +} + +func (s *sqliteStore) Close() error { return s.db.Close() } + +func (s *sqliteStore) Ping(ctx context.Context) error { return s.db.PingContext(ctx) } + +func formatTime(t time.Time) string { return t.UTC().Format(time.RFC3339Nano) } + +func parseTime(v string) time.Time { + t, err := time.Parse(time.RFC3339Nano, v) + if err != nil { + return time.Time{} + } + return t.UTC() +} + +func nullString(v *string) sql.NullString { + if v == nil || *v == "" { + return sql.NullString{} + } + return sql.NullString{String: *v, Valid: true} +} + +func fromNullString(v sql.NullString) *string { + if !v.Valid { + return nil + } + s := v.String + return &s +} + +func boolToInt(b bool) int { + if b { + return 1 + } + return 0 +} + +func (s *sqliteStore) CreateForm(ctx context.Context, form *Form) error { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer func() { _ = tx.Rollback() }() + + if err := insertForm(ctx, tx, form); err != nil { + return err + } + for i := range form.Questions { + if err := insertQuestion(ctx, tx, &form.Questions[i]); err != nil { + return err + } + } + return tx.Commit() +} + +func (s *sqliteStore) UpdateForm(ctx context.Context, form *Form) error { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer func() { _ = tx.Rollback() }() + + res, err := tx.ExecContext(ctx, `UPDATE forms SET + title = ?, description = ?, introduction = ?, is_active = ?, + requires_access_code = ?, signature_required = ?, + external_reference_required = ?, external_reference_label = ?, + logo_url = ?, pdf_url = ?, company_name = ?, contact_email = ?, + contact_phone = ?, contact_url = ?, updated_at = ? + WHERE id = ?`, + form.Title, nullString(form.Description), nullString(form.Introduction), + boolToInt(form.IsActive), boolToInt(form.RequiresAccessCode), + boolToInt(form.SignatureRequired), boolToInt(form.ExternalReferenceRequired), + nullString(form.ExternalReferenceLabel), nullString(form.LogoURL), + nullString(form.PDFURL), nullString(form.CompanyName), + nullString(form.ContactEmail), nullString(form.ContactPhone), + nullString(form.ContactURL), formatTime(form.UpdatedAt), form.ID, + ) + if err != nil { + return err + } + if n, _ := res.RowsAffected(); n == 0 { + return ErrNotFound + } + + if _, err := tx.ExecContext(ctx, `DELETE FROM questions WHERE form_id = ?`, form.ID); err != nil { + return err + } + for i := range form.Questions { + form.Questions[i].FormID = form.ID + if err := insertQuestion(ctx, tx, &form.Questions[i]); err != nil { + return err + } + } + return tx.Commit() +} + +func insertForm(ctx context.Context, tx *sql.Tx, form *Form) error { + _, err := tx.ExecContext(ctx, `INSERT INTO forms ( + id, form_group_id, version, title, description, introduction, is_active, + requires_access_code, signature_required, external_reference_required, + external_reference_label, logo_url, pdf_url, company_name, contact_email, + contact_phone, contact_url, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + form.ID, form.FormGroupID, form.Version, form.Title, + nullString(form.Description), nullString(form.Introduction), + boolToInt(form.IsActive), boolToInt(form.RequiresAccessCode), + boolToInt(form.SignatureRequired), boolToInt(form.ExternalReferenceRequired), + nullString(form.ExternalReferenceLabel), nullString(form.LogoURL), + nullString(form.PDFURL), nullString(form.CompanyName), + nullString(form.ContactEmail), nullString(form.ContactPhone), + nullString(form.ContactURL), formatTime(form.CreatedAt), formatTime(form.UpdatedAt), + ) + if err != nil { + if strings.Contains(err.Error(), "UNIQUE") { + return ErrConflict + } + return err + } + return nil +} + +func insertQuestion(ctx context.Context, tx *sql.Tx, q *Question) error { + options, err := json.Marshal(q.Options) + if err != nil { + return err + } + var minV, maxV sql.NullFloat64 + if q.MinValue != nil { + minV = sql.NullFloat64{Float64: *q.MinValue, Valid: true} + } + if q.MaxValue != nil { + maxV = sql.NullFloat64{Float64: *q.MaxValue, Valid: true} + } + _, err = tx.ExecContext(ctx, `INSERT INTO questions ( + id, form_id, position, question_key, label, description, type, required, + options_json, min_value, max_value, placeholder, multiline + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + q.ID, q.FormID, q.Position, q.Key, q.Label, nullString(q.Description), + q.Type, boolToInt(q.Required), string(options), minV, maxV, + nullString(q.Placeholder), boolToInt(q.Multiline), + ) + if err != nil { + if strings.Contains(err.Error(), "UNIQUE") { + return ErrConflict + } + return err + } + return nil +} + +const formColumns = `id, form_group_id, version, title, description, introduction, + is_active, requires_access_code, signature_required, external_reference_required, + external_reference_label, logo_url, pdf_url, company_name, contact_email, + contact_phone, contact_url, created_at, updated_at` + +func (s *sqliteStore) GetForm(ctx context.Context, id string) (*Form, error) { + row := s.db.QueryRowContext(ctx, `SELECT `+formColumns+` FROM forms WHERE id = ?`, id) + form, err := scanForm(row) + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrNotFound + } + if err != nil { + return nil, err + } + questions, err := s.listQuestions(ctx, id) + if err != nil { + return nil, err + } + form.Questions = questions + return form, nil +} + +func scanForm(row interface{ Scan(...any) error }) (*Form, error) { + var ( + form Form + description, introduction, logoURL sql.NullString + pdfURL, externalReferenceLabel sql.NullString + companyName, email, phone, contactURL sql.NullString + createdAt, updatedAt string + isActive, requiresAccessCode int + signatureRequired, externalRefReq int + ) + err := row.Scan( + &form.ID, &form.FormGroupID, &form.Version, &form.Title, + &description, &introduction, &isActive, &requiresAccessCode, + &signatureRequired, &externalRefReq, &externalReferenceLabel, + &logoURL, &pdfURL, &companyName, &email, &phone, &contactURL, + &createdAt, &updatedAt, + ) + if err != nil { + return nil, err + } + form.Description = fromNullString(description) + form.Introduction = fromNullString(introduction) + form.LogoURL = fromNullString(logoURL) + form.PDFURL = fromNullString(pdfURL) + form.ExternalReferenceLabel = fromNullString(externalReferenceLabel) + form.CompanyName = fromNullString(companyName) + form.ContactEmail = fromNullString(email) + form.ContactPhone = fromNullString(phone) + form.ContactURL = fromNullString(contactURL) + form.IsActive = isActive == 1 + form.RequiresAccessCode = requiresAccessCode == 1 + form.SignatureRequired = signatureRequired == 1 + form.ExternalReferenceRequired = externalRefReq == 1 + form.CreatedAt = parseTime(createdAt) + form.UpdatedAt = parseTime(updatedAt) + return &form, nil +} + +func (s *sqliteStore) listQuestions(ctx context.Context, formID string) ([]Question, error) { + rows, err := s.db.QueryContext(ctx, `SELECT + id, form_id, position, question_key, label, description, type, required, + options_json, min_value, max_value, placeholder, multiline + FROM questions WHERE form_id = ? ORDER BY position`, formID) + if err != nil { + return nil, err + } + defer rows.Close() + + var questions []Question + for rows.Next() { + var ( + q Question + description, placeholder sql.NullString + optionsJSON string + minV, maxV sql.NullFloat64 + required, multiline int + ) + if err := rows.Scan(&q.ID, &q.FormID, &q.Position, &q.Key, &q.Label, + &description, &q.Type, &required, &optionsJSON, &minV, &maxV, + &placeholder, &multiline); err != nil { + return nil, err + } + q.Description = fromNullString(description) + q.Placeholder = fromNullString(placeholder) + q.Required = required == 1 + q.Multiline = multiline == 1 + if minV.Valid { + v := minV.Float64 + q.MinValue = &v + } + if maxV.Valid { + v := maxV.Float64 + q.MaxValue = &v + } + if optionsJSON != "" && optionsJSON != "null" { + if err := json.Unmarshal([]byte(optionsJSON), &q.Options); err != nil { + return nil, fmt.Errorf("decode options for question %s: %w", q.ID, err) + } + } + questions = append(questions, q) + } + return questions, rows.Err() +} + +func (s *sqliteStore) ListForms(ctx context.Context, includeInactive bool) ([]FormSummary, error) { + query := `SELECT f.id, f.form_group_id, f.version, f.title, f.description, f.logo_url, + f.pdf_url, f.external_reference_required, f.external_reference_label, + f.is_active, f.created_at, f.updated_at, + (SELECT COUNT(*) FROM questions q WHERE q.form_id = f.id) AS question_count + FROM forms f` + if !includeInactive { + query += ` WHERE f.is_active = 1` + } + query += ` ORDER BY f.updated_at DESC` + + rows, err := s.db.QueryContext(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + summaries := make([]FormSummary, 0) + for rows.Next() { + var ( + summary FormSummary + description sql.NullString + logoURL sql.NullString + pdfURL sql.NullString + externalRefLabel sql.NullString + createdAt string + updatedAt string + isActive int + externalRefRequired int + ) + if err := rows.Scan(&summary.ID, &summary.FormGroupID, &summary.Version, + &summary.Title, &description, &logoURL, &pdfURL, + &externalRefRequired, &externalRefLabel, &isActive, &createdAt, &updatedAt, + &summary.QuestionCount); err != nil { + return nil, err + } + summary.Description = fromNullString(description) + summary.LogoURL = fromNullString(logoURL) + summary.PDFURL = fromNullString(pdfURL) + summary.ExternalReferenceLabel = fromNullString(externalRefLabel) + summary.ExternalReferenceRequired = externalRefRequired == 1 + summary.IsActive = isActive == 1 + summary.CreatedAt = parseTime(createdAt) + summary.UpdatedAt = parseTime(updatedAt) + summaries = append(summaries, summary) + } + return summaries, rows.Err() +} + +func (s *sqliteStore) SetFormActive(ctx context.Context, id string, active bool) error { + res, err := s.db.ExecContext(ctx, + `UPDATE forms SET is_active = ?, updated_at = ? WHERE id = ?`, + boolToInt(active), formatTime(time.Now()), id) + if err != nil { + return err + } + if n, _ := res.RowsAffected(); n == 0 { + return ErrNotFound + } + return nil +} + +func (s *sqliteStore) DeleteForm(ctx context.Context, id string) error { + res, err := s.db.ExecContext(ctx, `DELETE FROM forms WHERE id = ?`, id) + if err != nil { + return err + } + if n, _ := res.RowsAffected(); n == 0 { + return ErrNotFound + } + return nil +} + +func (s *sqliteStore) CreateSubmission(ctx context.Context, sub *Submission, accessCode string) error { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer func() { _ = tx.Rollback() }() + + var signatureJSON sql.NullString + if sub.Signature != nil { + encoded, err := json.Marshal(sub.Signature) + if err != nil { + return err + } + signatureJSON = sql.NullString{String: string(encoded), Valid: true} + } + var offlinePDF any + if len(sub.OfflinePDF) > 0 { + offlinePDF = sub.OfflinePDF + } + + _, err = tx.ExecContext(ctx, `INSERT INTO submissions ( + id, form_id, form_version, user_id, user_id_type, submitted_at, + ip_address, user_agent, locale, timezone, place, referer, + external_reference, signature_json, offline_pdf_name, offline_pdf + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + sub.ID, sub.FormID, sub.FormVersion, sub.UserID, sub.UserIDType, + formatTime(sub.SubmittedAt), nullString(sub.IPAddress), + nullString(sub.UserAgent), nullString(sub.Locale), nullString(sub.Timezone), + nullString(sub.Place), nullString(sub.Referer), + nullString(sub.ExternalReference), signatureJSON, + nullString(sub.OfflinePDFName), offlinePDF, + ) + if err != nil { + return err + } + + for i := range sub.Answers { + if _, err := tx.ExecContext(ctx, `INSERT INTO answers ( + id, submission_id, question_id, question_key, value_json + ) VALUES (?, ?, ?, ?, ?)`, + newID(), sub.ID, sub.Answers[i].QuestionID, + sub.Answers[i].QuestionKey, string(sub.Answers[i].Value), + ); err != nil { + return err + } + } + + if accessCode != "" { + res, err := tx.ExecContext(ctx, `UPDATE access_codes + SET used = 1, used_at = ?, submission_id = ? + WHERE form_id = ? AND code = ? AND used = 0`, + formatTime(time.Now()), sub.ID, sub.FormID, accessCode) + if err != nil { + return err + } + if n, _ := res.RowsAffected(); n == 0 { + var used int + err := tx.QueryRowContext(ctx, + `SELECT used FROM access_codes WHERE form_id = ? AND code = ?`, + sub.FormID, accessCode).Scan(&used) + if errors.Is(err, sql.ErrNoRows) { + return ErrAccessCode + } + if err != nil { + return err + } + return ErrCodeAlreadyIn + } + } + + return tx.Commit() +} + +func (s *sqliteStore) GetSubmission(ctx context.Context, id string) (*Submission, error) { + var ( + sub Submission + submittedAt string + ipAddress, userAgent, locale, timezone sql.NullString + place, referer sql.NullString + signatureJSON sql.NullString + offlinePDFName sql.NullString + externalReference sql.NullString + ) + err := s.db.QueryRowContext(ctx, `SELECT id, form_id, form_version, user_id, + user_id_type, submitted_at, ip_address, user_agent, locale, timezone, + place, referer, external_reference, signature_json, offline_pdf_name + FROM submissions WHERE id = ?`, id).Scan( + &sub.ID, &sub.FormID, &sub.FormVersion, &sub.UserID, &sub.UserIDType, + &submittedAt, &ipAddress, &userAgent, &locale, &timezone, &place, &referer, + &externalReference, &signatureJSON, &offlinePDFName, + ) + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrNotFound + } + if err != nil { + return nil, err + } + sub.SubmittedAt = parseTime(submittedAt) + sub.IPAddress = fromNullString(ipAddress) + sub.UserAgent = fromNullString(userAgent) + sub.Locale = fromNullString(locale) + sub.Timezone = fromNullString(timezone) + sub.Place = fromNullString(place) + sub.Referer = fromNullString(referer) + sub.ExternalReference = fromNullString(externalReference) + sub.OfflinePDFName = fromNullString(offlinePDFName) + if signatureJSON.Valid && signatureJSON.String != "" { + var signature Signature + if err := json.Unmarshal([]byte(signatureJSON.String), &signature); err != nil { + return nil, fmt.Errorf("decode signature for submission %s: %w", id, err) + } + sub.Signature = &signature + } + + rows, err := s.db.QueryContext(ctx, `SELECT question_id, question_key, value_json + FROM answers WHERE submission_id = ?`, id) + if err != nil { + return nil, err + } + defer rows.Close() + for rows.Next() { + var ( + a Answer + valueJSON string + ) + if err := rows.Scan(&a.QuestionID, &a.QuestionKey, &valueJSON); err != nil { + return nil, err + } + a.Value = json.RawMessage(valueJSON) + sub.Answers = append(sub.Answers, a) + } + return &sub, rows.Err() +} + +func (s *sqliteStore) GetSubmissionOfflinePDF(ctx context.Context, id string) ([]byte, string, error) { + var ( + data []byte + filename sql.NullString + ) + err := s.db.QueryRowContext(ctx, + `SELECT offline_pdf, offline_pdf_name FROM submissions WHERE id = ?`, id). + Scan(&data, &filename) + if errors.Is(err, sql.ErrNoRows) { + return nil, "", ErrNotFound + } + if err != nil { + return nil, "", err + } + if len(data) == 0 { + return nil, "", ErrNotFound + } + name := "submission.pdf" + if filename.Valid && strings.TrimSpace(filename.String) != "" { + name = filepath.Base(filename.String) + } + return data, name, nil +} + +func (s *sqliteStore) ListSubmissions(ctx context.Context, formID string) ([]SubmissionSummary, error) { + rows, err := s.db.QueryContext(ctx, `SELECT id, form_id, form_version, + user_id, user_id_type, external_reference, submitted_at FROM submissions + WHERE form_id = ? ORDER BY submitted_at DESC`, formID) + if err != nil { + return nil, err + } + defer rows.Close() + + summaries := make([]SubmissionSummary, 0) + for rows.Next() { + var ( + summary SubmissionSummary + submittedAt string + externalReference sql.NullString + ) + if err := rows.Scan(&summary.ID, &summary.FormID, &summary.FormVersion, + &summary.UserID, &summary.UserIDType, &externalReference, &submittedAt); err != nil { + return nil, err + } + summary.ExternalReference = fromNullString(externalReference) + summary.SubmittedAt = parseTime(submittedAt) + summaries = append(summaries, summary) + } + return summaries, rows.Err() +} + +func (s *sqliteStore) CreateAccessCodes(ctx context.Context, codes []AccessCode) error { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer func() { _ = tx.Rollback() }() + + for _, code := range codes { + _, err := tx.ExecContext(ctx, `INSERT INTO access_codes + (id, form_id, code, used, created_at) VALUES (?, ?, ?, ?, ?)`, + code.ID, code.FormID, code.Code, boolToInt(code.Used), formatTime(code.CreatedAt)) + if err != nil { + if strings.Contains(err.Error(), "UNIQUE") { + return ErrConflict + } + return err + } + } + return tx.Commit() +} + +func (s *sqliteStore) ListAccessCodes(ctx context.Context, formID string) ([]AccessCode, error) { + rows, err := s.db.QueryContext(ctx, `SELECT id, form_id, code, used, + used_at, created_at FROM access_codes WHERE form_id = ? ORDER BY created_at`, formID) + if err != nil { + return nil, err + } + defer rows.Close() + + codes := make([]AccessCode, 0) + for rows.Next() { + var ( + code AccessCode + usedAt sql.NullString + createdAt string + used int + ) + if err := rows.Scan(&code.ID, &code.FormID, &code.Code, &used, + &usedAt, &createdAt); err != nil { + return nil, err + } + code.Used = used == 1 + code.CreatedAt = parseTime(createdAt) + if usedAt.Valid { + t := parseTime(usedAt.String) + code.UsedAt = &t + } + codes = append(codes, code) + } + return codes, rows.Err() +} diff --git a/backend/internal/database/sqlite_test.go b/backend/internal/database/sqlite_test.go new file mode 100644 index 0000000..675cf71 --- /dev/null +++ b/backend/internal/database/sqlite_test.go @@ -0,0 +1,227 @@ +package database + +import ( + "context" + "encoding/json" + "errors" + "path/filepath" + "testing" + "time" +) + +func testStore(t *testing.T) (*sqliteStore, string) { + t.Helper() + path := filepath.Join(t.TempDir(), "kereru-test.db") + store, err := Open("sqlite", "file:"+path+"?_pragma=busy_timeout(5000)&_pragma=foreign_keys(1)") + if err != nil { + t.Fatalf("Open: %v", err) + } + t.Cleanup(func() { _ = store.Close() }) + return store.(*sqliteStore), path +} + +func sampleForm() *Form { + now := time.Now().UTC() + return &Form{ + ID: "11111111-1111-1111-1111-111111111111", + FormGroupID: "22222222-2222-2222-2222-222222222222", + Version: 1, + Title: "Customer feedback", + Description: strPtr("Tell us how we did"), + IsActive: true, + RequiresAccessCode: false, + CreatedAt: now, + UpdatedAt: now, + Questions: []Question{ + { + ID: "33333333-3333-3333-3333-333333333333", + FormID: "11111111-1111-1111-1111-111111111111", + Position: 0, + Key: "happy", + Label: "Are you happy?", + Type: "boolean", + Required: true, + }, + { + ID: "44444444-4444-4444-4444-444444444444", + FormID: "11111111-1111-1111-1111-111111111111", + Position: 1, + Key: "score", + Label: "Score", + Type: "number", + }, + }, + } +} + +func strPtr(s string) *string { return &s } + +func TestMigrateIsIdempotent(t *testing.T) { + _, path := testStore(t) + second, err := Open("sqlite", "file:"+path) + if err != nil { + t.Fatalf("re-open: %v", err) + } + _ = second.Close() +} + +func TestFormCRUD(t *testing.T) { + store, _ := testStore(t) + ctx := context.Background() + + form := sampleForm() + if err := store.CreateForm(ctx, form); err != nil { + t.Fatalf("CreateForm: %v", err) + } + + got, err := store.GetForm(ctx, form.ID) + if err != nil { + t.Fatalf("GetForm: %v", err) + } + if got.Title != form.Title || len(got.Questions) != 2 { + t.Fatalf("unexpected form: %+v", got) + } + if got.Questions[0].Key != "happy" || got.Questions[1].Key != "score" { + t.Fatalf("questions out of order: %+v", got.Questions) + } + + forms, err := store.ListForms(ctx, false) + if err != nil { + t.Fatalf("ListForms: %v", err) + } + if len(forms) != 1 || forms[0].QuestionCount != 2 { + t.Fatalf("ListForms = %+v", forms) + } + + got.Title = "Updated title" + got.Questions = got.Questions[:1] + got.UpdatedAt = time.Now().UTC() + if err := store.UpdateForm(ctx, got); err != nil { + t.Fatalf("UpdateForm: %v", err) + } + updated, err := store.GetForm(ctx, form.ID) + if err != nil { + t.Fatalf("GetForm after update: %v", err) + } + if updated.Title != "Updated title" || len(updated.Questions) != 1 { + t.Fatalf("update not applied: %+v", updated) + } + + if err := store.SetFormActive(ctx, form.ID, false); err != nil { + t.Fatalf("SetFormActive: %v", err) + } + active, err := store.ListForms(ctx, false) + if err != nil { + t.Fatalf("ListForms active: %v", err) + } + if len(active) != 0 { + t.Fatalf("expected no active forms, got %+v", active) + } + all, err := store.ListForms(ctx, true) + if err != nil { + t.Fatalf("ListForms all: %v", err) + } + if len(all) != 1 || all[0].IsActive { + t.Fatalf("expected one inactive form, got %+v", all) + } + + if err := store.DeleteForm(ctx, form.ID); err != nil { + t.Fatalf("DeleteForm: %v", err) + } + if _, err := store.GetForm(ctx, form.ID); !errors.Is(err, ErrNotFound) { + t.Fatalf("expected ErrNotFound, got %v", err) + } +} + +func TestSubmissionsAndAccessCodes(t *testing.T) { + store, _ := testStore(t) + ctx := context.Background() + + form := sampleForm() + form.RequiresAccessCode = true + if err := store.CreateForm(ctx, form); err != nil { + t.Fatalf("CreateForm: %v", err) + } + + now := time.Now().UTC() + code := AccessCode{ + ID: "55555555-5555-5555-5555-555555555555", + FormID: form.ID, + Code: "TEST-ABCD-EFGH", + CreatedAt: now, + } + if err := store.CreateAccessCodes(ctx, []AccessCode{code}); err != nil { + t.Fatalf("CreateAccessCodes: %v", err) + } + + codes, err := store.ListAccessCodes(ctx, form.ID) + if err != nil { + t.Fatalf("ListAccessCodes: %v", err) + } + if len(codes) != 1 || codes[0].Used { + t.Fatalf("unexpected codes: %+v", codes) + } + + value, _ := json.Marshal(true) + sub := &Submission{ + ID: "66666666-6666-6666-6666-666666666666", + FormID: form.ID, + FormVersion: form.Version, + UserID: "person@example.com", + UserIDType: "email", + SubmittedAt: now, + Answers: []Answer{ + {QuestionID: form.Questions[0].ID, QuestionKey: "happy", Value: value}, + }, + } + + if err := store.CreateSubmission(ctx, sub, "WRONG"); !errors.Is(err, ErrAccessCode) { + t.Fatalf("expected ErrAccessCode, got %v", err) + } + if err := store.CreateSubmission(ctx, sub, code.Code); err != nil { + t.Fatalf("CreateSubmission: %v", err) + } + + got, err := store.GetSubmission(ctx, sub.ID) + if err != nil { + t.Fatalf("GetSubmission: %v", err) + } + if len(got.Answers) != 1 || got.Answers[0].QuestionKey != "happy" { + t.Fatalf("unexpected submission: %+v", got) + } + + codes, _ = store.ListAccessCodes(ctx, form.ID) + if !codes[0].Used || codes[0].UsedAt == nil { + t.Fatalf("access code should be marked used: %+v", codes[0]) + } + + reuse := *sub + reuse.ID = "77777777-7777-7777-7777-777777777777" + if err := store.CreateSubmission(ctx, &reuse, code.Code); !errors.Is(err, ErrCodeAlreadyIn) { + t.Fatalf("expected ErrCodeAlreadyIn, got %v", err) + } + + subs, err := store.ListSubmissions(ctx, form.ID) + if err != nil { + t.Fatalf("ListSubmissions: %v", err) + } + if len(subs) != 1 { + t.Fatalf("expected 1 submission, got %d", len(subs)) + } + + if err := store.DeleteForm(ctx, form.ID); err != nil { + t.Fatalf("DeleteForm with submissions: %v", err) + } + if _, err := store.GetSubmission(ctx, sub.ID); !errors.Is(err, ErrNotFound) { + t.Fatalf("submission should cascade delete, got %v", err) + } + if codes, err := store.ListAccessCodes(ctx, form.ID); err != nil || len(codes) != 0 { + t.Fatalf("access codes should cascade delete, got %v err=%v", codes, err) + } +} + +func TestUnsupportedDriver(t *testing.T) { + if _, err := Open("oracle", "dsn"); err == nil { + t.Fatal("expected error for unsupported driver") + } +} diff --git a/backend/internal/database/store.go b/backend/internal/database/store.go new file mode 100644 index 0000000..c5ae807 --- /dev/null +++ b/backend/internal/database/store.go @@ -0,0 +1,164 @@ +// Package database provides the persistence layer for kereru-forms. +// +// The Store interface deliberately hides SQL details so additional providers +// (PostgreSQL, MariaDB) can be added later without changing the API layer. +package database + +import ( + "context" + "encoding/json" + "errors" + "time" +) + +// Common persistence errors. +var ( + ErrNotFound = errors.New("not found") + ErrConflict = errors.New("conflict") + ErrAccessCode = errors.New("access code is not valid") + ErrCodeAlreadyIn = errors.New("access code has already been used") +) + +// QuestionOption is a selectable value for a select question. +type QuestionOption struct { + Value string `json:"value"` + Label string `json:"label"` +} + +// Question is a single question belonging to a form version. +type Question struct { + ID string + FormID string + Position int + Key string + Label string + Description *string + Type string + Required bool + Options []QuestionOption + MinValue *float64 + MaxValue *float64 + Placeholder *string + Multiline bool +} + +// Form is a concrete version of a logical form. +type Form struct { + ID string + FormGroupID string + Version int + Title string + Description *string + Introduction *string + IsActive bool + RequiresAccessCode bool + SignatureRequired bool + ExternalReferenceRequired bool + ExternalReferenceLabel *string + LogoURL *string + PDFURL *string + CompanyName *string + ContactEmail *string + ContactPhone *string + ContactURL *string + CreatedAt time.Time + UpdatedAt time.Time + Questions []Question +} + +// FormSummary is a lightweight form listing entry. +type FormSummary struct { + ID string + FormGroupID string + Version int + Title string + Description *string + LogoURL *string + PDFURL *string + ExternalReferenceRequired bool + ExternalReferenceLabel *string + IsActive bool + QuestionCount int + CreatedAt time.Time + UpdatedAt time.Time +} + +// Answer is a captured answer inside a submission. +type Answer struct { + QuestionID string + QuestionKey string + Value json.RawMessage +} + +// Signature is a captured digital signature. +type Signature struct { + Type string `json:"type"` + DataURL string `json:"dataUrl"` + Name string `json:"name"` + Title *string `json:"title,omitempty"` + SignedAt time.Time `json:"signedAt"` +} + +// Submission is a completed form response. +type Submission struct { + ID string + FormID string + FormVersion int + UserID string + UserIDType string + SubmittedAt time.Time + IPAddress *string + UserAgent *string + Locale *string + Timezone *string + Place *string + Referer *string + ExternalReference *string + Signature *Signature + OfflinePDFName *string + OfflinePDF []byte + Answers []Answer +} + +// SubmissionSummary is a lightweight submission listing entry. +type SubmissionSummary struct { + ID string + FormID string + FormVersion int + UserID string + UserIDType string + ExternalReference *string + SubmittedAt time.Time +} + +// AccessCode is a single-use code granting access to a form. +type AccessCode struct { + ID string + FormID string + Code string + Used bool + UsedAt *time.Time + CreatedAt time.Time +} + +// Store is the persistence contract used by the API layer. +type Store interface { + Close() error + + CreateForm(ctx context.Context, form *Form) error + UpdateForm(ctx context.Context, form *Form) error + GetForm(ctx context.Context, id string) (*Form, error) + ListForms(ctx context.Context, includeInactive bool) ([]FormSummary, error) + SetFormActive(ctx context.Context, id string, active bool) error + DeleteForm(ctx context.Context, id string) error + + CreateSubmission(ctx context.Context, submission *Submission, accessCode string) error + GetSubmission(ctx context.Context, id string) (*Submission, error) + GetSubmissionOfflinePDF(ctx context.Context, id string) ([]byte, string, error) + ListSubmissions(ctx context.Context, formID string) ([]SubmissionSummary, error) + + CreateAccessCodes(ctx context.Context, codes []AccessCode) error + ListAccessCodes(ctx context.Context, formID string) ([]AccessCode, error) + + Ping(ctx context.Context) error +} diff --git a/backend/internal/logging/logging.go b/backend/internal/logging/logging.go new file mode 100644 index 0000000..8c7c303 --- /dev/null +++ b/backend/internal/logging/logging.go @@ -0,0 +1,89 @@ +// Package logging configures the application's structured logger. +package logging + +import ( + "os" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/rs/zerolog" +) + +// New builds a zerolog logger for the given level and format. +// +// level is one of: trace, debug, info, warn, error, fatal, panic. +// format is "json" for machine-readable output, anything else produces a +// human-friendly console output (useful for local development). +func New(level, format, environment string) zerolog.Logger { + lvl, err := zerolog.ParseLevel(strings.ToLower(strings.TrimSpace(level))) + if err != nil || lvl == zerolog.NoLevel { + lvl = zerolog.InfoLevel + } + + var writer zerolog.LevelWriter + if strings.EqualFold(format, "json") { + writer = zerolog.MultiLevelWriter(os.Stdout) + } else { + console := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339} + writer = zerolog.MultiLevelWriter(console) + } + + logger := zerolog.New(writer). + Level(lvl). + With(). + Timestamp(). + Str("service", "kereru-forms"). + Str("environment", environment). + Logger() + + zerolog.SetGlobalLevel(lvl) + return logger +} + +// GinMiddleware logs every HTTP request with useful observability fields. +func GinMiddleware(logger zerolog.Logger) gin.HandlerFunc { + return func(c *gin.Context) { + start := time.Now() + path := c.Request.URL.Path + if raw := c.Request.URL.RawQuery; raw != "" { + path = path + "?" + raw + } + + c.Next() + + event := logger.Info() + if c.Writer.Status() >= 500 { + event = logger.Error() + } else if c.Writer.Status() >= 400 { + event = logger.Warn() + } + + event. + Str("method", c.Request.Method). + Str("path", path). + Int("status", c.Writer.Status()). + Dur("latency", time.Since(start)). + Str("client_ip", c.ClientIP()). + Str("user_agent", c.Request.UserAgent()). + Str("request_id", RequestID(c)). + Msg("http request") + } +} + +const requestIDKey = "request_id" + +// RequestID returns the correlation id assigned to the current request. +func RequestID(c *gin.Context) string { + if v, ok := c.Get(requestIDKey); ok { + if s, ok := v.(string); ok { + return s + } + } + return "" +} + +// SetRequestID stores the correlation id on the context. +func SetRequestID(c *gin.Context, id string) { + c.Set(requestIDKey, id) +} diff --git a/backend/internal/pdf/generator.go b/backend/internal/pdf/generator.go new file mode 100644 index 0000000..4ca16f0 --- /dev/null +++ b/backend/internal/pdf/generator.go @@ -0,0 +1,475 @@ +// Package pdf renders a printable representation of a form definition. +package pdf + +import ( + "bytes" + "fmt" + "strings" + "time" + + "github.com/go-pdf/fpdf" + "github.com/google/uuid" + + "kereru.forms/internal/database" +) + +const ( + pdfLeftMargin = 18.0 + pdfPageWidth = 210.0 + pdfPageHeight = 297.0 + pdfBottomEdge = pdfPageHeight - 18 + pdfContentWide = pdfPageWidth - pdfLeftMargin*2 + pdfLabelWidth = 112.0 + pdfAnswerX = pdfLeftMargin + pdfLabelWidth + 4 + pdfAnswerWidth = pdfContentWide - pdfLabelWidth - 4 +) + +// Generate builds a PDF document for the supplied form. The document doubles +// as a printable offline form: it includes a submitter details area and space +// for handwritten answers. +func Generate(form *database.Form) ([]byte, error) { + if form == nil { + return nil, fmt.Errorf("form is nil") + } + + doc := fpdf.New("P", "mm", "A4", "") + doc.SetMargins(pdfLeftMargin, pdfLeftMargin, pdfLeftMargin) + doc.SetAutoPageBreak(true, 18) + doc.SetTitle(sanitize(form.Title), false) + doc.SetAuthor(sanitize(deref(form.CompanyName, "Kereru Forms")), false) + doc.SetCreator("Kereru Forms", false) + doc.SetSubject(fmt.Sprintf("Form version %d", form.Version), false) + + doc.SetFooterFunc(func() { + doc.SetY(-14) + doc.SetFont("Helvetica", "I", 8) + doc.SetTextColor(120, 120, 120) + doc.CellFormat(0, 6, + fmt.Sprintf("Kereru Forms - %s - page %d", sanitize(form.Title), doc.PageNo()), + "", 0, "C", false, 0, "") + }) + + doc.AddPage() + doc.SetTextColor(20, 20, 20) + + if form.CompanyName != nil && *form.CompanyName != "" { + doc.SetFont("Helvetica", "", 11) + doc.SetTextColor(90, 90, 90) + doc.MultiCell(0, 6, sanitize(*form.CompanyName), "", "L", false) + } + + doc.SetFont("Helvetica", "B", 20) + doc.SetTextColor(20, 20, 20) + doc.MultiCell(0, 9, sanitize(form.Title), "", "L", false) + + doc.SetFont("Helvetica", "", 9) + doc.SetTextColor(110, 110, 110) + doc.MultiCell(0, 5, fmt.Sprintf("Version %d - form id %s", form.Version, form.ID), "", "L", false) + doc.Ln(2) + + if form.Description != nil && strings.TrimSpace(*form.Description) != "" { + doc.SetDrawColor(220, 220, 220) + doc.SetFont("Helvetica", "", 10) + doc.SetTextColor(60, 60, 60) + doc.MultiCell(0, 5, sanitize(*form.Description), "", "L", false) + doc.Ln(2) + } + + if form.Introduction != nil && strings.TrimSpace(*form.Introduction) != "" { + doc.SetFont("Helvetica", "B", 11) + doc.SetTextColor(20, 20, 20) + doc.CellFormat(0, 7, "Introduction", "", 1, "L", false, 0, "") + doc.SetFont("Helvetica", "", 10) + doc.SetTextColor(60, 60, 60) + doc.MultiCell(0, 5, sanitize(*form.Introduction), "", "L", false) + doc.Ln(2) + } + + if hasContact(form) { + doc.SetFont("Helvetica", "B", 11) + doc.SetTextColor(20, 20, 20) + doc.CellFormat(0, 7, "Contact", "", 1, "L", false, 0, "") + doc.SetFont("Helvetica", "", 10) + doc.SetTextColor(60, 60, 60) + if form.ContactEmail != nil && *form.ContactEmail != "" { + doc.MultiCell(0, 5, sanitize("Email: "+*form.ContactEmail), "", "L", false) + } + if form.ContactPhone != nil && *form.ContactPhone != "" { + doc.MultiCell(0, 5, sanitize("Phone: "+*form.ContactPhone), "", "L", false) + } + if form.ContactURL != nil && *form.ContactURL != "" { + doc.MultiCell(0, 5, sanitize("Web: "+*form.ContactURL), "", "L", false) + } + doc.Ln(2) + } + + doc.SetDrawColor(210, 210, 210) + doc.Line(pdfLeftMargin, doc.GetY(), pdfPageWidth-pdfLeftMargin, doc.GetY()) + doc.Ln(4) + + renderSubmitterDetails(doc, form) + + doc.SetFont("Helvetica", "B", 12) + doc.SetTextColor(20, 20, 20) + doc.CellFormat(0, 8, "Questions", "", 1, "L", false, 0, "") + doc.Ln(1) + + if len(form.Questions) == 0 { + doc.SetFont("Helvetica", "I", 10) + doc.SetTextColor(110, 110, 110) + doc.MultiCell(0, 5, "This form does not contain any questions yet.", "", "L", false) + } + + for i, q := range form.Questions { + renderQuestion(doc, i+1, q) + } + + if form.SignatureRequired { + renderSignatureSection(doc) + } + + doc.Ln(4) + doc.SetFont("Helvetica", "I", 8) + doc.SetTextColor(130, 130, 130) + doc.MultiCell(0, 4, fmt.Sprintf( + "Generated %s - %s", time.Now().UTC().Format(time.RFC3339), uuid.NewString()), + "", "L", false) + + var buf bytes.Buffer + if err := doc.Output(&buf); err != nil { + return nil, fmt.Errorf("render pdf: %w", err) + } + return buf.Bytes(), nil +} + +func renderSignatureSection(doc *fpdf.Fpdf) { + ensureSpace(doc, 46) + doc.SetFont("Helvetica", "B", 12) + doc.SetTextColor(20, 20, 20) + doc.CellFormat(0, 8, "Digital signature", "", 1, "L", false, 0, "") + doc.SetFont("Helvetica", "I", 8) + doc.SetTextColor(120, 120, 120) + doc.MultiCell(0, 4, "Completed in the online form as a drawn or uploaded signature.", "", "L", false) + doc.Ln(2) + + y := doc.GetY() + drawnField(doc, pdfLeftMargin, y, "Name:", 90) + drawnField(doc, pdfLeftMargin+95, y, "Title (optional):", 79) + doc.SetY(y + 11) + drawnField(doc, pdfLeftMargin, doc.GetY(), "Signature:", pdfContentWide) + doc.SetY(doc.GetY() + 11) +} + +// renderSubmitterDetails draws the section a respondent completes by hand when +// the form is filled in offline. +func renderSubmitterDetails(doc *fpdf.Fpdf, form *database.Form) { + ensureSpace(doc, 62) + + doc.SetFont("Helvetica", "B", 12) + doc.SetTextColor(20, 20, 20) + doc.CellFormat(0, 8, "Submitter details", "", 1, "L", false, 0, "") + + doc.SetFont("Helvetica", "I", 8) + doc.SetTextColor(120, 120, 120) + doc.MultiCell(0, 4, "Complete this form offline, then return it using the contact details above.", "", "L", false) + doc.Ln(2) + + y := doc.GetY() + drawnField(doc, pdfLeftMargin, y, "Name / identifier:", pdfContentWide) + y += 9 + + doc.SetFont("Helvetica", "", 9) + doc.SetTextColor(90, 90, 90) + doc.SetXY(pdfLeftMargin, y) + doc.CellFormat(26, 6, "Identifier type:", "", 0, "L", false, 0, "") + x := pdfLeftMargin + 28 + for _, label := range []string{"Email", "Username", "Phone", "GUID"} { + drawCheckbox(doc, x, y+1) + doc.SetXY(x+5, y) + doc.CellFormat(0, 6, label, "", 0, "L", false, 0, "") + x += 5 + doc.GetStringWidth(label) + 8 + } + doc.SetY(y + 9) + + y = doc.GetY() + fieldWidth := 82.0 + if form.RequiresAccessCode { + drawnField(doc, pdfLeftMargin, y, "Access code:", fieldWidth) + } + drawnField(doc, pdfLeftMargin+90, y, "Date completed:", fieldWidth) + doc.SetY(y + 9) + + if form.ExternalReferenceRequired { + label := "External reference" + if form.ExternalReferenceLabel != nil && *form.ExternalReferenceLabel != "" { + label = *form.ExternalReferenceLabel + } + y = doc.GetY() + drawnField(doc, pdfLeftMargin, y, sanitize(label)+":", pdfContentWide) + doc.SetY(y + 9) + } + + y = doc.GetY() + drawnField(doc, pdfLeftMargin, y, "Signature:", pdfContentWide) + doc.SetY(y + 10) + + doc.SetDrawColor(210, 210, 210) + doc.Line(pdfLeftMargin, doc.GetY(), pdfPageWidth-pdfLeftMargin, doc.GetY()) + doc.Ln(4) +} + +func renderQuestion(doc *fpdf.Fpdf, number int, q database.Question) { + label := fmt.Sprintf("%d. %s", number, sanitize(q.Label)) + if q.Required { + label += " *" + } + + if q.Type == "text" { + renderTextQuestion(doc, label, q) + return + } + + labelHeight := measureLabel(doc, label, q) + rowHeight := max(labelHeight, answerBlockHeight(q)) + + doc.SetFont("Helvetica", "", 10) + startY := doc.GetY() + if startY+rowHeight > pdfBottomEdge { + doc.AddPage() + startY = doc.GetY() + } + + doc.SetXY(pdfLeftMargin, startY) + doc.SetFont("Helvetica", "B", 10) + doc.SetTextColor(25, 25, 25) + doc.MultiCell(pdfLabelWidth, 5, label, "", "L", false) + + doc.SetFont("Helvetica", "", 8) + doc.SetTextColor(120, 120, 120) + doc.MultiCell(pdfLabelWidth, 4, "Key: "+sanitize(q.Key)+" - "+typeDescription(q), "", "L", false) + + if q.Description != nil && *q.Description != "" { + doc.SetFont("Helvetica", "I", 9) + doc.SetTextColor(90, 90, 90) + doc.MultiCell(pdfLabelWidth, 4, sanitize(*q.Description), "", "L", false) + } + + if q.Placeholder != nil && *q.Placeholder != "" { + doc.SetFont("Helvetica", "I", 9) + doc.SetTextColor(140, 140, 140) + doc.MultiCell(pdfLabelWidth, 4, "Hint: "+sanitize(*q.Placeholder), "", "L", false) + } + + renderAnswerBlock(doc, startY, q) + doc.SetY(max(doc.GetY(), startY+rowHeight) + 3) +} + +func renderTextQuestion(doc *fpdf.Fpdf, label string, q database.Question) { + lines := 1 + if q.Multiline { + lines = 4 + } + needed := 8.0 + float64(lines)*9 + 6 + if q.Description != nil && *q.Description != "" { + needed += 5 + } + ensureSpace(doc, needed) + + doc.SetFont("Helvetica", "B", 10) + doc.SetTextColor(25, 25, 25) + doc.MultiCell(0, 5, label, "", "L", false) + + doc.SetFont("Helvetica", "", 8) + doc.SetTextColor(120, 120, 120) + doc.MultiCell(0, 4, "Key: "+sanitize(q.Key)+" - "+typeDescription(q), "", "L", false) + + if q.Description != nil && *q.Description != "" { + doc.SetFont("Helvetica", "I", 9) + doc.SetTextColor(90, 90, 90) + doc.MultiCell(0, 4, sanitize(*q.Description), "", "L", false) + } + + if q.Placeholder != nil && *q.Placeholder != "" { + doc.SetFont("Helvetica", "I", 9) + doc.SetTextColor(140, 140, 140) + doc.MultiCell(0, 4, "Hint: "+sanitize(*q.Placeholder), "", "L", false) + } + + doc.SetDrawColor(190, 190, 190) + doc.Ln(2) + for i := 0; i < lines; i++ { + doc.CellFormat(0, 7, "", "B", 1, "L", false, 0, "") + doc.Ln(2) + } + doc.Ln(2) +} + +func renderAnswerBlock(doc *fpdf.Fpdf, startY float64, q database.Question) { + switch q.Type { + case "boolean": + drawCheckbox(doc, pdfAnswerX, startY+1) + doc.SetFont("Helvetica", "", 10) + doc.SetTextColor(50, 50, 50) + doc.SetXY(pdfAnswerX+6, startY) + doc.CellFormat(20, 6, "True", "", 0, "L", false, 0, "") + + drawCheckbox(doc, pdfAnswerX+30, startY+1) + doc.SetXY(pdfAnswerX+36, startY) + doc.CellFormat(20, 6, "False", "", 0, "L", false, 0, "") + case "select": + y := startY + for _, opt := range q.Options { + drawCheckbox(doc, pdfAnswerX, y+1) + doc.SetXY(pdfAnswerX+6, y) + doc.SetFont("Helvetica", "", 9) + doc.SetTextColor(50, 50, 50) + doc.MultiCell(pdfAnswerWidth-6, 5, sanitize(opt.Label), "", "L", false) + y = doc.GetY() + } + case "number": + doc.SetDrawColor(150, 150, 150) + doc.Line(pdfAnswerX, startY+6, pdfAnswerX+48, startY+6) + case "date": + doc.SetFont("Helvetica", "", 8) + doc.SetTextColor(140, 140, 140) + doc.SetXY(pdfAnswerX+50, startY+1) + doc.CellFormat(0, 4, "YYYY-MM-DD", "", 0, "L", false, 0, "") + doc.SetDrawColor(150, 150, 150) + doc.Line(pdfAnswerX, startY+6, pdfAnswerX+48, startY+6) + } +} + +func measureLabel(doc *fpdf.Fpdf, label string, q database.Question) float64 { + doc.SetFont("Helvetica", "B", 10) + height := float64(len(doc.SplitText(label, pdfLabelWidth))) * 5 + doc.SetFont("Helvetica", "", 8) + hint := "Key: " + sanitize(q.Key) + " - " + typeDescription(q) + height += float64(len(doc.SplitText(hint, pdfLabelWidth))) * 4 + if q.Description != nil && *q.Description != "" { + doc.SetFont("Helvetica", "I", 9) + height += float64(len(doc.SplitText(sanitize(*q.Description), pdfLabelWidth))) * 4 + } + if q.Placeholder != nil && *q.Placeholder != "" { + doc.SetFont("Helvetica", "I", 9) + height += float64(len(doc.SplitText("Hint: "+sanitize(*q.Placeholder), pdfLabelWidth))) * 4 + } + return height + 2 +} + +func answerBlockHeight(q database.Question) float64 { + switch q.Type { + case "boolean", "number", "date": + return 10 + case "select": + return float64(max(1, len(q.Options))) * 6 + default: + return 10 + } +} + +func ensureSpace(doc *fpdf.Fpdf, needed float64) { + if doc.GetY()+needed > pdfBottomEdge { + doc.AddPage() + } +} + +func drawnField(doc *fpdf.Fpdf, x, y float64, label string, width float64) { + doc.SetFont("Helvetica", "", 9) + doc.SetTextColor(90, 90, 90) + runes := []rune(label) + for len(runes) > 0 && doc.GetStringWidth(string(runes))+2 > width-12 { + runes = runes[:len(runes)-1] + } + label = string(runes) + labelWidth := doc.GetStringWidth(label) + 2 + doc.SetXY(x, y) + doc.CellFormat(labelWidth, 6, label, "", 0, "L", false, 0, "") + doc.SetDrawColor(150, 150, 150) + doc.CellFormat(width-labelWidth, 6, "", "B", 0, "L", false, 0, "") +} + +func drawCheckbox(doc *fpdf.Fpdf, x, y float64) { + doc.SetDrawColor(120, 120, 120) + doc.Rect(x, y+1.2, 4, 4, "D") +} + +func typeDescription(q database.Question) string { + switch q.Type { + case "boolean": + return "True / false" + case "number": + desc := "Number" + if q.MinValue != nil || q.MaxValue != nil { + desc += " (" + if q.MinValue != nil { + desc += fmt.Sprintf("min %g", *q.MinValue) + } + if q.MinValue != nil && q.MaxValue != nil { + desc += ", " + } + if q.MaxValue != nil { + desc += fmt.Sprintf("max %g", *q.MaxValue) + } + desc += ")" + } + return desc + case "select": + return "Select one option" + case "text": + if q.Multiline { + return "Text (multiline)" + } + return "Text (single line)" + case "date": + return "Date (YYYY-MM-DD)" + default: + return q.Type + } +} + +func hasContact(form *database.Form) bool { + vals := []*string{form.ContactEmail, form.ContactPhone, form.ContactURL} + for _, v := range vals { + if v != nil && strings.TrimSpace(*v) != "" { + return true + } + } + return false +} + +func deref(v *string, fallback string) string { + if v == nil || *v == "" { + return fallback + } + return *v +} + +var replacements = map[rune]string{ + 'ā': "a", 'Ā': "A", 'ē': "e", 'Ē': "E", 'ī': "i", 'Ī': "I", + 'ō': "o", 'Ō': "O", 'ū': "u", 'Ū': "U", + '‘': "'", '’': "'", '“': `"`, '”': `"`, + '–': "-", '—': "-", '…': "...", '\u00a0': " ", +} + +// sanitize maps text to the WinAnsi/Latin-1 range supported by the built-in +// PDF fonts. Characters outside that range are transliterated or replaced. +func sanitize(s string) string { + return strings.Map(func(r rune) rune { + if r >= 32 && r <= 255 { + return r + } + return '?' + }, replaceRunes(s)) +} + +func replaceRunes(s string) string { + var b strings.Builder + for _, r := range s { + if rep, ok := replacements[r]; ok { + b.WriteString(rep) + continue + } + b.WriteRune(r) + } + return b.String() +} diff --git a/backend/internal/pdf/generator_test.go b/backend/internal/pdf/generator_test.go new file mode 100644 index 0000000..3a557de --- /dev/null +++ b/backend/internal/pdf/generator_test.go @@ -0,0 +1,76 @@ +package pdf + +import ( + "bytes" + "strings" + "testing" + "time" + + "kereru.forms/internal/database" +) + +func formFixture() *database.Form { + now := time.Now() + desc := "A short description" + intro := "Welcome to the survey" + email := "hello@example.com" + placeholder := "Type here" + multiline := true + return &database.Form{ + ID: "11111111-1111-1111-1111-111111111111", + FormGroupID: "22222222-2222-2222-2222-222222222222", + Version: 3, + Title: "Kererū feedback", + Description: &desc, + Introduction: &intro, + IsActive: true, + ContactEmail: &email, + CreatedAt: now, + UpdatedAt: now, + Questions: []database.Question{ + {ID: "1", Key: "name", Label: "Your name", Type: "text", Required: true, Placeholder: &placeholder}, + {ID: "2", Key: "notes", Label: "Notes", Type: "text", Multiline: multiline}, + {ID: "3", Key: "happy", Label: "Happy?", Type: "boolean"}, + {ID: "4", Key: "score", Label: "Score", Type: "number", MinValue: floatPtr(1), MaxValue: floatPtr(5)}, + {ID: "5", Key: "colour", Label: "Colour", Type: "select", Options: []database.QuestionOption{ + {Value: "red", Label: "Red"}, + {Value: "blue", Label: "Blue"}, + }}, + {ID: "6", Key: "when", Label: "When", Type: "date", Required: true}, + }, + } +} + +func floatPtr(f float64) *float64 { return &f } + +func TestGenerate(t *testing.T) { + data, err := Generate(formFixture()) + if err != nil { + t.Fatalf("Generate: %v", err) + } + if len(data) < 500 { + t.Fatalf("pdf is suspiciously small: %d bytes", len(data)) + } + if !bytes.HasPrefix(data, []byte("%PDF")) { + t.Fatal("output does not start with %PDF") + } + if !bytes.Contains(data, []byte("%%EOF")) { + t.Fatal("output does not contain the EOF marker") + } +} + +func TestSanitize(t *testing.T) { + got := sanitize("Kererū — “quoted” …") + if strings.ContainsAny(got, "ū—") { + t.Fatalf("sanitize left unsupported characters: %q", got) + } + if got != `Kereru - "quoted" ...` { + t.Fatalf("sanitize = %q", got) + } +} + +func TestGenerateNil(t *testing.T) { + if _, err := Generate(nil); err == nil { + t.Fatal("expected error for nil form") + } +} diff --git a/backend/internal/telemetry/telemetry.go b/backend/internal/telemetry/telemetry.go new file mode 100644 index 0000000..278ed80 --- /dev/null +++ b/backend/internal/telemetry/telemetry.go @@ -0,0 +1,125 @@ +// Package telemetry wires OpenTelemetry tracing and metrics. +package telemetry + +import ( + "context" + "strings" + "time" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/resource" + "go.opentelemetry.io/otel/sdk/trace" +) + +// Options configures the telemetry providers. +type Options struct { + Enabled bool + Endpoint string + ServiceName string + Environment string + Version string +} + +// Provider owns the configured OpenTelemetry providers. +type Provider struct { + Enabled bool + shutdown []func(context.Context) error +} + +// Setup installs global tracer and meter providers. When disabled, the +// OpenTelemetry no-op providers remain in place and Shutdown is a no-op. +func Setup(ctx context.Context, opts Options) (*Provider, error) { + p := &Provider{Enabled: opts.Enabled} + if !opts.Enabled { + return p, nil + } + + serviceName := opts.ServiceName + if serviceName == "" { + serviceName = "kereru-forms" + } + endpoint := opts.Endpoint + if endpoint == "" { + endpoint = "localhost:4318" + } + + res, err := resource.New(ctx, + resource.WithAttributes( + attribute.String("service.name", serviceName), + attribute.String("service.version", opts.Version), + attribute.String("deployment.environment", opts.Environment), + ), + ) + if err != nil { + return nil, err + } + + traceExporter, err := otlptracehttp.New(ctx, traceOptions(endpoint)...) + if err != nil { + return nil, err + } + tp := trace.NewTracerProvider( + trace.WithBatcher(traceExporter), + trace.WithResource(res), + ) + otel.SetTracerProvider(tp) + p.shutdown = append(p.shutdown, tp.Shutdown) + + metricExporter, err := otlpmetrichttp.New(ctx, metricOptions(endpoint)...) + if err != nil { + _ = tp.Shutdown(ctx) + return nil, err + } + mp := metric.NewMeterProvider( + metric.WithReader(metric.NewPeriodicReader(metricExporter, + metric.WithInterval(15*time.Second))), + metric.WithResource(res), + ) + otel.SetMeterProvider(mp) + p.shutdown = append(p.shutdown, mp.Shutdown) + + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, propagation.Baggage{}, + )) + + return p, nil +} + +// Shutdown flushes telemetry. Safe to call when telemetry is disabled. +func (p *Provider) Shutdown(ctx context.Context) error { + if p == nil { + return nil + } + var firstErr error + for _, fn := range p.shutdown { + if err := fn(ctx); err != nil && firstErr == nil { + firstErr = err + } + } + return firstErr +} + +func traceOptions(endpoint string) []otlptracehttp.Option { + if strings.Contains(endpoint, "://") { + return []otlptracehttp.Option{otlptracehttp.WithEndpointURL(endpoint)} + } + return []otlptracehttp.Option{ + otlptracehttp.WithEndpoint(endpoint), + otlptracehttp.WithInsecure(), + } +} + +func metricOptions(endpoint string) []otlpmetrichttp.Option { + if strings.Contains(endpoint, "://") { + return []otlpmetrichttp.Option{otlpmetrichttp.WithEndpointURL(endpoint)} + } + return []otlpmetrichttp.Option{ + otlpmetrichttp.WithEndpoint(endpoint), + otlpmetrichttp.WithInsecure(), + } +} diff --git a/backend/oapi-codegen.yaml b/backend/oapi-codegen.yaml new file mode 100644 index 0000000..23e818a --- /dev/null +++ b/backend/oapi-codegen.yaml @@ -0,0 +1,9 @@ +package: api +generate: + models: true + gin-server: true + strict-server: true + embedded-spec: true +output: internal/api/generated.go +output-options: + skip-prune: false diff --git a/backend/openapi/openapi.yaml b/backend/openapi/openapi.yaml new file mode 100644 index 0000000..be9c0ba --- /dev/null +++ b/backend/openapi/openapi.yaml @@ -0,0 +1,845 @@ +openapi: 3.0.3 +info: + title: Kererū Forms API + version: 1.0.0 + description: | + API for **Kererū forms**, a simple survey/form capture system. + + Forms are versioned and can be inactivated. A form contains an ordered set of + questions whose answers are one of: true/false, numeric, a drop-down selection, + or free-form text. Responses are identified by a user identifier captured at the + start of the form. A form may optionally require a single-use access code. + contact: + name: Kererū Forms + email: support@kereru.forms + license: + name: MIT +servers: + - url: http://localhost:8080 + description: Local development + - url: http://localhost:8080/api/v1 + description: Local development (v1 base) +tags: + - name: system + description: Health and meta endpoints + - name: forms + description: Form definition and management + - name: submissions + description: Form submissions and management of captured data + - name: access-codes + description: Single-use access codes +paths: + /health: + get: + tags: [system] + operationId: getHealth + summary: Service health + responses: + '200': + description: Service is healthy + content: + application/json: + schema: + $ref: '#/components/schemas/Health' + /forms: + get: + tags: [forms] + operationId: listForms + summary: List forms + description: Returns form summaries. Use `includeInactive` to include inactivated forms (management view). + parameters: + - name: includeInactive + in: query + required: false + schema: + type: boolean + default: false + - name: activeOnly + in: query + required: false + deprecated: true + schema: + type: boolean + responses: + '200': + description: A list of forms + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/FormSummary' + '500': + $ref: '#/components/responses/Error' + post: + tags: [forms] + operationId: createForm + summary: Create a form + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateFormRequest' + responses: + '201': + description: Form created + content: + application/json: + schema: + $ref: '#/components/schemas/Form' + '400': + $ref: '#/components/responses/Error' + '500': + $ref: '#/components/responses/Error' + /forms/{formId}: + parameters: + - $ref: '#/components/parameters/FormId' + get: + tags: [forms] + operationId: getForm + summary: Get a form by id + responses: + '200': + description: The form + content: + application/json: + schema: + $ref: '#/components/schemas/Form' + '404': + $ref: '#/components/responses/Error' + '500': + $ref: '#/components/responses/Error' + put: + tags: [forms] + operationId: updateForm + summary: Update a form definition + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateFormRequest' + responses: + '200': + description: Updated form + content: + application/json: + schema: + $ref: '#/components/schemas/Form' + '400': + $ref: '#/components/responses/Error' + '404': + $ref: '#/components/responses/Error' + '500': + $ref: '#/components/responses/Error' + delete: + tags: [forms] + operationId: deleteForm + summary: Delete a form definition + responses: + '204': + description: Deleted + '404': + $ref: '#/components/responses/Error' + '500': + $ref: '#/components/responses/Error' + /forms/{formId}/inactivate: + parameters: + - $ref: '#/components/parameters/FormId' + post: + tags: [forms] + operationId: inactivateForm + summary: Inactivate a form + description: Inactivated forms remain visible in management but cannot accept new submissions. + responses: + '200': + description: Inactivated form + content: + application/json: + schema: + $ref: '#/components/schemas/Form' + '404': + $ref: '#/components/responses/Error' + '500': + $ref: '#/components/responses/Error' + /forms/{formId}/activate: + parameters: + - $ref: '#/components/parameters/FormId' + post: + tags: [forms] + operationId: activateForm + summary: Activate a form + responses: + '200': + description: Activated form + content: + application/json: + schema: + $ref: '#/components/schemas/Form' + '404': + $ref: '#/components/responses/Error' + '500': + $ref: '#/components/responses/Error' + /forms/{formId}/versions: + parameters: + - $ref: '#/components/parameters/FormId' + post: + tags: [forms] + operationId: createFormVersion + summary: Create a new version of a form + description: Clones the form and its questions into a new version with the version number incremented. The new version is active; the previous version is inactivated. + responses: + '201': + description: New form version + content: + application/json: + schema: + $ref: '#/components/schemas/Form' + '404': + $ref: '#/components/responses/Error' + '500': + $ref: '#/components/responses/Error' + /forms/{formId}/pdf: + parameters: + - $ref: '#/components/parameters/FormId' + get: + tags: [forms] + operationId: getFormPdf + summary: Download a PDF rendering of the form definition + responses: + '200': + description: PDF document generated from the form definition + content: + application/pdf: + schema: + type: string + format: binary + '404': + $ref: '#/components/responses/Error' + '500': + $ref: '#/components/responses/Error' + /forms/{formId}/submissions: + parameters: + - $ref: '#/components/parameters/FormId' + get: + tags: [submissions] + operationId: listFormSubmissions + summary: List submissions for a form (management) + responses: + '200': + description: Submissions + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/SubmissionSummary' + '404': + $ref: '#/components/responses/Error' + '500': + $ref: '#/components/responses/Error' + post: + tags: [submissions] + operationId: submitForm + summary: Submit an answer set for a form + description: | + Captures a completed form. The `userId` (captured at the start of the form) + identifies the respondent. If the form requires an access code, a valid, + unused `accessCode` must be supplied and is consumed on success. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SubmitFormRequest' + responses: + '201': + description: Submission accepted + content: + application/json: + schema: + $ref: '#/components/schemas/Submission' + '400': + $ref: '#/components/responses/Error' + '404': + $ref: '#/components/responses/Error' + '409': + $ref: '#/components/responses/Error' + '410': + $ref: '#/components/responses/Error' + '500': + $ref: '#/components/responses/Error' + /submissions/{submissionId}: + parameters: + - name: submissionId + in: path + required: true + schema: + type: string + format: uuid + get: + tags: [submissions] + operationId: getSubmission + summary: Get a submission by id (management) + responses: + '200': + description: Submission + content: + application/json: + schema: + $ref: '#/components/schemas/Submission' + '404': + $ref: '#/components/responses/Error' + '500': + $ref: '#/components/responses/Error' + /submissions/{submissionId}/offline-pdf: + parameters: + - name: submissionId + in: path + required: true + schema: + type: string + format: uuid + get: + tags: [submissions] + operationId: getSubmissionOfflinePdf + summary: Download the offline PDF uploaded with a submission + responses: + '200': + description: The uploaded PDF + content: + application/pdf: + schema: + type: string + format: binary + '404': + $ref: '#/components/responses/Error' + '500': + $ref: '#/components/responses/Error' + /forms/{formId}/access-codes: + parameters: + - $ref: '#/components/parameters/FormId' + get: + tags: [access-codes] + operationId: listAccessCodes + summary: List access codes for a form + responses: + '200': + description: Access codes + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/AccessCode' + '404': + $ref: '#/components/responses/Error' + '500': + $ref: '#/components/responses/Error' + post: + tags: [access-codes] + operationId: createAccessCodes + summary: Create one or more access codes for a form + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateAccessCodesRequest' + responses: + '201': + description: Created access codes + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/AccessCode' + '400': + $ref: '#/components/responses/Error' + '404': + $ref: '#/components/responses/Error' + '500': + $ref: '#/components/responses/Error' +components: + parameters: + FormId: + name: formId + in: path + required: true + schema: + type: string + format: uuid + responses: + Error: + description: Error response + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + schemas: + Health: + type: object + required: [status, version] + properties: + status: + type: string + example: ok + version: + type: string + example: 1.0.0 + uptimeSeconds: + type: integer + format: int64 + QuestionType: + type: string + description: | + * `boolean` – true/false + * `number` – numeric answer + * `select` – one option from a configured list + * `text` – free-form text (single or multiline) + * `date` - calendar date in `YYYY-MM-DD` form + enum: [boolean, number, select, text, date] + QuestionOption: + type: object + required: [value, label] + properties: + value: + type: string + label: + type: string + Question: + type: object + required: [id, position, key, label, type, required] + properties: + id: + type: string + format: uuid + position: + type: integer + key: + type: string + description: Stable machine key for this question within the form + label: + type: string + description: The question text (may be multiline, no formatting directives) + description: + type: string + type: + $ref: '#/components/schemas/QuestionType' + required: + type: boolean + options: + type: array + description: Present for `select` questions + items: + $ref: '#/components/schemas/QuestionOption' + minValue: + type: number + description: Minimum allowed value for `number` questions + maxValue: + type: number + description: Maximum allowed value for `number` questions + placeholder: + type: string + multiline: + type: boolean + description: Whether a `text` answer may span multiple lines + FormSummary: + type: object + required: [id, formGroupId, version, title, isActive, questionCount, createdAt, updatedAt] + properties: + id: + type: string + format: uuid + formGroupId: + type: string + format: uuid + description: Groups all versions of the same logical form + version: + type: integer + title: + type: string + description: + type: string + logoUrl: + type: string + pdfUrl: + type: string + description: Optional absolute URL of a prebuilt PDF form linked to this definition + externalReferenceRequired: + type: boolean + description: Whether a reference identifier is required before the form can be submitted + externalReferenceLabel: + type: string + description: Label shown for the external reference, for example Invoice number + isActive: + type: boolean + questionCount: + type: integer + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + Form: + type: object + required: [id, formGroupId, version, title, isActive, requiresAccessCode, questions, createdAt, updatedAt] + properties: + id: + type: string + format: uuid + formGroupId: + type: string + format: uuid + version: + type: integer + title: + type: string + description: + type: string + introduction: + type: string + description: Introduction section explaining purpose, background and support details + isActive: + type: boolean + requiresAccessCode: + type: boolean + signatureRequired: + type: boolean + description: Whether a digital signature is captured at the end of the form + logoUrl: + type: string + pdfUrl: + type: string + description: Optional absolute URL of a prebuilt PDF form linked to this definition + externalReferenceRequired: + type: boolean + description: Whether a reference identifier is required before the form can be submitted + externalReferenceLabel: + type: string + description: Label shown for the external reference, for example Invoice number + companyName: + type: string + contactEmail: + type: string + contactPhone: + type: string + contactUrl: + type: string + questions: + type: array + items: + $ref: '#/components/schemas/Question' + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + QuestionInput: + type: object + required: [key, label, type, required] + properties: + key: + type: string + label: + type: string + description: + type: string + type: + $ref: '#/components/schemas/QuestionType' + required: + type: boolean + options: + type: array + items: + $ref: '#/components/schemas/QuestionOption' + minValue: + type: number + maxValue: + type: number + placeholder: + type: string + multiline: + type: boolean + CreateFormRequest: + type: object + required: [title, questions] + properties: + title: + type: string + description: + type: string + introduction: + type: string + requiresAccessCode: + type: boolean + signatureRequired: + type: boolean + logoUrl: + type: string + pdfUrl: + type: string + externalReferenceRequired: + type: boolean + externalReferenceLabel: + type: string + companyName: + type: string + contactEmail: + type: string + contactPhone: + type: string + contactUrl: + type: string + questions: + type: array + items: + $ref: '#/components/schemas/QuestionInput' + UpdateFormRequest: + type: object + required: [title, questions] + properties: + title: + type: string + description: + type: string + introduction: + type: string + requiresAccessCode: + type: boolean + signatureRequired: + type: boolean + logoUrl: + type: string + pdfUrl: + type: string + externalReferenceRequired: + type: boolean + externalReferenceLabel: + type: string + companyName: + type: string + contactEmail: + type: string + contactPhone: + type: string + contactUrl: + type: string + questions: + type: array + items: + $ref: '#/components/schemas/QuestionInput' + UserIdType: + type: string + enum: [email, username, phone, guid] + Answer: + type: object + required: [questionId, value] + properties: + questionId: + type: string + format: uuid + value: + description: Answer value. String, number, boolean or list of strings depending on the question type. + SubmitFormRequest: + type: object + required: [userId, userIdType, answers] + properties: + userId: + type: string + description: Respondent identifier captured at the start (email, username, phone or GUID) + userIdType: + $ref: '#/components/schemas/UserIdType' + accessCode: + type: string + description: Required when the form has `requiresAccessCode = true` + externalReference: + type: string + description: |- + External reference identifier used to link this response to a record in + another system, for example an invoice, account or policy number. + answers: + type: array + items: + $ref: '#/components/schemas/Answer' + signature: + $ref: '#/components/schemas/SignatureInput' + offlinePdf: + type: string + description: | + Base64 data URL of a PDF completed offline (for example + `data:application/pdf;base64,...`). Maximum decoded size is 10 MB. + offlinePdfName: + type: string + description: Original filename of the uploaded offline PDF + tracking: + $ref: '#/components/schemas/ClientTracking' + ClientTracking: + type: object + description: Client supplied tracking details. IP and user agent are captured server-side. + properties: + locale: + type: string + timezone: + type: string + place: + type: string + referer: + type: string + SignatureType: + type: string + description: | + * `drawn` - signature drawn in the browser + * `image` - uploaded image containing a signature + enum: [drawn, image] + SignatureInput: + type: object + required: [type, dataUrl, name] + properties: + type: + $ref: '#/components/schemas/SignatureType' + dataUrl: + type: string + description: PNG or JPEG image data URL (maximum decoded size 2 MB) + name: + type: string + description: Name of the person signing + title: + type: string + description: Optional title of the person signing + Signature: + type: object + required: [type, dataUrl, name, signedAt] + properties: + type: + $ref: '#/components/schemas/SignatureType' + dataUrl: + type: string + name: + type: string + title: + type: string + signedAt: + type: string + format: date-time + AnswerRecord: + type: object + required: [questionId, questionKey, value] + properties: + questionId: + type: string + format: uuid + questionKey: + type: string + value: + description: The captured value + Tracking: + type: object + properties: + ipAddress: + type: string + userAgent: + type: string + locale: + type: string + timezone: + type: string + place: + type: string + referer: + type: string + Submission: + type: object + required: [id, formId, formVersion, userId, userIdType, answers, submittedAt] + properties: + id: + type: string + format: uuid + formId: + type: string + format: uuid + formVersion: + type: integer + userId: + type: string + userIdType: + $ref: '#/components/schemas/UserIdType' + externalReference: + type: string + answers: + type: array + items: + $ref: '#/components/schemas/AnswerRecord' + signature: + $ref: '#/components/schemas/Signature' + offlinePdfName: + type: string + description: Present when the respondent uploaded a completed offline PDF + submittedAt: + type: string + format: date-time + tracking: + $ref: '#/components/schemas/Tracking' + SubmissionSummary: + type: object + required: [id, formId, formVersion, userId, userIdType, submittedAt] + properties: + id: + type: string + format: uuid + formId: + type: string + format: uuid + formVersion: + type: integer + userId: + type: string + userIdType: + $ref: '#/components/schemas/UserIdType' + externalReference: + type: string + submittedAt: + type: string + format: date-time + AccessCode: + type: object + required: [id, formId, code, used, createdAt] + properties: + id: + type: string + format: uuid + formId: + type: string + format: uuid + code: + type: string + used: + type: boolean + usedAt: + type: string + format: date-time + createdAt: + type: string + format: date-time + CreateAccessCodesRequest: + type: object + required: [count] + properties: + count: + type: integer + minimum: 1 + maximum: 100 + prefix: + type: string + Error: + type: object + required: [error] + properties: + error: + type: string + code: + type: string diff --git a/config/.env.example b/config/.env.example new file mode 100644 index 0000000..e67c2c3 --- /dev/null +++ b/config/.env.example @@ -0,0 +1,27 @@ +# Kererū Forms environment sample +# Copy this file to .env next to the executable (or in the working directory). +# Values are never overwritten by existing environment variables. + +# Logging +KERERU_LOG_LEVEL=info +KERERU_LOG_FORMAT=console +KERERU_ENVIRONMENT=development + +# HTTP listeners (IPv4 and/or IPv6) +KERERU_LISTEN_ADDR=0.0.0.0 +KERERU_LISTEN_ADDR6= +KERERU_PORT=8080 + +# Database (SQLite by default; swap to PostgreSQL/MariaDB later via dbDriver) +KERERU_DB_DRIVER=sqlite +KERERU_DB_DSN=file:kereru.db?_pragma=busy_timeout(5000)&_pragma=foreign_keys(1) + +# Optional: directory with the built SPA (served with history fallback) +KERERU_STATIC_DIR= + +# Telemetry (OpenTelemetry OTLP) +KERERU_TELEMETRY_ENABLED=false +OTEL_EXPORTER_OTLP_ENDPOINT=localhost:4318 + +# CORS allowed origins (comma separated). Use "*" to allow all. +KERERU_CORS_ORIGINS=http://localhost:5173 diff --git a/config/config.example.json b/config/config.example.json new file mode 100644 index 0000000..7964e93 --- /dev/null +++ b/config/config.example.json @@ -0,0 +1,16 @@ +{ + "logLevel": "info", + "logFormat": "console", + "environment": "development", + "listenAddr": "0.0.0.0", + "listenAddr6": "", + "port": "8080", + "dbDriver": "sqlite", + "dbDsn": "file:kereru.db?_pragma=busy_timeout(5000)&_pragma=foreign_keys(1)", + "staticDir": "", + "telemetryEnabled": false, + "otlpEndpoint": "localhost:4318", + "corsOrigins": [ + "http://localhost:5173" + ] +} diff --git a/config/config.example.yaml b/config/config.example.yaml new file mode 100644 index 0000000..21423c5 --- /dev/null +++ b/config/config.example.yaml @@ -0,0 +1,22 @@ +# Kererū Forms configuration (YAML) +# Pass with: kereru-forms --conf ./config.yaml + +logLevel: info +logFormat: console +environment: development + +listenAddr: 0.0.0.0 +listenAddr6: "" +port: "8080" + +dbDriver: sqlite +dbDsn: "file:kereru.db?_pragma=busy_timeout(5000)&_pragma=foreign_keys(1)" + +# Optional: directory with the built SPA (served with history fallback) +staticDir: "" + +telemetryEnabled: false +otlpEndpoint: "localhost:4318" + +corsOrigins: + - "http://localhost:5173" diff --git a/deploy/k8s.yaml b/deploy/k8s.yaml new file mode 100644 index 0000000..13247f5 --- /dev/null +++ b/deploy/k8s.yaml @@ -0,0 +1,138 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: kereru-forms +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: kereru-forms-config + namespace: kereru-forms +data: + KERERU_LOG_LEVEL: info + KERERU_LOG_FORMAT: json + KERERU_ENVIRONMENT: production + KERERU_LISTEN_ADDR: 0.0.0.0 + KERERU_PORT: "8080" + KERERU_DB_DRIVER: sqlite + KERERU_DB_DSN: "file:/data/kereru.db?_pragma=busy_timeout(5000)&_pragma=foreign_keys(1)" + KERERU_STATIC_DIR: /app/static + KERERU_CORS_ORIGINS: "*" + KERERU_TELEMETRY_ENABLED: "false" + OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector.observability.svc.cluster.local:4318" +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: kereru-forms-data + namespace: kereru-forms +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: kereru-forms + namespace: kereru-forms + labels: + app: kereru-forms +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: kereru-forms + template: + metadata: + labels: + app: kereru-forms + spec: + securityContext: + runAsNonRoot: true + runAsUser: 10001 + runAsGroup: 10001 + fsGroup: 10001 + fsGroupChangePolicy: OnRootMismatch + containers: + - name: kereru-forms + image: kereru-forms:1.0.0 + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8080 + envFrom: + - configMapRef: + name: kereru-forms-config + volumeMounts: + - name: data + mountPath: /data + - name: tmp + mountPath: /tmp + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 15 + periodSeconds: 20 + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 500m + memory: 256Mi + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + volumes: + - name: data + persistentVolumeClaim: + claimName: kereru-forms-data + - name: tmp + emptyDir: {} +--- +apiVersion: v1 +kind: Service +metadata: + name: kereru-forms + namespace: kereru-forms +spec: + selector: + app: kereru-forms + ports: + - name: http + port: 80 + targetPort: http +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: kereru-forms + namespace: kereru-forms + annotations: + kubernetes.io/ingress.class: nginx +spec: + rules: + - host: forms.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: kereru-forms + port: + name: http diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..e277a7b --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,43 @@ +# syntax=docker/dockerfile:1.7 + +# --- Backend build --- +FROM golang:1.26 AS backend-builder +WORKDIR /app/backend +COPY backend/go.mod backend/go.sum ./ +RUN go mod download +COPY backend/. ./ +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w -X main.version=1.0.0" -o /kereru-forms ./cmd/kereru-forms + +# --- Frontend build --- +FROM node:22 AS frontend-builder +WORKDIR /app/frontend +COPY frontend/package*.json ./ +RUN npm ci +COPY frontend/. ./ +COPY backend/openapi /app/backend/openapi +RUN npm run build + +# --- Runtime --- +FROM alpine:3.20 +RUN apk add --no-cache ca-certificates tzdata \ + && addgroup -g 10001 -S kereru \ + && adduser -u 10001 -S -G kereru kereru \ + && mkdir -p /data /app/static \ + && chown -R kereru:kereru /data /app +WORKDIR /app +ENV KERERU_LOG_LEVEL=info \ + KERERU_LOG_FORMAT=json \ + KERERU_ENVIRONMENT=production \ + KERERU_LISTEN_ADDR=0.0.0.0 \ + KERERU_PORT=8080 \ + KERERU_DB_DRIVER=sqlite \ + KERERU_DB_DSN="file:/data/kereru.db?_pragma=busy_timeout(5000)&_pragma=foreign_keys(1)" \ + KERERU_STATIC_DIR=/app/static \ + KERERU_CORS_ORIGINS="*" +COPY --from=backend-builder /kereru-forms /app/kereru-forms +COPY --from=frontend-builder /app/frontend/dist /app/static +VOLUME ["/data"] +EXPOSE 8080 +USER kereru:kereru +ENTRYPOINT ["/app/kereru-forms"] +CMD ["--listen-addr", "0.0.0.0", "--port", "8080"] diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..6e70efb --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,33 @@ +name: kereru-forms + +services: + kereru-forms: + build: + context: .. + dockerfile: docker/Dockerfile + image: kereru-forms:1.0.0 + container_name: kereru-forms + restart: unless-stopped + ports: + - "8080:8080" + environment: + KERERU_LOG_LEVEL: info + KERERU_LOG_FORMAT: json + KERERU_ENVIRONMENT: production + KERERU_LISTEN_ADDR: 0.0.0.0 + KERERU_PORT: 8080 + KERERU_DB_DRIVER: sqlite + KERERU_DB_DSN: "file:/data/kereru.db?_pragma=busy_timeout(5000)&_pragma=foreign_keys(1)" + KERERU_CORS_ORIGINS: "http://localhost:8080" + KERERU_TELEMETRY_ENABLED: "false" + volumes: + - kereru-data:/data + healthcheck: + test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/health"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s + +volumes: + kereru-data: diff --git a/docs/AGENTS.md b/docs/AGENTS.md new file mode 100644 index 0000000..f3afbde --- /dev/null +++ b/docs/AGENTS.md @@ -0,0 +1,158 @@ +# Design + +Note **AI AGENT NOT TO UPDATE THIS FILE** + +This is the design for a simple forms site to be used to capture a number of survey type questions +fom a person. The forms will be like Adove PDF forms and allow for configuration of questions to +ask and an answer. The questions can be multiline text and do not contain formatting commands +within the question. + +The answer can be of types: + +1. True or false +2. Numeric answer +3. Drop down options +4. Free form text +5. Date field + +Project name is ``Kererū forms`` and its internal identifier is ``kereru.forms``. + +## Technology stack + +The design is based around a modern web UI using React + Vite + Tailwind CSS as the frontend and a Golang backend. + +The database is any standard SQL DB and the initial verion is using SQLite. A SQL provider +will allow future flexibility for other database SQL variants such as: + +* Postgres +* MariaDB + +## Backend + +The backend executable server name is to be ``kereru-forms``. + +The backend is to expose APIs that are defined in an OpenAPI file +which is used to generate the golang code. A health service is +to be included. + +Include telemetry, logging and observability. + +## Frontend + +Allow for themeing the UI for light, dark and system. + +Past data captured is not presented again to the user inputing the data + +A management section exists for viewing past data forms completions, and +managing forms. Forms can be inactivated and can have versions. + +A form can contain an introduction section explaining purpose, background +and further contact or support details. + +There is the ability by the user supplying the form to request a +PDF version that is generated from the form definition. +The PDF form can be generated by the person submitting the form +so that the PDF form can be completed offline and even printed. +The PDF form is to have an area for including the submitters details. +Alternatively the PDF form can be a prebuilt Adbove PDF document +linked to the survey definition. + +The form can be configured to request a unique user code for filling in +the form and once used the code cannot be used again. + +The questions are to be presented on one line with question at left and answer options on right, +except for free form text which is input below the question. + +A form can have a digital signature defined at the end which can capture +a drawn sigature, a picture file with signature, name of person and +optional title. + +Include an option at the start of the survey to upload a PDF form that +has been completed offline by the user. This upload will still need to +capture the identity of the person uploading the PDF document. + +Include UI for displaying the OpenAPI definition as a web page under path /docs. +The UI for OpenAPI definition is not to be linked to an external service +providing the rendering and must be able to be run locally for +offline dvelopment. + +For management section when viewing past submissions and answers, show +the question along with the answwer. + +## Database Schema + +The schema is to allow for multiple forms and capture the user input +using an user identifier such as email, username, international phine number or GUID. +The identifier is captured at the start. + +Forms can be versioned and include a logo and company name plus contact details that is +linked to the form. Existing forms can be edited which will generate a new version number +for the form definition. + +A form can be configured to require an external reference identifier such as +invoice number, account number, policy number. This identifier will be used +to verfiy and link the form to an external consumer of the data. + +Each form capture is to gather details about the time, place, IP, browser as tracking detais. + +## Authentication + +Include OAuth authentication for the management services including viewing past submissions. + +The OAuth is to be configurable to use external service such as Okta, KeyCloak. If no serviec provided +then a simple password is to be used so that the application can be tested locally. + +Accessing the form submission can optionally be configured to require authentication against the +OAuth identity provider. Access for submission does not require a role claim just authentication. + +Access to manage forms for CRUD purposes requires an admin role claim in the JWT. Access to view +forms that have been submitted requires a view role claim in the JWT. Uusers with admin role claim +only cannot view submissions. + +## Configuration + +The solution is to include configuration. Configuration is supplied via a JSON or YAML +file. + +The configuration file is loaded first and can be specified as argument --conf to +the backend program. Once the configuration file is loaded the values are overridden +by envionment variables that can also be supplied as a .env file in the path of +the executable. + +The following arguments can be supplied to executable as arguments, overriding +the configuration and environment inputs. + +* configuration file location +* logging level +* listening address either IPv4 and/or IPv6. Both can be supplied. +* listening port + +## Testing + +Create test cases for code in both frontend and backend. + +## Containerisation + +The solution is to be containerised and has + +* Dockerfile +* Docker compose file + +Instructions for Docker, Podman and Kubernetes setup are to be provided. + +## Documentation + +The design documentation and design decisions are to be captured in +document ``ARCHITECTURE.md`` in folder ``docs``. + +Create a sample environment and configuration file. + +## Development + +Create developer instructions for local execution and producton deployment. + +The frontend is to be executable separately from the backend +using ``npm run dev`` for local development + +The backend is to be executed in local development with standard +Golang command to run. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..62883b3 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,117 @@ +# Architecture + +## Overview + +``` +┌──────────────────────────────┐ ┌──────────────────────────────────────────┐ +│ React SPA (Vite, Tailwind) │─────▶│ kereru-forms (Go, Gin) │ +│ - form filling │ HTTP │ ┌────────────┐ ┌────────────────────┐ │ +│ - management │ │ │ OpenAPI │ │ Strict handlers │ │ +│ - light/dark/system theme │ │ │ generated │─▶│ (validation, PDF, │ │ +└──────────────────────────────┘ │ │ router │ │ access codes) │ │ + │ └────────────┘ └─────────┬──────────┘ │ + │ │ │ + │ ┌────────────┐ ┌────────▼──────────┐ │ + │ │ Telemetry │ │ Store interface │ │ + │ │ (OTLP) │ │ SQLite adapter │ │ + │ └────────────┘ └────────┬──────────┘ │ + └───────────────────────────│──────────────┘ + ▼ + SQLite database file +``` + +The API is mounted both at the root (for Kubernetes probes and direct clients) and under `/api` (used by the SPA). The backend binary can also serve the built SPA (`KERERU_STATIC_DIR`) with history fallback, so a single container is enough for most deployments. + +## Backend layout + +``` +backend/ +├── openapi/openapi.yaml Contract: paths, schemas, enums +├── oapi-codegen.yaml Code generation settings +├── cmd/kereru-forms/main.go Wiring, listeners, graceful shutdown +└── internal/ + ├── api/ Generated server + strict handlers + router + ├── config/ YAML/JSON + .env + env + flags, precedence + ├── database/ Store interface, SQLite implementation, migrations + ├── logging/ zerolog setup + Gin access logging + ├── pdf/ PDF rendering of a form definition + └── telemetry/ OpenTelemetry traces and metrics (OTLP/HTTP) +``` + +### Request flow + +1. Gin middleware: telemetry span (`otelgin`), panic recovery, request id, request metadata (client IP/user agent), structured access log, CORS. +2. `api.RegisterHandlers` routes to the **generated strict handler**, which performs OpenAPI request decoding/validation. +3. `api.Server` methods validate business rules and call the `database.Store`. +4. Responses are mapped back to generated types; errors use the `Error` schema (`{error, code}`). + +### Configuration + +`internal/config` implements a deterministic precedence chain: defaults → YAML/JSON file (`--conf`) → `.env` (next to executable or cwd; never overrides existing env) → environment variables → CLI flags. The `--conf` flag is pre-scanned so the file is available as defaults for the remaining flags. Both IPv4 and IPv6 listeners are supported and started concurrently. + +### Data model + +`forms` represents one concrete version. `form_group_id` groups all versions of the same logical form; `(form_group_id, version)` is unique. Creating a new version clones the current definition, increments `version`, activates the new row and inactivates the previous one. Historical submissions keep `form_version` and denormalised `question_key` values, so they remain readable after questions change. + +| Table | Purpose | +| --- | --- | +| `forms` | Form version metadata, contact details, state | +| `questions` | Ordered questions with type-specific configuration (JSON options, bounds) | +| `submissions` | Response header: user identifier, form version, server-captured tracking | +| `answers` | One row per answered question, value stored as JSON | +| `access_codes` | Single-use codes (`used`, `used_at`, owning submission) | +| `schema_migrations` | Applied schema versions | + +All timestamps are stored as RFC 3339 UTC strings. IDs are UUIDs (TEXT). + +### Validation + +Form definitions: non-empty title, unique keys (`[A-Za-z][A-Za-z0-9_-]*`), supported question types, select questions require unique non-empty option values, numeric bounds ordered. + +Submissions: identifier type must be `email|username|phone|guid`; email identifiers are parsed; answers must reference questions of that form; duplicates rejected; required questions enforced; type checks for boolean/number/select/text; numeric bounds; select values must exist; text length capped and single-line questions reject newlines. + +Access codes are consumed in the same SQL transaction as the submission insert. The conditional `UPDATE ... WHERE used = 0` makes reuse safe under concurrency; failure rolls back the submission. + +### Observability + +- **Logs**: zerolog, JSON or console, level configurable. Access logs include method, path, status, latency, client IP, user agent and `X-Request-ID`. +- **Traces/metrics**: OpenTelemetry SDK with OTLP/HTTP exporters. Spans cover HTTP requests (`otelgin`) and individual use cases (`GetForm`, `SubmitForm`, `GetFormPdf`, …) with form/submission attributes. Metrics are exported every 15s. Telemetry is opt-in (`KERERU_TELEMETRY_ENABLED`). +- **Health**: `GET /health` returns status, version and uptime for liveness/readiness probes. + +### PDF + +`internal/pdf` renders the form definition with `go-pdf/fpdf`: title, description, introduction, contact block, numbered questions, type hints, options and answer lines. Built-in PDF fonts cover WinAnsi; macrons and common typographic characters are transliterated so the output is always valid. For full Unicode, embed a TTF font instead. + +## Frontend layout + +``` +frontend/src/ +├── api/… (reserved) +├── components/ Layout, ThemeToggle, QuestionField, StatusBadge +├── hooks/useTheme light/dark/system with localStorage + matchMedia +├── lib/api.ts Typed fetch client, ApiError, PDF download +├── lib/types.ts Mirrors the OpenAPI schemas +├── pages/ Home, FillForm, Manage, FormBuilder, FormDetail, SubmissionDetail +└── test/ Vitest + Testing Library +``` + +Theming uses Tailwind's `class` dark mode. An inline script in `index.html` applies the stored preference before paint to avoid a flash. The `useTheme` hook keeps `documentElement.dark`, `localStorage` and the OS preference in sync. + +Forms may carry a company name, contact details and a logo URL; the UI renders the logo on the home, fill and detail views. Remote logos are not embedded in the generated PDF. + +The fill flow asks for the respondent identifier and (optionally) an access code first, then renders the questions by type. On success the local answers are cleared and only the confirmation is shown — previously entered data is never re-presented. The management section lists active and inactive versions and supports activation, versioning, deletion, PDF download, access-code generation and submission review. + +## Extending the database + +`database.Store` is the boundary used by the API layer. To add PostgreSQL or MariaDB: + +1. Add a new adapter implementing `Store` (or generalise the SQL adapter with a dialect for placeholders and upserts). +2. Extend `database.Open` with the driver case and adjust migrations (TEXT timestamps and JSON strings map cleanly to `timestamptz`/`jsonb` and `varchar`/`json`). +3. Keep transaction semantics for submission + access-code consumption. + +## Known limitations / next steps + +- Management endpoints have no authentication or authorisation yet; front them with an auth proxy or add an auth layer. +- SQLite is a single-writer store; for horizontal scaling use a network database adapter and remove the single-connection pool limit. +- Access codes are generated with 8 characters from a 32-symbol alphabet (~40 bits) — sufficient for single-use gating, not for secrets. +- PDF output uses built-in fonts and does not embed form logos (see above). diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md new file mode 100644 index 0000000..d46bc24 --- /dev/null +++ b/docs/DEPLOYMENT.md @@ -0,0 +1,156 @@ +# Deployment + +## Local development + +### Backend + +```bash +cd backend +go mod download +go run ./cmd/kereru-forms \ + --listen-addr 127.0.0.1 \ + --port 8080 \ + --log-format console \ + --db-dsn "file:./kereru-dev.db?_pragma=busy_timeout(5000)&_pragma=foreign_keys(1)" +``` + +Useful flags: + +```bash +kereru-forms --help +``` + +### Frontend + +```bash +cd frontend +npm install +npm run dev +``` + +Vite serves on and proxies `/api/*` to `http://localhost:8080`. To point at another API host, set `VITE_API_BASE_URL` at build/dev time: + +```bash +VITE_API_BASE_URL=http://localhost:8080 npm run dev +``` + +## Production build without containers + +```bash +# Backend binary +cd backend +CGO_ENABLED=0 go build -ldflags "-s -w -X main.version=1.0.0" -o kereru-forms ./cmd/kereru-forms + +# Frontend assets +cd ../frontend +npm ci +npm run build +``` + +Run the binary with the SPA directory and a persistent database: + +```bash +KERERU_STATIC_DIR=../frontend/dist \ +KERERU_DB_DSN="file:/var/lib/kereru/kereru.db?_pragma=busy_timeout(5000)&_pragma=foreign_keys(1)" \ +KERERU_LOG_FORMAT=json \ +KERERU_ENVIRONMENT=production \ +./kereru-forms --listen-addr 0.0.0.0 --port 8080 +``` + +The binary serves the API at `/health`, `/forms`, `/submissions` and the SPA for every other path. + +## Docker + +Build from the repository root so both `backend/` and `frontend/` are in context: + +```bash +docker build -f docker/Dockerfile -t kereru-forms:1.0.0 . +docker run -d --name kereru-forms \ + -p 8080:8080 \ + -v kereru-data:/data \ + -e KERERU_CORS_ORIGINS="https://forms.example.com" \ + kereru-forms:1.0.0 +``` + +Or with Compose: + +```bash +docker compose -f docker/docker-compose.yml up -d --build +``` + +The image runs as an unprivileged user, stores SQLite data in `/data`, and serves the built frontend from `/app/static`. + +## Podman + +The image is OCI-compatible. Use rootless Podman: + +```bash +podman build -f docker/Dockerfile -t kereru-forms:1.0.0 . +podman run -d --name kereru-forms \ + -p 8080:8080 \ + -v kereru-data:/data:Z \ + -e KERERU_CORS_ORIGINS="*" \ + kereru-forms:1.0.0 +``` + +With Compose (either `podman-compose` or `docker compose` using the Podman socket): + +```bash +podman-compose -f docker/docker-compose.yml up -d --build +``` + +`podman generate systemd --new --name kereru-forms` (or a Quadlet unit) can turn the container into a system service. + +## Kubernetes + +1. Build, tag and push the image to a registry reachable by the cluster: + + ```bash + docker build -f docker/Dockerfile -t registry.example.com/kereru-forms:1.0.0 . + docker push registry.example.com/kereru-forms:1.0.0 + ``` + +2. Update `image:` and the Ingress host in [`deploy/k8s.yaml`](../deploy/k8s.yaml). + +3. Apply: + + ```bash + kubectl apply -f deploy/k8s.yaml + kubectl -n kereru-forms rollout status deployment/kereru-forms + ``` + +The manifest includes a namespace, config map, 1 GiB `ReadWriteOnce` PVC mounted at `/data`, a `Recreate` deployment (SQLite is single-writer), liveness/readiness probes on `/health`, a service on port 80 and an Ingress. A writable `/tmp` emptyDir is mounted because the container root filesystem is read-only. + +### Scaling notes + +SQLite requires a single writer. Keep `replicas: 1` and use the `Recreate` strategy. To scale horizontally, implement a network database adapter (PostgreSQL/MariaDB) behind `database.Store`, then switch `dbDriver`/`dbDsn` and lift the Deployment restrictions. + +Use a `ReadWriteMany` volume only if you replace SQLite; do not share a SQLite file across pods. + +## Configuration in production + +Prefer environment variables (twelve-factor) or a mounted YAML/JSON file: + +```bash +kereru-forms --conf /etc/kereru/config.yaml +``` + +Typical production settings: + +- `KERERU_LOG_FORMAT=json` +- `KERERU_ENVIRONMENT=production` +- `KERERU_CORS_ORIGINS=https://forms.example.com` +- `KERERU_TELEMETRY_ENABLED=true` and `OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4318` +- `KERERU_STATIC_DIR=/app/static` +- SQLite DSN on a persistent volume: `file:/data/kereru.db?_pragma=busy_timeout(5000)&_pragma=foreign_keys(1)` + +### Backups + +For SQLite, back up the database file while the service is stopped, or use the SQLite online backup API/`.backup` command. Keep the volume and file permissions owned by the runtime user (UID used in the container image). + +### Security + +- Terminate TLS at the ingress/load balancer. +- Management routes (`/forms`, `/submissions`, `/forms/{id}/pdf`) are not authenticated; restrict them with an auth proxy or network policy. +- Set `KERERU_CORS_ORIGINS` to the exact frontend origin in production. +- Do not expose the SQLite file over a network share. diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..f5f480b --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,25 @@ + + + + + + + + Kererū Forms + + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..6f651b5 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,6308 @@ +{ + "name": "kereru-forms-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "kereru-forms-frontend", + "version": "1.0.0", + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.28.0", + "swagger-ui-react": "^5.34.0" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.0.1", + "@testing-library/user-event": "^14.5.2", + "@types/node": "^26.6.5", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@types/swagger-ui-react": "^5.18.0", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "jsdom": "^25.0.1", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.17", + "typescript": "^5.6.3", + "vite": "^5.4.11", + "vitest": "^2.1.8" + } + }, + "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/@alloc/quick-lru": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.3.0.tgz", + "integrity": "sha512-U4+70Pc5ZS9osnCBCE5Jha/ciHM+Yp+CNMNC/7HvYbNRk1Ldd+f7qO65W5qfhu/TCv+/ozljlXXe9Nj8419DMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/@asamuzakjp/css-color/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/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.10.tgz", + "integrity": "sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.10.tgz", + "integrity": "sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime-corejs3": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.29.10.tgz", + "integrity": "sha512-R4FKrg4Vy0hB7PfAxf9LOorEPd5/36SCIXPXAvjlQaQmPvtrjwoDXV15jsSm2ecsqBW1Iewwb7mEnG83xIEApg==", + "license": "MIT", + "dependencies": { + "core-js-pure": "^3.48.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.10.tgz", + "integrity": "sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.9", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@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/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.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/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.4", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.4.tgz", + "integrity": "sha512-q7j5geK7xs3UJSdm9/iytUNclBnLmYx1EnSeCFXHPeutdqgIMeFeHtUZgS3EhlKxdBEAu8OwtJCwmLrEzpSs7Q==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.65.0.tgz", + "integrity": "sha512-t2AnVba25baFdq7gczzS13JyVcdubLVbFX1X04mxrggDrTQPUXNv3XM0k9H1hr5tqvg7AVvtQ720QHAj0iYqhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.65.0.tgz", + "integrity": "sha512-dNONUaEwPGhl/nuO2KV8VABWlcjYyU6KtZGN8dQv5OS5JU7JSCxPijVNKXbRJd0iuTzTiJ4TjPt/4bbvDRNiMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.65.0.tgz", + "integrity": "sha512-zWQqUH+9zavr2Bxuzfpuv2SIF3X31cr58TvSQC9o1GhpOvkqRwr60GkZdQcudyT/BEgdpdSafjgd2I13MbTL7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.65.0.tgz", + "integrity": "sha512-RQYreBFWtO4//hIuOaZkYUnF/lPU4lz35dveDimDshWCJvVisVtJ/shmT9bkXK8wFJY+FJF89X6F3Cq1qI3LCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.65.0.tgz", + "integrity": "sha512-A0NfMj6Cuh/9BE5k3awJKv6HoxFVXIcLAb6SZqAMoxUHPA1ZNNK4RsGuUPD/vL7nEAm9LG+ZzXWWTrnC0XtkpA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.65.0.tgz", + "integrity": "sha512-BI6/H7q3aP0I7S7GOrjyxTjgEJlSnovXE38+bdxBpuKN6z+d//uF+T+VQ3Q7ltJL/71p17ufartARX9Ja5gOGQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.65.0.tgz", + "integrity": "sha512-W1vUprf09RWaVWnsFP1vlUkEWJzTZlpObOEAYZXt9Fq0UVAOYEkeGr7wBX7McKqqIQskXdBnpiOMgwPujordgA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.65.0.tgz", + "integrity": "sha512-s8QTcZWgrl9haFzEGIqyE5BWyo89pFDGLMBFZ752WYxk8DJwNU/O1X6pZgPw829d9/sm6CCHyrMP3aurt76p1Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.65.0.tgz", + "integrity": "sha512-ke1f6dLRzkqveyjjF2fkEwQYZvxHtNNwjegY8nJrwcO0Bdqr8CzFO/pKE0Bns13cx0qz2Ba2Y0Hs3dSwMdnHxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.65.0.tgz", + "integrity": "sha512-926SMeUcy4y18Rz/w2aJiUKOnyAnm6czg72bXkQEaeemsll+Wr0hnsjb2BGRpHvWgwAMYy5ACJCotbvfLCfj0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.65.0.tgz", + "integrity": "sha512-6T7JNRP1hc4hqF2dUsEoVLOt9K8tmODoCLldtQRYnrddGGQY8Gqe2pM/qMMUo7EMowpP7oa2N95k0/8sYK/ipw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.65.0.tgz", + "integrity": "sha512-SImGFl1Je5kpyAgOCPONVT7h6ASV8mAUwGRjY7eZvVjPg//PfHOXgI+P241RF/+HqsA4PP3qD527ysWFKNQyhQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.65.0.tgz", + "integrity": "sha512-XeDTbmPF1Yu5asfXGPrMkWoxTDhgryQbmw5Q1+R582O51VNCk23J6f2pDrsAp5YkneBVBwtExx6ZGOb9D2aQ6A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.65.0.tgz", + "integrity": "sha512-VS55JUI/1nsljmV/jfqNkLZXFw8uMO8nNlfoHmSevNf6fO3neuhyDvx9dv3c/Ytt8r/431SL8Qax1dpYvOMZTg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.65.0.tgz", + "integrity": "sha512-Vblw84TbX7RS4la1Y4+yQ7aore/oSyaNtFbppuBr5Vs/4LfBeXKq9dIfoExw6zOvRERqd2rRAjnZRaMhH/0x5w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.65.0.tgz", + "integrity": "sha512-7+gHjtONvKgwR9InTKFaLq53f6YzBJkgeMRBENdWTsQr+spahGqQm8HF5aXpq8i4JaRrOqYyv6Zn1WltDTxaZQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.65.0.tgz", + "integrity": "sha512-KvNDC0J6s4vVHhtJ6KrDXU0Er0au/hmRIBq/+1maCV8TseGupeKizq0bWcrbp2HE1T8xoknvN0Q/KZ/YF0qX3w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.65.0.tgz", + "integrity": "sha512-GExdD15WPvu/x/drELjVam7vX8EsPgvTMOa/iJL/p/4m/oOqzbb7JXZg8fXR4hlXtg/sF2zyTZ3bR0AEtEMlhQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.65.0.tgz", + "integrity": "sha512-8sT9PemIZfIMED4Vd4KGoOXVDj5CMSWNcGdbheRUx9TYS03yOWdT2Q0lXPyEyNBB9YTilwewIQKNIBk6Nxi0Kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.65.0.tgz", + "integrity": "sha512-pO6Ehi0jo5yE0R6PltKhJhv9mlIiyzBJk5N5NUF2OF6xtVKI+1bdXxj/rb0jGXy3hck1ELVT0QPY3tzkAm360w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.65.0.tgz", + "integrity": "sha512-vo7N3/+cjjPDYr8vtxC+qSNgWa9sDvJ7g8N8R5SWqFY4WiNH1ULcAU70SKVyHeiIZOYYwNP9pTipmhN5sl1NPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.65.0.tgz", + "integrity": "sha512-ArwvgFAbjKcDM7qqotZ4OVYf+UMT9xH2YzkCzK4+AuZSDihMlz1Ekoe4INwBVXzn2mnJzuIYOcSx07iTBijGRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.65.0.tgz", + "integrity": "sha512-QGMmK4/cxKE/36FoLMSHQBCyymv/eFTnsCvvsBF0rgmOvVbvX78VWZRerL3dHa77f4pM6Xcc2iGDhJ10w/svKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.65.0.tgz", + "integrity": "sha512-XfKYFWHIPs4HPODIMsGF1IKlS3H1e3WokUZIgpmn4+uNnZBlnRDDHEylNyazbQXwhMKZre68OjtRYbKqb5+j8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.65.0.tgz", + "integrity": "sha512-wPWi++K4Bs1VR4XihOhfGi4IGX6Cub7FlerTf8X2v+2BtDHddgSqWCXwGD+h2yAe4/niIhNDwAltioOC+wEogQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "node_modules/@swagger-api/apidom-ast": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ast/-/apidom-ast-1.13.3.tgz", + "integrity": "sha512-5722GHs+iLl4F5EbyReh+d0XEmG6t4F1BRTPEjHBrmfboBICi7u1cwwZ2wsCskQl0TehEl3RX1ZJWPmpkzjQFw==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-error": "^1.13.3", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0", + "unraw": "^3.0.0" + } + }, + "node_modules/@swagger-api/apidom-core": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-core/-/apidom-core-1.13.3.tgz", + "integrity": "sha512-Sgd8pWucD2vs055koRTrRG1zH+pj9VXdyLtjKKC/iML6eAFgd0lmLccK2aFNGt2/36olIzNODeRMTPRnufZgCA==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-ast": "^1.13.3", + "@swagger-api/apidom-error": "^1.13.3", + "@types/ramda": "~0.30.0", + "minim": "~0.23.8", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0", + "short-unique-id": "^5.3.2", + "ts-mixer": "^6.0.3" + } + }, + "node_modules/@swagger-api/apidom-error": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-error/-/apidom-error-1.13.3.tgz", + "integrity": "sha512-5erobaeuRu5rAwcL/ljE3jGuX/ILENq3zLU8rZCV0/rvlc5vuGGhyowt8UmI79L7y+vifYFs6d+kcfn/di0T6A==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime-corejs3": "^7.20.7" + } + }, + "node_modules/@swagger-api/apidom-json-pointer": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-json-pointer/-/apidom-json-pointer-1.13.3.tgz", + "integrity": "sha512-eQaMH3HnR5eElsBQc5IueDsZoK76uCnW78JZDP2uLDTq3Lqx0hzUyFnZUSwY/XrLmfcLWtJiSABezEU/DcfJBg==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.13.3", + "@swagger-api/apidom-error": "^1.13.3", + "@swaggerexpert/json-pointer": "^2.10.1" + } + }, + "node_modules/@swagger-api/apidom-ns-a2a-1": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-a2a-1/-/apidom-ns-a2a-1-1.13.3.tgz", + "integrity": "sha512-TZEmV3E2Hji9dDm7e1wAckMnqFGdxnFSrbDtwKbubSfYgshSMqx0Vc3mQu/IsigqSmZZLWgmOI9iq737SqraZQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.13.3", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0", + "ts-mixer": "^6.0.3" + } + }, + "node_modules/@swagger-api/apidom-ns-api-design-systems": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-api-design-systems/-/apidom-ns-api-design-systems-1.13.3.tgz", + "integrity": "sha512-00aflYATDo1RbkLNvAq/lS3aEZHfCKFwBZbSSvoLAXsIguzKYCCIjm5F+4fWLIiToL7junHvnUuS9PHTMre7bQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.13.3", + "@swagger-api/apidom-error": "^1.13.3", + "@swagger-api/apidom-ns-openapi-3-1": "^1.13.3", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0", + "ts-mixer": "^6.0.3" + } + }, + "node_modules/@swagger-api/apidom-ns-arazzo-1": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-arazzo-1/-/apidom-ns-arazzo-1-1.13.3.tgz", + "integrity": "sha512-42apSY7ttFRHIX7JDCH494IGwd6AhS7Owj4ftdfzhZUj1ZQU8YcXzunmlwig4tw1RyOR3hBzH51h0uvWO6M2+w==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.13.3", + "@swagger-api/apidom-ns-json-schema-2020-12": "^1.13.3", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0", + "ts-mixer": "^6.0.3" + } + }, + "node_modules/@swagger-api/apidom-ns-asyncapi-2": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-asyncapi-2/-/apidom-ns-asyncapi-2-1.13.3.tgz", + "integrity": "sha512-TrUYjBi8obSkt30/rqmDS9mNepoPbJCF1zrnMVKx2EmYMqq3S35ZRC+PJSqCDCZ7VlVm8oE4/jtRm1LCmCPFvg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.13.3", + "@swagger-api/apidom-ns-json-schema-draft-7": "^1.13.3", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0", + "ts-mixer": "^6.0.3" + } + }, + "node_modules/@swagger-api/apidom-ns-asyncapi-3": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-asyncapi-3/-/apidom-ns-asyncapi-3-1.13.3.tgz", + "integrity": "sha512-uN8dK3zUS0rR2uWgeH7X2AGVzsupBjJqz3jebkneTdPm9r8xmJuKhPboXiafrveqv0V9BGFUcMSn9S9FPyD1Cg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.13.3", + "@swagger-api/apidom-ns-asyncapi-2": "^1.13.3", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0", + "ts-mixer": "^6.0.3" + } + }, + "node_modules/@swagger-api/apidom-ns-json-schema-2019-09": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-json-schema-2019-09/-/apidom-ns-json-schema-2019-09-1.13.3.tgz", + "integrity": "sha512-W0P/yN5ZDxjEFb2W+f7NK1XokhBxXvfhWVSPoze7K+bF4ekcB2qkfxAdsI38FWvlxQGL6TQETxQna9Lp1d7blw==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.13.3", + "@swagger-api/apidom-error": "^1.13.3", + "@swagger-api/apidom-ns-json-schema-draft-7": "^1.13.3", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0", + "ts-mixer": "^6.0.4" + } + }, + "node_modules/@swagger-api/apidom-ns-json-schema-2020-12": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-json-schema-2020-12/-/apidom-ns-json-schema-2020-12-1.13.3.tgz", + "integrity": "sha512-fF5Wqvmb2vvx0o0Tr5Vcz+Q6gedVi7oGeKJWaSBA7WJ0awUz+PCIW3OPiKhcxdVW2/Zrxd5kMELDCmarnHS65Q==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.13.3", + "@swagger-api/apidom-error": "^1.13.3", + "@swagger-api/apidom-ns-json-schema-2019-09": "^1.13.3", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0", + "ts-mixer": "^6.0.4" + } + }, + "node_modules/@swagger-api/apidom-ns-json-schema-draft-4": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-json-schema-draft-4/-/apidom-ns-json-schema-draft-4-1.13.3.tgz", + "integrity": "sha512-8jh6dhDKskIPP/76aIMlNpCHf5oHHyMuAe4FL2nw3fat3GOq/l0ea0a/6E1tjUJLokAaES0sWTkxYgNRWMBrBg==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-ast": "^1.13.3", + "@swagger-api/apidom-core": "^1.13.3", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0", + "ts-mixer": "^6.0.4" + } + }, + "node_modules/@swagger-api/apidom-ns-json-schema-draft-6": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-json-schema-draft-6/-/apidom-ns-json-schema-draft-6-1.13.3.tgz", + "integrity": "sha512-axUur9XmKCi42WSGceBXKwnqPTMMVF+0Etaksazo5j7ydWLEM0J1EyeeE7uZlpaoIA1/ev2m09j2v1LNOc6NYg==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.13.3", + "@swagger-api/apidom-error": "^1.13.3", + "@swagger-api/apidom-ns-json-schema-draft-4": "^1.13.3", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0", + "ts-mixer": "^6.0.4" + } + }, + "node_modules/@swagger-api/apidom-ns-json-schema-draft-7": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-json-schema-draft-7/-/apidom-ns-json-schema-draft-7-1.13.3.tgz", + "integrity": "sha512-AyIMoer1bFhn3ULeEe6EMQDcD4b4JiTWQMvS/qDUoQqY6fCLPu6er/6cxcjcgTgf8L86aG6PhPxucnYDWr0DFQ==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.13.3", + "@swagger-api/apidom-error": "^1.13.3", + "@swagger-api/apidom-ns-json-schema-draft-6": "^1.13.3", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0", + "ts-mixer": "^6.0.4" + } + }, + "node_modules/@swagger-api/apidom-ns-openapi-2": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-openapi-2/-/apidom-ns-openapi-2-1.13.3.tgz", + "integrity": "sha512-ndrNoyETfWPOvoqNIQ19yoYGVhXlrLCK2xanTn+IGJ4hg/9vK2Le4Qmiv1O/E90hODs5JPx1WuZBRQikKOLW8Q==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.13.3", + "@swagger-api/apidom-error": "^1.13.3", + "@swagger-api/apidom-ns-json-schema-draft-4": "^1.13.3", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0", + "ts-mixer": "^6.0.3" + } + }, + "node_modules/@swagger-api/apidom-ns-openapi-3-0": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-openapi-3-0/-/apidom-ns-openapi-3-0-1.13.3.tgz", + "integrity": "sha512-emLzxkwOb/RCenwZ6DRukuTAWwwqcM6a76gpHL8rq9Ax8AEbmKB0Ex25tqc/HWXRRTOioylSuotluUvHcRqXEQ==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.13.3", + "@swagger-api/apidom-error": "^1.13.3", + "@swagger-api/apidom-ns-json-schema-draft-4": "^1.13.3", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0", + "ts-mixer": "^6.0.3" + } + }, + "node_modules/@swagger-api/apidom-ns-openapi-3-1": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-openapi-3-1/-/apidom-ns-openapi-3-1-1.13.3.tgz", + "integrity": "sha512-Oh1lonFFB1sEATek6e18efUMNG+ttXWrRHrblJtVMx7cUelP1BTvBt4ykrvlnXw4EoG8bIHwLtD6yVgCX1mcpw==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-ast": "^1.13.3", + "@swagger-api/apidom-core": "^1.13.3", + "@swagger-api/apidom-json-pointer": "^1.13.3", + "@swagger-api/apidom-ns-json-schema-2020-12": "^1.13.3", + "@swagger-api/apidom-ns-openapi-3-0": "^1.13.3", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0", + "ts-mixer": "^6.0.3" + } + }, + "node_modules/@swagger-api/apidom-ns-openapi-3-2": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-openapi-3-2/-/apidom-ns-openapi-3-2-1.13.3.tgz", + "integrity": "sha512-1wUZvT2E71WorxQDy9zxrq2GJud7wUO6r/oDPTldnRiagAe/KXyLyRbucxeaLqh417wCg2ySGyClZVUT5QwE9g==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-ast": "^1.13.3", + "@swagger-api/apidom-core": "^1.13.3", + "@swagger-api/apidom-json-pointer": "^1.13.3", + "@swagger-api/apidom-ns-json-schema-2020-12": "^1.13.3", + "@swagger-api/apidom-ns-openapi-3-0": "^1.13.3", + "@swagger-api/apidom-ns-openapi-3-1": "^1.13.3", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0", + "ts-mixer": "^6.0.3" + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-a2a-json-1": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-a2a-json-1/-/apidom-parser-adapter-a2a-json-1-1.13.3.tgz", + "integrity": "sha512-v1GCfvk/goJBlroHSW5gVHWI0OgHG3HfsfHR6XlWwronJaaTq/Adz7yE/rNyjC+Wfmi7VVhuNoWvOUuWjbWr3w==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.13.3", + "@swagger-api/apidom-ns-a2a-1": "^1.13.3", + "@swagger-api/apidom-parser-adapter-json": "^1.13.3", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0" + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-a2a-yaml-1": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-a2a-yaml-1/-/apidom-parser-adapter-a2a-yaml-1-1.13.3.tgz", + "integrity": "sha512-WMDimop6PdvAoNKymE5sm4xclvEfFFQhASKjY48t9E7gYg0qterZKspNBVDw0Zd6OP/Lg4NxmdafPlkhD5JTQA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.13.3", + "@swagger-api/apidom-ns-a2a-1": "^1.13.3", + "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.13.3", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0" + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-api-design-systems-json": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-api-design-systems-json/-/apidom-parser-adapter-api-design-systems-json-1.13.3.tgz", + "integrity": "sha512-eaoQxXaMBBqYlNxYDbmzNyYCfn73TiqVuTQxfDNENtQmX36w3c/T7fBwOjfL41GqzzJF6pzVvu3jFHOFgse0Ww==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.13.3", + "@swagger-api/apidom-ns-api-design-systems": "^1.13.3", + "@swagger-api/apidom-parser-adapter-json": "^1.13.3", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0" + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-api-design-systems-yaml": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-api-design-systems-yaml/-/apidom-parser-adapter-api-design-systems-yaml-1.13.3.tgz", + "integrity": "sha512-Ne6oCXVaanpN1h4ThuOhiF4HDUN3lZmSfpkJt5qJrsMYNGIVqIIEjrqhVs3MDK+gSa+oQszHnbZSkePz+6dsJQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.13.3", + "@swagger-api/apidom-ns-api-design-systems": "^1.13.3", + "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.13.3", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0" + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-arazzo-json-1": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-arazzo-json-1/-/apidom-parser-adapter-arazzo-json-1-1.13.3.tgz", + "integrity": "sha512-UTV8YHKZInMCyXDBQq4rmgW+dK9rQ7tjRFjr4lzXWMZuBPcd0+ot4Wh0azn7NC8vXqa1qBX6a+pqX9hUVLRXQA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.13.3", + "@swagger-api/apidom-ns-arazzo-1": "^1.13.3", + "@swagger-api/apidom-parser-adapter-json": "^1.13.3", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0" + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-arazzo-yaml-1": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-arazzo-yaml-1/-/apidom-parser-adapter-arazzo-yaml-1-1.13.3.tgz", + "integrity": "sha512-s0eT30Yy0izzZIRSJfQedwI0nOFMLs5E5wRzAUje8DI2qdSlFjA1AFtDtylMq9TQPn9y6AG2ysPy/TecfigdbQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.13.3", + "@swagger-api/apidom-ns-arazzo-1": "^1.13.3", + "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.13.3", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0" + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-asyncapi-json-2": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-asyncapi-json-2/-/apidom-parser-adapter-asyncapi-json-2-1.13.3.tgz", + "integrity": "sha512-u7OXUI17VK+SX6j8whSBNPnMl85fvexFyu1lbrdpNIJP3gojUgvSZN6g9w0pII7VYBfsYDkDLKQemYVQtK4jEA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.13.3", + "@swagger-api/apidom-ns-asyncapi-2": "^1.13.3", + "@swagger-api/apidom-parser-adapter-json": "^1.13.3", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0" + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-asyncapi-json-3": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-asyncapi-json-3/-/apidom-parser-adapter-asyncapi-json-3-1.13.3.tgz", + "integrity": "sha512-Z65quIJg7z74wGJH/JuAbGM92FsWxmHKrZQl7/0yMAzI56mATUTgDWNXbm51JypRzSA393MKpFzy0dinU0eN+A==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.13.3", + "@swagger-api/apidom-ns-asyncapi-3": "^1.13.3", + "@swagger-api/apidom-parser-adapter-json": "^1.13.3", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0" + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-asyncapi-yaml-2": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-asyncapi-yaml-2/-/apidom-parser-adapter-asyncapi-yaml-2-1.13.3.tgz", + "integrity": "sha512-18m7ubllmlEp1xUL2idh7u5QgJbQVkLVYtQvIalvVHC8Lr6I4kbG4LiZXjxGx1SBg+BpPV1jjlQ+bG+Iz4Vh7w==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.13.3", + "@swagger-api/apidom-ns-asyncapi-2": "^1.13.3", + "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.13.3", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0" + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-asyncapi-yaml-3": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-asyncapi-yaml-3/-/apidom-parser-adapter-asyncapi-yaml-3-1.13.3.tgz", + "integrity": "sha512-iuLrgNFv1mujaHkeO1xTRvrP28RMtwT3D6zq8RmqHrdlXEYd9RFTsecuBBax6ZyB4WRU3r/bbTPhy2UfhM0vaw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.13.3", + "@swagger-api/apidom-ns-asyncapi-3": "^1.13.3", + "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.13.3", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0" + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-json": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-json/-/apidom-parser-adapter-json-1.13.3.tgz", + "integrity": "sha512-1TbxPuoh+ZSoGo0bcGnAeOFANU8GPTYVt1jsDnVIhNQL/DU/27hCuKse5xiystKNt6qiGyAQ1wEKqtaBedojzg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-ast": "^1.13.3", + "@swagger-api/apidom-core": "^1.13.3", + "@swagger-api/apidom-error": "^1.13.3", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0", + "tree-sitter": "=0.21.1", + "tree-sitter-json": "=0.24.8", + "web-tree-sitter": "=0.24.5" + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-openapi-json-2": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-json-2/-/apidom-parser-adapter-openapi-json-2-1.13.3.tgz", + "integrity": "sha512-gwc2lBGvmMlHXPKU5a8isCbGvo1CDg84uEjp3A/zT3SSYXaEBKOqrqLFvSVoPAtVo3tpTTwtSoxodESxdl1YLw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.13.3", + "@swagger-api/apidom-ns-openapi-2": "^1.13.3", + "@swagger-api/apidom-parser-adapter-json": "^1.13.3", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0" + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-openapi-json-3-0": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-json-3-0/-/apidom-parser-adapter-openapi-json-3-0-1.13.3.tgz", + "integrity": "sha512-VrW34GMn8PerjUBCNDHJpulSEAH8BsUAV09jPw4PhVsIFSombRbEflLDH8cpmLYS4l2ywNVgn6YiZKy92jnJ6Q==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.13.3", + "@swagger-api/apidom-ns-openapi-3-0": "^1.13.3", + "@swagger-api/apidom-parser-adapter-json": "^1.13.3", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0" + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-openapi-json-3-1": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-json-3-1/-/apidom-parser-adapter-openapi-json-3-1-1.13.3.tgz", + "integrity": "sha512-h06hVkFgpRiyWes2kRBKvquAVozIjAXBmEV7xowyo1hRyE+45VFPjey/pdaV6YWiSCekLZ2kRYh4U4b+U+oL0w==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.13.3", + "@swagger-api/apidom-ns-openapi-3-1": "^1.13.3", + "@swagger-api/apidom-parser-adapter-json": "^1.13.3", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0" + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-openapi-json-3-2": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-json-3-2/-/apidom-parser-adapter-openapi-json-3-2-1.13.3.tgz", + "integrity": "sha512-QsVjugqHqwM4VvN2DIIHpHt+5clvliHOBAj/WBYeiCBTBBI6m1/2hlG2Vu7jyMEpA88ML1WLK8JOHf2+fOwN1A==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.13.3", + "@swagger-api/apidom-ns-openapi-3-2": "^1.13.3", + "@swagger-api/apidom-parser-adapter-json": "^1.13.3", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0" + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-openapi-yaml-2": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-yaml-2/-/apidom-parser-adapter-openapi-yaml-2-1.13.3.tgz", + "integrity": "sha512-lWTxEXmISAE5qN3H0sKLZyNdlBTM3LwyeRbkuGSLbzT8SyIAL37chiuYaBNRfdA1Y8x9xbIdimCWZpACet4Glw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.13.3", + "@swagger-api/apidom-ns-openapi-2": "^1.13.3", + "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.13.3", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0" + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-openapi-yaml-3-0": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-yaml-3-0/-/apidom-parser-adapter-openapi-yaml-3-0-1.13.3.tgz", + "integrity": "sha512-EDmWNX7OFaHQl8YIRNdH06aehJZr3qt+1p2Qzrc2vn0adnmw9lQX3FvBUI44A5w3R/Q2z0iMtOXIo3baqr7MZQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.13.3", + "@swagger-api/apidom-ns-openapi-3-0": "^1.13.3", + "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.13.3", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0" + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-openapi-yaml-3-1": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-yaml-3-1/-/apidom-parser-adapter-openapi-yaml-3-1-1.13.3.tgz", + "integrity": "sha512-0Qi+dK+6F6cgu0GTm1Y/tCSbhN6zZI9iULjLPNsQYLZEKvUzo56FFcGVIOaYveG7qguXKC9YQ9bZcS1ZqhgS1Q==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.13.3", + "@swagger-api/apidom-ns-openapi-3-1": "^1.13.3", + "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.13.3", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0" + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-openapi-yaml-3-2": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-yaml-3-2/-/apidom-parser-adapter-openapi-yaml-3-2-1.13.3.tgz", + "integrity": "sha512-dL3vJ8kFSvh9mmlHM1Cl8hNFJWHQ8CpypkfN7QTuMHIVJruvOwJ2VWhNXM8GTH07ytFg8cFto0UAGYgG2E/raw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.13.3", + "@swagger-api/apidom-ns-openapi-3-2": "^1.13.3", + "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.13.3", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0" + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-yaml-1-2": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-yaml-1-2/-/apidom-parser-adapter-yaml-1-2-1.13.3.tgz", + "integrity": "sha512-6jNhcCGnEE35dCG1NGEEddUb26kFU8VzuBoCJpOeCOJcKcsVehSpS2igD9bOVepoWMORRRE7CQkcCjoKMk95Xg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-ast": "^1.13.3", + "@swagger-api/apidom-core": "^1.13.3", + "@swagger-api/apidom-error": "^1.13.3", + "@tree-sitter-grammars/tree-sitter-yaml": "=0.7.1", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0", + "tree-sitter": "=0.22.4", + "web-tree-sitter": "=0.24.5" + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-yaml-1-2/node_modules/@tree-sitter-grammars/tree-sitter-yaml": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@tree-sitter-grammars/tree-sitter-yaml/-/tree-sitter-yaml-0.7.1.tgz", + "integrity": "sha512-AynBwkIoQCTgjDR33bDUp9Mqq+YTco0is3n5hRApMqG9of/6A4eQsfC1/uSEeHSUyMQSYawcAWamsexnVpIP4Q==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-addon-api": "^8.3.1", + "node-gyp-build": "^4.8.4" + }, + "peerDependencies": { + "tree-sitter": "^0.22.4" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-yaml-1-2/node_modules/tree-sitter": { + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.22.4.tgz", + "integrity": "sha512-usbHZP9/oxNsUY65MQUsduGRqDHQOou1cagUSwjhoSYAmSahjQDAVsh9s+SlZkn8X8+O1FULRGwHu7AFP3kjzg==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + } + }, + "node_modules/@swagger-api/apidom-reference": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-reference/-/apidom-reference-1.13.3.tgz", + "integrity": "sha512-mOJ7ANqcrxhzGHPybNuzAV8Ytm7gCGRj+S4xOkNmg7cerPmMgUxTwhDPF3FDQ/wUfQL+jjdYef2IydwdOxab+A==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.13.3", + "@swagger-api/apidom-error": "^1.13.3", + "@types/ramda": "~0.30.0", + "axios": "^1.20.0", + "minimatch": "^10.2.6", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0" + }, + "optionalDependencies": { + "@swagger-api/apidom-json-pointer": "^1.13.3", + "@swagger-api/apidom-ns-a2a-1": "^1.13.3", + "@swagger-api/apidom-ns-arazzo-1": "^1.13.3", + "@swagger-api/apidom-ns-asyncapi-2": "^1.13.3", + "@swagger-api/apidom-ns-openapi-2": "^1.13.3", + "@swagger-api/apidom-ns-openapi-3-0": "^1.13.3", + "@swagger-api/apidom-ns-openapi-3-1": "^1.13.3", + "@swagger-api/apidom-ns-openapi-3-2": "^1.13.3", + "@swagger-api/apidom-parser-adapter-a2a-json-1": "^1.13.3", + "@swagger-api/apidom-parser-adapter-a2a-yaml-1": "^1.13.3", + "@swagger-api/apidom-parser-adapter-api-design-systems-json": "^1.13.3", + "@swagger-api/apidom-parser-adapter-api-design-systems-yaml": "^1.13.3", + "@swagger-api/apidom-parser-adapter-arazzo-json-1": "^1.13.3", + "@swagger-api/apidom-parser-adapter-arazzo-yaml-1": "^1.13.3", + "@swagger-api/apidom-parser-adapter-asyncapi-json-2": "^1.13.3", + "@swagger-api/apidom-parser-adapter-asyncapi-json-3": "^1.13.3", + "@swagger-api/apidom-parser-adapter-asyncapi-yaml-2": "^1.13.3", + "@swagger-api/apidom-parser-adapter-asyncapi-yaml-3": "^1.13.3", + "@swagger-api/apidom-parser-adapter-json": "^1.13.3", + "@swagger-api/apidom-parser-adapter-openapi-json-2": "^1.13.3", + "@swagger-api/apidom-parser-adapter-openapi-json-3-0": "^1.13.3", + "@swagger-api/apidom-parser-adapter-openapi-json-3-1": "^1.13.3", + "@swagger-api/apidom-parser-adapter-openapi-json-3-2": "^1.13.3", + "@swagger-api/apidom-parser-adapter-openapi-yaml-2": "^1.13.3", + "@swagger-api/apidom-parser-adapter-openapi-yaml-3-0": "^1.13.3", + "@swagger-api/apidom-parser-adapter-openapi-yaml-3-1": "^1.13.3", + "@swagger-api/apidom-parser-adapter-openapi-yaml-3-2": "^1.13.3", + "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.13.3" + } + }, + "node_modules/@swaggerexpert/cookie": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@swaggerexpert/cookie/-/cookie-2.0.2.tgz", + "integrity": "sha512-DPI8YJ0Vznk4CT+ekn3rcFNq1uQwvUHZhH6WvTSPD0YKBIlMS9ur2RYKghXuxxOiqOam/i4lHJH4xTIiTgs3Mg==", + "license": "Apache-2.0", + "dependencies": { + "apg-lite": "^1.0.3" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@swaggerexpert/json-pointer": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/@swaggerexpert/json-pointer/-/json-pointer-2.10.2.tgz", + "integrity": "sha512-qMx1nOrzoB+PF+pzb26Q4Tc2sOlrx9Ba2UBNX9hB31Omrq+QoZ2Gly0KLrQWw4Of1AQ4J9lnD+XOdwOdcdXqqw==", + "license": "Apache-2.0", + "dependencies": { + "apg-lite": "^1.0.4" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@tanstack/react-virtual": { + "version": "3.14.10", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.14.10.tgz", + "integrity": "sha512-SRyoUbdFMRHuYXMijV5H4ZarQWpXkj3iANq8OFre+pybeVap8ZJjZ3Nz9bVjx4d8PfobVUQUdKyyyHYk3E+djw==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.17.8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.17.8", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.17.8.tgz", + "integrity": "sha512-BfEvehNpOT75r5Ksc5xW6NZuXujTfb7nlSEyVu4XHG3gdxNg1KqXruWbDewXOUaUYIo4oRbSfkjIajz4MAT8tA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "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": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "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": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "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/@testing-library/user-event": { + "version": "14.6.8", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.8.tgz", + "integrity": "sha512-vgSSqnANMnkcYR/vUNDbwS7aM3OErRLNUG8vCVVlj4oc6cKffUrccX7UAS5AmJQQNZTSibi9z5GvvRZ0aYgUCQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "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/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.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/hast": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/node": { + "version": "26.6.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz", + "integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@types/prismjs": { + "version": "1.26.6", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz", + "integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/ramda": { + "version": "0.30.2", + "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.30.2.tgz", + "integrity": "sha512-PyzHvjCalm2BRYjAU6nIB3TprYwMNOUY/7P/N8bSzp9W/yM2YrtGtAnnVtaCNSeOZ8DzKyFDvaqQs7LnWwwmBA==", + "license": "MIT", + "dependencies": { + "types-ramda": "^0.30.1" + } + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/swagger-ui-react": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-react/-/swagger-ui-react-5.18.0.tgz", + "integrity": "sha512-c2M9adVG7t28t1pq19K9Jt20VLQf0P/fwJwnfcmsVVsdkwCWhRmbKDu+tIs0/NGwJ/7GY8lBx+iKZxuDI5gDbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "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/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/apg-lite": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/apg-lite/-/apg-lite-1.0.5.tgz", + "integrity": "sha512-SlI+nLMQDzCZfS39ihzjGp3JNBQfJXyMi6cg9tkLOCPVErgFsUIAEdO9IezR7kbP5Xd0ozcPNQBkf9TO5cHgWw==", + "license": "BSD-2-Clause" + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "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==", + "license": "MIT" + }, + "node_modules/autolinker": { + "version": "3.16.2", + "resolved": "https://registry.npmjs.org/autolinker/-/autolinker-3.16.2.tgz", + "integrity": "sha512-JiYl7j2Z19F9NdTmirENSUUIIL/9MytEWtmzhfmsKPCp9E+G35Y0UNCMoM9tFigxT59qSc8Ml2dlZXOCVTYwuA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.6.1", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.6.1.tgz", + "integrity": "sha512-cL1Qz6ADZhcEbny/8HPfe99J6HhNoYtpX2LFLIbhgGE7Q1hlQVkYFdetDN7Id3KiQxhDrHwzlHr/YQCnZ8+xSA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.9", + "caniuse-lite": "^1.0.30001810", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz", + "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/axios/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/axios/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.31", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.31.tgz", + "integrity": "sha512-b0k7wXlWjpuL/Uxb/pYlpjY0ej5BjAmurtPUk3eRGuloI4yjK32m8TiKUQ81l1ptcLjKS/KH2+uqIolZti1x4Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.29.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.4.tgz", + "integrity": "sha512-2TcbbiwXEq+N0kD36tykpCfyyQigUXmuwNQbnPp4Xk4PX9JOqVIKjJlIDjpH7uY6tmRGoAd+apeT1p60T8wsdA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.29", + "caniuse-lite": "^1.0.30001816", + "electron-to-chromium": "^1.5.459", + "node-releases": "^2.0.59", + "update-browserslist-db": "^1.3.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001819", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001819.tgz", + "integrity": "sha512-LFUqIIAwpzsIuevnKnsR9qnStBcgzPShTX61lBwN9uFkkf8o8zLiebEjbW8AYtZiF+1YKDWfdt/pWDtMuG8Xwg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "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==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "license": "MIT", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, + "node_modules/core-js-pure": { + "version": "3.51.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.51.0.tgz", + "integrity": "sha512-NE4SFDJ+qzQeqHG2vVSNwopzS6zZNPYJ5kinIqs+OofTqGzOmICgT89E/yvS9XY7rwb3eU1utWXTJ9nVFqv74A==", + "hasInstallScript": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "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==", + "devOptional": 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==", + "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/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "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/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "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/dompurify": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.17.tgz", + "integrity": "sha512-B/pSlYVHQ58kqISD/yDjuJ1yfzI34m1gF4lAod2bwSRhiN4K5Cl3H8Af7hqB2PmTL911tdEudnnXn5FnhRoz0w==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/drange": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/drange/-/drange-1.1.1.tgz", + "integrity": "sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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==", + "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/electron-to-chromium": { + "version": "1.5.463", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.463.tgz", + "integrity": "sha512-nVWNU6ltFjopvOdD44J4zoBkgO0M+WZiFKKFV7EjebOH9z0KNphk6bCEjplXtJofL21ARIuuwWOCsFkBVklm6w==", + "dev": true, + "license": "ISC" + }, + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "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==", + "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==", + "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/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/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/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-patch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz", + "integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==", + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fault": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", + "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.1.tgz", + "integrity": "sha512-FNvFGzoMLWmE6Yj9spb/zjd7yiNCHiAW9/Tg9CXrQ8wuu32HtlJOwWO11OJafl5FfY3DxTdQ0vj42zU1kvv5jg==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "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==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "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==", + "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==", + "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==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/highlightjs-vue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz", + "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==", + "license": "CC0-1.0" + }, + "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/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/immutable": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz", + "integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==", + "license": "MIT" + }, + "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/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.17.0.tgz", + "integrity": "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "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/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-file-download": { + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/js-file-download/-/js-file-download-0.4.12.tgz", + "integrity": "sha512-rML+NkoD08p5Dllpjo0ffy4jRHeY6Zsapvr/W86N7E0yuzAO6qa5X9+xog6zQNlH102J7IXljNY2FtS6Lj3ucg==", + "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==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.1.0", + "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.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "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.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.4.tgz", + "integrity": "sha512-zJZyBmQhArDRefuWTypZxwDpFDIYmrj8ypSBvwGD5P13Y95s6DjzKgu4DVdGbeQa5PDEsAOCAPNk4X+QOR006w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lowlight": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", + "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", + "license": "MIT", + "dependencies": { + "fault": "^1.0.0", + "highlight.js": "~10.7.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/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": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "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==", + "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==", + "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/minim": { + "version": "0.23.8", + "resolved": "https://registry.npmjs.org/minim/-/minim-0.23.8.tgz", + "integrity": "sha512-bjdr2xW1dBCMsMGGsUeqM4eFI60m94+szhxWys+B1ztIt6gWSfeGBdSVCIawezeHYLYn0j6zrsXdQS/JllBzww==", + "license": "MIT", + "dependencies": { + "lodash": "^4.15.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neotraverse": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-1.0.1.tgz", + "integrity": "sha512-WmmLty1YWwJl9yZi77v2dVIV6X2kuYV8YYBI/G3LWGKdGHmHUvL1z7FW0iDvEvGAwNEoc5x1tOOOyDnf5jJw/w==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "8.9.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.2.tgz", + "integrity": "sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-releases": { + "version": "2.0.60", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.60.tgz", + "integrity": "sha512-OmO8+GM1JUZqXET720VrJCwkjUCXmCMQm0Z3UDDRbfNLHS4NEAvY60j5jiFv0tkKw2PNNlqOLhahVmwJIOLDIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/openapi-path-templating": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/openapi-path-templating/-/openapi-path-templating-2.3.0.tgz", + "integrity": "sha512-rjqLJDhInqH2ZqD76oTfc2Ufc3BoKY7+E0AB3POH39E6ltsaWS5zLSiwflQi9NtFQqEUYzwWrzUjX1BIYpNpBQ==", + "license": "Apache-2.0", + "dependencies": { + "apg-lite": "^1.0.5" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/openapi-server-url-templating": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/openapi-server-url-templating/-/openapi-server-url-templating-1.3.0.tgz", + "integrity": "sha512-DPlCms3KKEbjVQb0spV6Awfn6UWNheuG/+folQPzh/wUaKwuqvj8zt5gagD7qoyxtE03cIiKPgLFS3Q8Bz00uQ==", + "license": "Apache-2.0", + "dependencies": { + "apg-lite": "^1.0.4" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "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/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz", + "integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.5.tgz", + "integrity": "sha512-TEHFzb5KRDKaOgJ4TYE+0jyJtFbjLvkdVOu0fZ2b6MTWzoqZd4Le+61aEppWkvTlYePgOX4ATL1wsAIPMLTw6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "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/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/ramda": { + "version": "0.30.1", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.30.1.tgz", + "integrity": "sha512-tEF5I22zJnuclswcZMc8bDIrwRHRzf+NqVEmqg50ShAZMP7MWeR/RGDthfM/p+BlqvF2fXAzpn8i+SJcYD3alw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ramda" + } + }, + "node_modules/ramda-adjunct": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ramda-adjunct/-/ramda-adjunct-5.1.0.tgz", + "integrity": "sha512-8qCpl2vZBXEJyNbi4zqcgdfHtcdsWjOGbiNSEnEBrM6Y0OKOT8UxJbIVGm1TIcjaSu2MxaWcgtsNlKlCk7o7qg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ramda-adjunct" + }, + "peerDependencies": { + "ramda": ">= 0.30.0" + } + }, + "node_modules/randexp": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.5.3.tgz", + "integrity": "sha512-U+5l2KrcMNOUPYvazA3h5ekF80FHTUG+87SEAmHZmolh1M+i/WyTCxVzmi+tidIa1tM4BSe8g2Y/D3loWDjj+w==", + "license": "MIT", + "dependencies": { + "drange": "^1.0.2", + "ret": "^0.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-copy-to-clipboard": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.1.tgz", + "integrity": "sha512-s+HrzLyJBxrpGTYXF15dTgMjAJpEPZT/Yp6NytAtZMRngejxt6Pt5WrfFxLAcsqUDU6sY1Jz6tyHwIicE1U2Xg==", + "license": "MIT", + "dependencies": { + "copy-to-clipboard": "^3.3.3", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">=15.3.0" + } + }, + "node_modules/react-debounce-input": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/react-debounce-input/-/react-debounce-input-3.3.0.tgz", + "integrity": "sha512-VEqkvs8JvY/IIZvh71Z0TC+mdbxERvYF33RcebnodlsUZ8RSgyKe2VWaHXv4+/8aoOgXLxWrdsYs2hDhcwbUgA==", + "license": "MIT", + "dependencies": { + "lodash.debounce": "^4", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": "^15.3.0 || 16 || 17 || 18" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-immutable-proptypes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/react-immutable-proptypes/-/react-immutable-proptypes-2.2.0.tgz", + "integrity": "sha512-Vf4gBsePlwdGvSZoLSBfd4HAP93HDauMY4fDjXhreg/vg6F3Fj/MXDNyTbltPC/xZKmZc+cjLu3598DdYK6sgQ==", + "license": "MIT", + "dependencies": { + "invariant": "^2.2.2" + }, + "peerDependencies": { + "immutable": ">=3.6.2" + } + }, + "node_modules/react-immutable-pure-component": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/react-immutable-pure-component/-/react-immutable-pure-component-2.2.2.tgz", + "integrity": "sha512-vkgoMJUDqHZfXXnjVlG3keCxSO/U6WeDQ5/Sl0GK2cH8TOxEzQ5jXqDXHEL/jqk6fsNxV05oH5kD7VNMUE2k+A==", + "license": "MIT", + "peerDependencies": { + "immutable": ">= 2 || >= 4.0.0-rc", + "react": ">= 16.6", + "react-dom": ">= 16.6" + } + }, + "node_modules/react-inspector": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/react-inspector/-/react-inspector-6.0.2.tgz", + "integrity": "sha512-x+b7LxhmHXjHoU/VrFAzw5iutsILRoYyDq97EDYdFpPLcvqtEzk4ZSZSQjnFPbr5T57tLXnHcqFYoN1pI6u8uQ==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.4 || ^17.0.0 || ^18.0.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/react-redux": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", + "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.6", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.6.tgz", + "integrity": "sha512-5HfK7k5im7LTOB0EqCQmfvy4C13G92Ssj1VTmouTK3AJvyjKTnFuCV0vcMAD/JS+JC4DvDIBRrlAeJIFjh5VWg==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.6", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.6.tgz", + "integrity": "sha512-0RHKZz7wwffvkU+2MFVT2NnjK44ssLEV+m0CAJaS2Ksmorrwj7WxH00jO0SOCW26/tINUnJHToXblDs33I38YQ==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.4", + "react-router": "6.30.6" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-syntax-highlighter": { + "version": "16.1.2", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-16.1.2.tgz", + "integrity": "sha512-4dM0+mYtfnhgwYWAvFx56fgyjIYk2yFopfiICFvMDhRfeSGRNd+ngFALdPD9q7llyGAmiE6pzOSOwI8Lf9ca1w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "highlight.js": "^10.4.1", + "highlightjs-vue": "^1.0.0", + "lowlight": "^1.17.0", + "prismjs": "^1.30.0", + "refractor": "^5.0.0" + }, + "engines": { + "node": ">= 16.20.2" + }, + "peerDependencies": { + "react": ">= 0.14.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.2.tgz", + "integrity": "sha512-/peqiBB/n07gQGLsWaHho3WfvUyRscw0gYTsEFMhrIe/nWLkYaf5SbKYjGYqtRV3aPwykJgF2VEMo1ac4bnsGA==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "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/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/refractor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-5.0.0.tgz", + "integrity": "sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/prismjs": "^1.0.0", + "hastscript": "^9.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remarkable": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/remarkable/-/remarkable-2.0.1.tgz", + "integrity": "sha512-YJyMcOH5lrR+kZdmB0aJJ4+93bEojRZ1HGDn9Eagu6ibg7aVZhc3OWbbShRid+Q5eAfsEqWxpe+g5W5nYNfNiA==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.10", + "autolinker": "^3.11.0" + }, + "bin": { + "remarkable": "bin/remarkable.js" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/remarkable/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/reselect": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.3.0.tgz", + "integrity": "sha512-XGoLeRAVzUTcJ1qkxPQhDJyIZ5d6zzZD9nT7AEZOaaU9UbWclhycElmhO+VD5bFeLuzhPBaOV2oXC8uG35ZSpg==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.13", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.13.tgz", + "integrity": "sha512-Dj/cW9zBV2aRj6XzCxuGIs3ESZKv02jixne3q6jFZYZ3/Tg09gHHviSMDL4kObh9pqSahdRHEnDoaP2gn8YLhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.17.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ret": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", + "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.65.0.tgz", + "integrity": "sha512-QVCvQUTGDN6JRwi3jVGCY3H4aTHvi9rX0LyoBMkGUvdh/Z6O51CzcLz85BWZaTCDGhl6ySGqYZs+xI1jmPqGcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.65.0", + "@rollup/rollup-android-arm64": "4.65.0", + "@rollup/rollup-darwin-arm64": "4.65.0", + "@rollup/rollup-darwin-x64": "4.65.0", + "@rollup/rollup-freebsd-arm64": "4.65.0", + "@rollup/rollup-freebsd-x64": "4.65.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.65.0", + "@rollup/rollup-linux-arm-musleabihf": "4.65.0", + "@rollup/rollup-linux-arm64-gnu": "4.65.0", + "@rollup/rollup-linux-arm64-musl": "4.65.0", + "@rollup/rollup-linux-loong64-gnu": "4.65.0", + "@rollup/rollup-linux-loong64-musl": "4.65.0", + "@rollup/rollup-linux-ppc64-gnu": "4.65.0", + "@rollup/rollup-linux-ppc64-musl": "4.65.0", + "@rollup/rollup-linux-riscv64-gnu": "4.65.0", + "@rollup/rollup-linux-riscv64-musl": "4.65.0", + "@rollup/rollup-linux-s390x-gnu": "4.65.0", + "@rollup/rollup-linux-x64-gnu": "4.65.0", + "@rollup/rollup-linux-x64-musl": "4.65.0", + "@rollup/rollup-openbsd-x64": "4.65.0", + "@rollup/rollup-openharmony-arm64": "4.65.0", + "@rollup/rollup-win32-arm64-msvc": "4.65.0", + "@rollup/rollup-win32-ia32-msvc": "4.65.0", + "@rollup/rollup-win32-x64-gnu": "4.65.0", + "@rollup/rollup-win32-x64-msvc": "4.65.0", + "fsevents": "~2.3.2" + } + }, + "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/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "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.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/serialize-error": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz", + "integrity": "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/short-unique-id": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/short-unique-id/-/short-unique-id-5.3.2.tgz", + "integrity": "sha512-KRT/hufMSxXKEDSQujfVE0Faa/kZ51ihUcZQAcmP04t00DvPj7Ox5anHke1sJYUtzSuiT/Y5uyzg/W7bBEGhCg==", + "license": "Apache-2.0", + "bin": { + "short-unique-id": "bin/short-unique-id", + "suid": "bin/short-unique-id" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "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==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "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/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/swagger-client": { + "version": "3.38.4", + "resolved": "https://registry.npmjs.org/swagger-client/-/swagger-client-3.38.4.tgz", + "integrity": "sha512-uY92350FG2Nc54rZ9hClmRMXa7hKyEGL08iERCAXLAODS0d3RjTrttVyV/T0pM0HknCc2S1gi2whyZcGMx4FSQ==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime-corejs3": "^7.22.15", + "@scarf/scarf": "=1.4.0", + "@swagger-api/apidom-core": "^1.12.1", + "@swagger-api/apidom-error": "^1.12.1", + "@swagger-api/apidom-json-pointer": "^1.12.1", + "@swagger-api/apidom-ns-openapi-3-1": "^1.12.1", + "@swagger-api/apidom-ns-openapi-3-2": "^1.12.1", + "@swagger-api/apidom-reference": "^1.12.1", + "@swaggerexpert/cookie": "^2.0.2", + "deepmerge": "~4.3.0", + "fast-json-patch": "^3.0.0-1", + "js-yaml": "^4.3.2", + "neotraverse": "=1.0.1", + "node-abort-controller": "^3.1.1", + "openapi-path-templating": "^2.2.1", + "openapi-server-url-templating": "^1.3.0", + "ramda": "^0.30.1", + "ramda-adjunct": "^5.1.0" + }, + "engines": { + "node": ">=22" + }, + "optionalDependencies": { + "@swagger-api/apidom-ns-asyncapi-2": "^1.12.1", + "@swagger-api/apidom-ns-asyncapi-3": "^1.12.1", + "@swagger-api/apidom-ns-openapi-2": "^1.12.1", + "@swagger-api/apidom-parser-adapter-api-design-systems-json": "^1.12.1", + "@swagger-api/apidom-parser-adapter-api-design-systems-yaml": "^1.12.1", + "@swagger-api/apidom-parser-adapter-arazzo-json-1": "^1.12.1", + "@swagger-api/apidom-parser-adapter-arazzo-yaml-1": "^1.12.1", + "@swagger-api/apidom-parser-adapter-asyncapi-json-2": "^1.12.1", + "@swagger-api/apidom-parser-adapter-asyncapi-json-3": "^1.12.1", + "@swagger-api/apidom-parser-adapter-asyncapi-yaml-2": "^1.12.1", + "@swagger-api/apidom-parser-adapter-asyncapi-yaml-3": "^1.12.1", + "@swagger-api/apidom-parser-adapter-json": "^1.12.1", + "@swagger-api/apidom-parser-adapter-openapi-json-2": "^1.12.1", + "@swagger-api/apidom-parser-adapter-openapi-json-3-0": "^1.12.1", + "@swagger-api/apidom-parser-adapter-openapi-json-3-1": "^1.12.1", + "@swagger-api/apidom-parser-adapter-openapi-json-3-2": "^1.12.1", + "@swagger-api/apidom-parser-adapter-openapi-yaml-2": "^1.12.1", + "@swagger-api/apidom-parser-adapter-openapi-yaml-3-0": "^1.12.1", + "@swagger-api/apidom-parser-adapter-openapi-yaml-3-1": "^1.12.1", + "@swagger-api/apidom-parser-adapter-openapi-yaml-3-2": "^1.12.1", + "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.12.1" + } + }, + "node_modules/swagger-ui-react": { + "version": "5.34.0", + "resolved": "https://registry.npmjs.org/swagger-ui-react/-/swagger-ui-react-5.34.0.tgz", + "integrity": "sha512-pwWmbBfDoHHVS/Fx4L4KpgqZQPCx5leWk9Wi98tHw1eYmTrxcFmyOUXvjaj3gOO55Stjtw6fqlL9W6WT28YJfA==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime-corejs3": "^7.27.1", + "@scarf/scarf": "=1.4.0", + "@tanstack/react-virtual": "=3.14.10", + "base64-js": "^1.5.1", + "buffer": "^6.0.3", + "classnames": "^2.5.1", + "css.escape": "1.5.1", + "deep-extend": "0.6.0", + "dompurify": "^3.4.16", + "ieee754": "^1.2.1", + "immutable": "^5.1.9", + "js-file-download": "^0.4.12", + "js-yaml": "=4.3.2", + "lodash": "^4.18.1", + "prop-types": "^15.8.1", + "randexp": "^0.5.3", + "randombytes": "^2.1.0", + "react-copy-to-clipboard": "5.1.1", + "react-debounce-input": "=3.3.0", + "react-immutable-proptypes": "2.2.0", + "react-immutable-pure-component": "^2.2.0", + "react-inspector": "^6.0.1", + "react-redux": "^9.3.0", + "react-syntax-highlighter": "^16.0.0", + "redux": "^5.0.1", + "remarkable": "^2.0.1", + "reselect": "^5.1.1", + "serialize-error": "^8.1.0", + "sha.js": "^2.4.12", + "swagger-client": "^3.38.4", + "url-parse": "^1.5.10", + "xml": "=1.0.1", + "xml-but-prettier": "^1.0.1", + "zenscroll": "^4.0.2" + }, + "peerDependencies": { + "react": ">=16.8.0 <20", + "react-dom": ">=16.8.0 <20" + } + }, + "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/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "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/tinyglobby/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/tinyglobby/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/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "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/tree-sitter": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.21.1.tgz", + "integrity": "sha512-7dxoA6kYvtgWw80265MyqJlkRl4yawIjO7S5MigytjELkX43fV2WsAXzsNfO7sBpPPCF5Gp0+XzHk0DwLCq3xQ==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-addon-api": "^8.0.0", + "node-gyp-build": "^4.8.0" + } + }, + "node_modules/tree-sitter-json": { + "version": "0.24.8", + "resolved": "https://registry.npmjs.org/tree-sitter-json/-/tree-sitter-json-0.24.8.tgz", + "integrity": "sha512-Tc9ZZYwHyWZ3Tt1VEw7Pa2scu1YO7/d2BCBbKTx5hXwig3UfdQjsOPkPyLpDJOn/m1UBEWYAtSdGAwCSyagBqQ==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-addon-api": "^8.2.2", + "node-gyp-build": "^4.8.2" + }, + "peerDependencies": { + "tree-sitter": "^0.21.1" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/ts-mixer": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", + "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==", + "license": "MIT" + }, + "node_modules/ts-toolbelt": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz", + "integrity": "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==", + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/types-ramda": { + "version": "0.30.1", + "resolved": "https://registry.npmjs.org/types-ramda/-/types-ramda-0.30.1.tgz", + "integrity": "sha512-1HTsf5/QVRmLzcGfldPFvkVsAdi1db1BBKzi7iW3KBUlOICg/nKnFS+jGqDJS3YD8VsWbAh7JiHeBvbsw8RPxA==", + "license": "MIT", + "dependencies": { + "ts-toolbelt": "^9.6.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/unraw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unraw/-/unraw-3.0.0.tgz", + "integrity": "sha512-08/DA66UF65OlpUDIQtbJyrqTR0jTAlJ+jsnkQ4jxR7+K5g5YG1APZKQSMCE1vqqmD+2pv6+IdEjmopFatacvg==", + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", + "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/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==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.7.0.tgz", + "integrity": "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "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/web-tree-sitter": { + "version": "0.24.5", + "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.24.5.tgz", + "integrity": "sha512-+J/2VSHN8J47gQUAvF8KDadrfz6uFYVjxoxbKWDoXVsH2u7yLdarCnIURnrMA6uSRkgX3SdmqM5BOoQjPdSh5w==", + "license": "MIT", + "optional": true + }, + "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/which-typed-array": { + "version": "1.1.24", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.24.tgz", + "integrity": "sha512-wk4Mf4pR5mRP7eYuuTBCIQ9d0ud2Fv2jRLQpfgnRjbOxAFHmjKFValgTpitVKzJJS8ajnYQV2Du1SZ8j6b/EUQ==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "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": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", + "license": "MIT" + }, + "node_modules/xml-but-prettier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml-but-prettier/-/xml-but-prettier-1.0.1.tgz", + "integrity": "sha512-C2CJaadHrZTqESlH03WOyw0oZTtoy2uEg6dSDF6YRg+9GnYNub53RRemLpnvtbHDFelxMx4LajiFsYeR6XJHgQ==", + "license": "MIT", + "dependencies": { + "repeat-string": "^1.5.2" + } + }, + "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" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/zenscroll": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zenscroll/-/zenscroll-4.0.2.tgz", + "integrity": "sha512-jEA1znR7b4C/NnaycInCU6h/d15ZzCd1jmsruqOKnZP6WXQSMH3W2GL+OXbkruslU4h+Tzuos0HdswzRUk/Vgg==", + "license": "Unlicense" + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..ed4f925 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,36 @@ +{ + "name": "kereru-forms-frontend", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc --noEmit && vite build", + "preview": "vite preview", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.28.0", + "swagger-ui-react": "^5.34.0" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.0.1", + "@testing-library/user-event": "^14.5.2", + "@types/node": "^26.6.5", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@types/swagger-ui-react": "^5.18.0", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "jsdom": "^25.0.1", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.17", + "typescript": "^5.6.3", + "vite": "^5.4.11", + "vitest": "^2.1.8" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..21e2e96 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..8e35fa4 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,39 @@ +import { Suspense, lazy } from 'react' +import { BrowserRouter, Route, Routes } from 'react-router-dom' +import { Layout } from './components/Layout' +import { FillFormPage } from './pages/FillFormPage' +import { FormBuilderPage } from './pages/FormBuilderPage' +import { FormDetailPage } from './pages/FormDetailPage' +import { HomePage } from './pages/HomePage' +import { ManagePage } from './pages/ManagePage' +import { SubmissionDetailPage } from './pages/SubmissionDetailPage' + +const DocsPage = lazy(() => + import('./pages/DocsPage').then((module) => ({ default: module.DocsPage })), +) + +export function App() { + return ( + + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + Loading API docs…

}> + + + } + /> +
+
+
+ ) +} diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx new file mode 100644 index 0000000..ab0d7c7 --- /dev/null +++ b/frontend/src/components/Layout.tsx @@ -0,0 +1,55 @@ +import { Link, NavLink, Outlet } from 'react-router-dom' +import { ThemeToggle } from './ThemeToggle' + +const navLinkClass = ({ isActive }: { isActive: boolean }) => + `rounded-lg px-3 py-2 text-sm font-medium transition ${ + isActive + ? 'bg-brand-50 text-brand-700 dark:bg-brand-950 dark:text-brand-300' + : 'text-slate-600 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-300 dark:hover:bg-slate-800 dark:hover:text-white' + }` + +export function Layout() { + return ( +
+
+
+ + + + Kererū Forms + + + + + +
+ +
+
+
+ +
+ +
+ +
+ Kererū Forms · simple, versioned surveys +
+
+ ) +} diff --git a/frontend/src/components/QuestionField.tsx b/frontend/src/components/QuestionField.tsx new file mode 100644 index 0000000..3ed1a10 --- /dev/null +++ b/frontend/src/components/QuestionField.tsx @@ -0,0 +1,164 @@ +import type { ReactNode } from 'react' +import type { Question } from '../lib/types' + +interface QuestionFieldProps { + question: Question + value: unknown + onChange: (value: unknown) => void +} + +export function QuestionField({ question, value, onChange }: QuestionFieldProps) { + const id = `q-${question.id}` + const descriptionId = question.description ? `${id}-description` : undefined + const isText = question.type === 'text' + + const label = ( + + ) + + const description = question.description ? ( +

+ {question.description} +

+ ) : null + + let control: ReactNode = null + + if (question.type === 'boolean') { + control = ( +
+ {[ + { label: 'True', checked: value === true }, + { label: 'False', checked: value === false }, + ].map((option) => ( + + ))} +
+ ) + } + + if (question.type === 'number') { + control = ( +
+ { + const raw = event.target.value + onChange(raw === '' ? undefined : Number(raw)) + }} + /> + {(question.minValue !== undefined || question.maxValue !== undefined) && ( +

+ {question.minValue !== undefined && `Minimum ${question.minValue}`} + {question.minValue !== undefined && question.maxValue !== undefined && ' · '} + {question.maxValue !== undefined && `Maximum ${question.maxValue}`} +

+ )} +
+ ) + } + + if (question.type === 'date') { + control = ( + onChange(event.target.value || undefined)} + /> + ) + } + + if (question.type === 'select') { + control = ( + + ) + } + + if (question.type === 'text' && question.multiline) { + control = ( +