From fcaf9df05d60d33b9e630ec7ddb873308b9607cc Mon Sep 17 00:00:00 2001 From: Alex Wichmann Date: Mon, 4 May 2026 20:43:03 +0200 Subject: [PATCH 1/5] update v0.3.0 batch 1 --- Dockerfile | 11 + README.md | 174 +++++++------ cmd/server/main.go | 4 + docker-compose.yml | 13 + internal/api/license_handlers.go | 24 ++ internal/api/management_licenses.go | 152 +++++++++++ internal/api/management_offline.go | 176 +++++++++++++ internal/api/management_slugs.go | 140 +++++++--- internal/api/management_webhooks.go | 59 +++++ internal/api/offline_tokens.go | 57 +++++ internal/api/server.go | 163 +++++++++++- internal/api/server_test.go | 337 ++++++++++++++++++++++++- internal/config/config.go | 15 ++ internal/config/config_test.go | 11 + internal/offlinejwt/offlinejwt.go | 210 +++++++++++++++ internal/storage/license_management.go | 212 ++++++++++++++++ internal/storage/schema.sql | 57 ++++- internal/storage/signing_keys.go | 241 ++++++++++++++++++ internal/storage/slug_management.go | 115 +++++++-- internal/storage/store.go | 258 +++++++++++++++---- 20 files changed, 2228 insertions(+), 201 deletions(-) create mode 100644 internal/api/management_licenses.go create mode 100644 internal/api/management_offline.go create mode 100644 internal/api/offline_tokens.go create mode 100644 internal/offlinejwt/offlinejwt.go create mode 100644 internal/storage/license_management.go create mode 100644 internal/storage/signing_keys.go diff --git a/Dockerfile b/Dockerfile index ec87886..bc7e2c9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,10 +10,21 @@ COPY internal ./internal RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/simple-license-server ./cmd/server +FROM node:22-alpine AS ui-builder + +WORKDIR /ui + +COPY ui/package.json ui/package-lock.json ./ +RUN npm ci + +COPY ui ./ +RUN npm run build + FROM gcr.io/distroless/static-debian12 WORKDIR /app COPY --from=builder /out/simple-license-server /app/simple-license-server +COPY --from=ui-builder /ui/dist /app/ui EXPOSE 8080 diff --git a/README.md b/README.md index 0a9cd4f..6ed4ac4 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ -# simplelicenseserver -https://simplelicenseserver.com +# Simple License Server API -A simple, postgres-backed license server. +Current API version: `0.2.0` + +This repository contains the Go API implementation for the license and management flows described in `simple-license-server.md`. + +## Implemented endpoints -## Endpoints License API: - `GET /healthz` @@ -27,79 +29,103 @@ Management API: - `POST /management/webhooks` - `PATCH /management/webhooks/{id}` - `DELETE /management/webhooks/{id}` +- `GET /management/offline/signing-keys` +- `POST /management/offline/signing-keys` +- `POST /management/offline/signing-keys/{id}/activate` +- `POST /management/offline/signing-keys/{id}/retire` +- `GET /management/offline/public-keys` + +`POST /generate` supports idempotency via the `Idempotency-Key` request header. + +## Local development + +1. Start Postgres. +2. Copy `.env.example` values into your environment. +3. Run: + +```bash +go run ./cmd/server +``` + +The server auto-runs schema setup on startup and ensures a default slug named `default` exists. + +## Docker compose + +```bash +docker compose up --build +``` + +This starts: + +- Postgres on `localhost:5432` +- API on `localhost:8080` + +## Quick smoke test +1) Create a generated server key with the management key: -## Example Compose -```yaml -services: - postgres: - image: postgres:16-alpine - restart: unless-stopped - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: simple_license_server - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres -d simple_license_server"] - interval: 5s - timeout: 5s - retries: 10 - volumes: - - postgres_data:/var/lib/postgresql/data - # Optional: expose Postgres to your host for debugging. - # ports: - # - "5432:5432" - - api: - image: bytebardorg/simplelicenseserver - restart: unless-stopped - depends_on: - postgres: - condition: service_healthy - ports: - - "8080:8080" - environment: - # REQUIRED: database connection used by the API service. - DATABASE_URL: postgres://postgres:postgres@postgres:5432/simple_license_server?sslmode=disable - - # REQUIRED: management bootstrap key (16+ chars). You can also use MANAGEMENT_API_KEY. - MANAGEMENT_API_KEYS: management_key_dev_123456 - - # OPTIONAL: API listen port (default: 8080). - PORT: "8080" - - # OPTIONAL: request and server shutdown controls. - REQUEST_TIMEOUT: 15s - SHUTDOWN_TIMEOUT: 10s - HTTP_READ_TIMEOUT: 15s - HTTP_WRITE_TIMEOUT: 30s - HTTP_IDLE_TIMEOUT: 60s - - # OPTIONAL: global and per-IP rate limiting. - RATE_LIMIT_ENABLED: "true" - RATE_LIMIT_GLOBAL_RPS: "100" - RATE_LIMIT_GLOBAL_BURST: "200" - RATE_LIMIT_PER_IP_RPS: "20" - RATE_LIMIT_PER_IP_BURST: "40" - RATE_LIMIT_IP_TTL: 10m - RATE_LIMIT_MAX_IP_ENTRIES: "10000" - - # OPTIONAL: set true only when a trusted proxy sets forwarding headers. - TRUST_PROXY_HEADERS: "false" - -volumes: - postgres_data: +```bash +curl -sS http://localhost:8080/management/api-keys \ + -H "Authorization: Bearer management_key_dev_123456" \ + -H "Content-Type: application/json" \ + -d '{"name":"local-dev"}' ``` -## What it is not -Simple License Server is not a product management solution. +2) Use returned `api_key` against `/generate`: + +```bash +curl -sS http://localhost:8080/generate \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -H "Idempotency-Key: evt_123" \ + -d '{"slug":"default","metadata":{"email":"user@example.com"}}' +``` + +## Authentication + +- Management API routes require env bootstrap keys from `MANAGEMENT_API_KEYS` or `MANAGEMENT_API_KEY`. +- Provisioning routes (`/generate`, `/revoke`) require generated active server API keys from the management API. +- Runtime routes (`/activate`, `/validate`, `/deactivate`) use `license_key` + `fingerprint` and do not use API keys. + +Both key-protected surfaces accept either: + +- `Authorization: Bearer ` +- `X-API-Key: ` + +## Operational defaults + +Timeout controls: + +- `REQUEST_TIMEOUT` (default `15s`) +- `SHUTDOWN_TIMEOUT` (default `10s`) +- `HTTP_READ_TIMEOUT` (default `15s`) +- `HTTP_WRITE_TIMEOUT` (default `30s`) +- `HTTP_IDLE_TIMEOUT` (default `60s`) + +Rate limiting defaults: + +- `RATE_LIMIT_ENABLED` (default `true`) +- `RATE_LIMIT_GLOBAL_RPS` (default `100`) +- `RATE_LIMIT_GLOBAL_BURST` (default `200`) +- `RATE_LIMIT_PER_IP_RPS` (default `20`) +- `RATE_LIMIT_PER_IP_BURST` (default `40`) +- `RATE_LIMIT_IP_TTL` (default `10m`) +- `RATE_LIMIT_MAX_IP_ENTRIES` (default `10000`) +- `TRUST_PROXY_HEADERS` (default `false`) + +Offline token defaults: + +- Offline JWT issuance is controlled per slug with `offline_enabled` and `offline_token_lifetime_hours`. +- All slugs, including the seeded `default` slug, default to offline disabled. +- `POST /activate` and valid `POST /validate` responses include `token` only when the license slug has offline enabled and an active signing key exists. +- `OFFLINE_SIGNING_ENCRYPTION_KEY` encrypts signing private keys at rest and must be at least 32 characters before creating or using signing keys. +- `OFFLINE_TOKEN_ISSUER` defaults to `simple-license-server`. +- `OFFLINE_TOKEN_AUDIENCE` is optional. +- Slug `offline_token_lifetime_hours` defaults to `24`. -It is also not a checkout or billing orchestration system. -It is a focused license server that can be safely exposed to the internet and used for: +Additional API security behavior: -- issuing licenses -- activating licenses -- deactivating seats -- validating licenses -- revoking licenses -- managing simple policy through slugs +- Requires `Content-Type: application/json` for JSON POST/PATCH endpoints. +- Applies defensive response headers (`nosniff`, CSP, no-store cache policy). +- Enforces request field and metadata size limits. +- Stores generated API keys hashed at rest. diff --git a/cmd/server/main.go b/cmd/server/main.go index deecea5..6a03cd9 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -37,6 +37,7 @@ func main() { server := api.NewServerWithOptions(store, logger, cfg.ManagementAPIKeys, api.Options{ RequestTimeout: cfg.RequestTimeout, + UIEnabled: cfg.UIEnabled, RateLimitEnabled: cfg.RateLimitEnabled, RateLimitGlobalRPS: cfg.RateLimitGlobalRPS, RateLimitGlobalBurst: cfg.RateLimitGlobalBurst, @@ -45,6 +46,9 @@ func main() { RateLimitIPTTL: cfg.RateLimitIPTTL, RateLimitMaxIPEntries: cfg.RateLimitMaxIPEntries, TrustProxyHeaders: cfg.TrustProxyHeaders, + OfflineSigningKey: cfg.OfflineSigningKey, + OfflineTokenIssuer: cfg.OfflineTokenIssuer, + OfflineTokenAudience: cfg.OfflineTokenAudience, }) dispatcher := webhook.NewDispatcher(store, logger, webhook.DefaultOptions()) diff --git a/docker-compose.yml b/docker-compose.yml index bf38fec..dfa4ba3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,10 +20,21 @@ services: build: context: . restart: unless-stopped + develop: + watch: + - path: ./cmd + action: rebuild + - path: ./internal + action: rebuild + - path: ./ui + action: rebuild + ignore: + - ui/node_modules/ environment: PORT: "8080" DATABASE_URL: postgres://postgres:postgres@postgres:5432/simple_license_server?sslmode=disable MANAGEMENT_API_KEYS: management_key_dev_123456 + UI_ENABLED: "true" REQUEST_TIMEOUT: 15s SHUTDOWN_TIMEOUT: 10s HTTP_READ_TIMEOUT: 15s @@ -37,6 +48,8 @@ services: RATE_LIMIT_IP_TTL: 10m RATE_LIMIT_MAX_IP_ENTRIES: "10000" TRUST_PROXY_HEADERS: "false" + OFFLINE_SIGNING_ENCRYPTION_KEY: development_offline_signing_key_123456 + OFFLINE_TOKEN_ISSUER: simple-license-server depends_on: postgres: condition: service_healthy diff --git a/internal/api/license_handlers.go b/internal/api/license_handlers.go index 310c2fe..38bd9a0 100644 --- a/internal/api/license_handlers.go +++ b/internal/api/license_handlers.go @@ -48,6 +48,7 @@ type activateResponse struct { LicenseKey string `json:"license_key"` Fingerprint string `json:"fingerprint"` ExpiresAt *time.Time `json:"expires_at"` + Token string `json:"token,omitempty"` Reason string `json:"reason,omitempty"` } @@ -60,6 +61,7 @@ type validateResponse struct { Valid bool `json:"valid"` Status string `json:"status"` ExpiresAt *time.Time `json:"expires_at"` + Token string `json:"token,omitempty"` Reason string `json:"reason,omitempty"` } @@ -294,12 +296,23 @@ func (s *Server) handleActivate(w http.ResponseWriter, r *http.Request) { }) } + token := "" + if result.Valid && result.OfflineEnabled { + var err error + token, err = s.issueOfflineToken(r.Context(), result.LicenseID, result.Slug, result.Fingerprint, result.ExpiresAt, result.OfflineTokenLifetimeSeconds) + if err != nil { + s.writeUnexpectedError(w, "failed to issue offline token", err) + return + } + } + writeJSON(w, http.StatusOK, activateResponse{ Valid: result.Valid, Status: result.Status, LicenseKey: result.LicenseKey, Fingerprint: result.Fingerprint, ExpiresAt: result.ExpiresAt, + Token: token, Reason: result.Reason, }) } @@ -352,10 +365,21 @@ func (s *Server) handleValidate(w http.ResponseWriter, r *http.Request) { "reason": result.Reason, }) + token := "" + if result.Valid && result.OfflineEnabled { + var err error + token, err = s.issueOfflineToken(r.Context(), result.LicenseID, result.Slug, req.Fingerprint, result.ExpiresAt, result.OfflineTokenLifetimeSeconds) + if err != nil { + s.writeUnexpectedError(w, "failed to refresh offline token", err) + return + } + } + writeJSON(w, http.StatusOK, validateResponse{ Valid: result.Valid, Status: result.Status, ExpiresAt: result.ExpiresAt, + Token: token, Reason: result.Reason, }) } diff --git a/internal/api/management_licenses.go b/internal/api/management_licenses.go new file mode 100644 index 0000000..6c0d6fd --- /dev/null +++ b/internal/api/management_licenses.go @@ -0,0 +1,152 @@ +package api + +import ( + "fmt" + "net/http" + "strings" + "time" + + "simple-license-server/internal/storage" +) + +type licenseManagementResponse struct { + ID string `json:"id"` + LicenseKey string `json:"license_key"` + Slug string `json:"slug"` + Status string `json:"status"` + Metadata map[string]any `json:"metadata"` + MaxActivations int `json:"max_activations"` + ActiveSeats int `json:"active_seats"` + OfflineEnabled bool `json:"offline_enabled"` + OfflineTokenLifetimeHours int `json:"offline_token_lifetime_hours"` + ExpiresAt *time.Time `json:"expires_at"` + CreatedAt time.Time `json:"created_at"` + ActivatedAt *time.Time `json:"activated_at"` + LastValidatedAt *time.Time `json:"last_validated_at"` + RevokedAt *time.Time `json:"revoked_at"` +} + +type licensePaginationResponse struct { + Page int `json:"page"` + PageSize int `json:"page_size"` + Total int `json:"total"` + TotalPages int `json:"total_pages"` +} + +type licenseCountsResponse struct { + Total int `json:"total"` + Active int `json:"active"` + Inactive int `json:"inactive"` + Revoked int `json:"revoked"` + Expired int `json:"expired"` +} + +type listLicensesResponse struct { + Licenses []licenseManagementResponse `json:"licenses"` + Pagination licensePaginationResponse `json:"pagination"` + Counts licenseCountsResponse `json:"counts"` +} + +func (s *Server) handleListLicenses(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + page, err := parsePositiveIntQuery(query, "page", 1, 0) + if err != nil { + writeJSON(w, http.StatusBadRequest, errorResponse{Error: err.Error()}) + return + } + + pageSize, err := parsePositiveIntQuery(query, "page_size", 10, maxListPageSize) + if err != nil { + writeJSON(w, http.StatusBadRequest, errorResponse{Error: err.Error()}) + return + } + + search := strings.TrimSpace(query.Get("q")) + if err := validateOptionalFieldLength(search, "search", maxSearchQueryLength); err != nil { + writeJSON(w, http.StatusBadRequest, errorResponse{Error: err.Error()}) + return + } + + status := strings.TrimSpace(query.Get("status")) + if err := validateLicenseListStatus(status); err != nil { + writeJSON(w, http.StatusBadRequest, errorResponse{Error: err.Error()}) + return + } + + result, err := s.service.ListLicenses(r.Context(), storage.LicenseListParams{ + Page: page, + PageSize: pageSize, + Search: search, + Status: status, + }) + if err != nil { + s.writeUnexpectedError(w, "failed to list licenses", err) + return + } + + licenses := make([]licenseManagementResponse, 0, len(result.Licenses)) + for _, license := range result.Licenses { + licenses = append(licenses, mapLicenseManagementResponse(license)) + } + + writeJSON(w, http.StatusOK, listLicensesResponse{ + Licenses: licenses, + Pagination: licensePaginationResponse{ + Page: page, + PageSize: pageSize, + Total: result.Total, + TotalPages: totalPages(result.Total, pageSize), + }, + Counts: licenseCountsResponse{ + Total: result.Counts.Total, + Active: result.Counts.Active, + Inactive: result.Counts.Inactive, + Revoked: result.Counts.Revoked, + Expired: result.Counts.Expired, + }, + }) +} + +func mapLicenseManagementResponse(record storage.LicenseRow) licenseManagementResponse { + return licenseManagementResponse{ + ID: record.ID, + LicenseKey: record.Key, + Slug: record.SlugName, + Status: displayLicenseStatus(record), + Metadata: record.Metadata, + MaxActivations: record.MaxActivations, + ActiveSeats: record.ActiveSeats, + OfflineEnabled: record.OfflineEnabled, + OfflineTokenLifetimeHours: offlineTokenLifetimeHoursFromSeconds(record.OfflineTokenLifetimeSeconds), + ExpiresAt: record.ExpiresAt, + CreatedAt: record.CreatedAt, + ActivatedAt: record.ActivatedAt, + LastValidatedAt: record.LastValidatedAt, + RevokedAt: record.RevokedAt, + } +} + +func displayLicenseStatus(record storage.LicenseRow) string { + if record.Status != "revoked" && record.ExpiresAt != nil && !time.Now().UTC().Before(record.ExpiresAt.UTC()) { + return "expired" + } + + return record.Status +} + +func validateLicenseListStatus(status string) error { + switch status { + case "", "inactive", "active", "revoked", "expired": + return nil + default: + return fmt.Errorf("status must be one of inactive, active, revoked, or expired") + } +} + +func totalPages(total, pageSize int) int { + if total == 0 || pageSize <= 0 { + return 0 + } + + return (total + pageSize - 1) / pageSize +} diff --git a/internal/api/management_offline.go b/internal/api/management_offline.go new file mode 100644 index 0000000..ec5540d --- /dev/null +++ b/internal/api/management_offline.go @@ -0,0 +1,176 @@ +package api + +import ( + "errors" + "net/http" + "strings" + "time" + + "simple-license-server/internal/offlinejwt" + "simple-license-server/internal/storage" +) + +type signingKeyResponse struct { + ID int64 `json:"id"` + Name string `json:"name"` + Kid string `json:"kid"` + Algorithm string `json:"algorithm"` + Status string `json:"status"` + PublicKeyPEM string `json:"public_key_pem,omitempty"` + CreatedAt time.Time `json:"created_at"` + ActivatedAt *time.Time `json:"activated_at"` + RetiredAt *time.Time `json:"retired_at"` +} + +type listSigningKeysResponse struct { + SigningKeys []signingKeyResponse `json:"signing_keys"` +} + +type listPublicSigningKeysResponse struct { + SigningKeys []signingKeyResponse `json:"signing_keys"` +} + +type createSigningKeyRequest struct { + Name string `json:"name"` +} + +func (s *Server) handleListSigningKeys(w http.ResponseWriter, r *http.Request) { + keys, err := s.service.ListSigningKeys(r.Context()) + if err != nil { + s.writeUnexpectedError(w, "failed to list signing keys", err) + return + } + + response := make([]signingKeyResponse, 0, len(keys)) + for _, key := range keys { + response = append(response, mapSigningKeyResponse(key, false)) + } + + writeJSON(w, http.StatusOK, listSigningKeysResponse{SigningKeys: response}) +} + +func (s *Server) handleListPublicSigningKeys(w http.ResponseWriter, r *http.Request) { + keys, err := s.service.ListPublicSigningKeys(r.Context()) + if err != nil { + s.writeUnexpectedError(w, "failed to list public signing keys", err) + return + } + + response := make([]signingKeyResponse, 0, len(keys)) + for _, key := range keys { + response = append(response, mapSigningKeyResponse(key, true)) + } + + writeJSON(w, http.StatusOK, listPublicSigningKeysResponse{SigningKeys: response}) +} + +func (s *Server) handleCreateSigningKey(w http.ResponseWriter, r *http.Request) { + if strings.TrimSpace(s.offlineSigningKey) == "" { + writeJSON(w, http.StatusBadRequest, errorResponse{Error: "OFFLINE_SIGNING_ENCRYPTION_KEY is not configured"}) + return + } + + var req createSigningKeyRequest + if err := decodeJSON(r, &req); err != nil { + writeJSON(w, http.StatusBadRequest, errorResponse{Error: err.Error()}) + return + } + + name := strings.TrimSpace(req.Name) + if err := validateOptionalFieldLength(name, "name", maxSigningKeyNameLength); err != nil { + writeJSON(w, http.StatusBadRequest, errorResponse{Error: err.Error()}) + return + } + + keyPair, err := offlinejwt.GenerateEd25519KeyPair() + if err != nil { + s.writeUnexpectedError(w, "failed to generate signing key", err) + return + } + + encryptedPrivateKey, err := offlinejwt.EncryptPrivateKey(keyPair.PrivatePEM, s.offlineSigningKey) + if err != nil { + writeJSON(w, http.StatusBadRequest, errorResponse{Error: err.Error()}) + return + } + + created, err := s.service.CreateSigningKey(r.Context(), storage.CreateSigningKeyParams{ + Name: name, + Kid: keyPair.Kid, + Algorithm: keyPair.Algorithm, + PrivateKeyEncrypted: encryptedPrivateKey, + PublicKeyPEM: keyPair.PublicPEM, + }) + if err != nil { + if errors.Is(err, storage.ErrConflict) { + writeJSON(w, http.StatusConflict, errorResponse{Error: "signing key already exists"}) + return + } + s.writeUnexpectedError(w, "failed to create signing key", err) + return + } + + writeJSON(w, http.StatusCreated, mapSigningKeyResponse(created, true)) +} + +func (s *Server) handleActivateSigningKey(w http.ResponseWriter, r *http.Request) { + id, err := parsePathID(r.PathValue("id")) + if err != nil { + writeJSON(w, http.StatusBadRequest, errorResponse{Error: err.Error()}) + return + } + + key, err := s.service.ActivateSigningKey(r.Context(), id) + if err != nil { + if errors.Is(err, storage.ErrNotFound) { + writeJSON(w, http.StatusNotFound, errorResponse{Error: "signing key not found"}) + return + } + if errors.Is(err, storage.ErrConflict) { + writeJSON(w, http.StatusConflict, errorResponse{Error: "retired signing key cannot be activated"}) + return + } + s.writeUnexpectedError(w, "failed to activate signing key", err) + return + } + + writeJSON(w, http.StatusOK, mapSigningKeyResponse(key, true)) +} + +func (s *Server) handleRetireSigningKey(w http.ResponseWriter, r *http.Request) { + id, err := parsePathID(r.PathValue("id")) + if err != nil { + writeJSON(w, http.StatusBadRequest, errorResponse{Error: err.Error()}) + return + } + + key, err := s.service.RetireSigningKey(r.Context(), id) + if err != nil { + if errors.Is(err, storage.ErrNotFound) { + writeJSON(w, http.StatusNotFound, errorResponse{Error: "signing key not found"}) + return + } + s.writeUnexpectedError(w, "failed to retire signing key", err) + return + } + + writeJSON(w, http.StatusOK, mapSigningKeyResponse(key, true)) +} + +func mapSigningKeyResponse(record storage.SigningKeyRecord, includePublicKey bool) signingKeyResponse { + resp := signingKeyResponse{ + ID: record.ID, + Name: record.Name, + Kid: record.Kid, + Algorithm: record.Algorithm, + Status: record.Status, + CreatedAt: record.CreatedAt, + ActivatedAt: record.ActivatedAt, + RetiredAt: record.RetiredAt, + } + if includePublicKey { + resp.PublicKeyPEM = record.PublicKeyPEM + } + + return resp +} diff --git a/internal/api/management_slugs.go b/internal/api/management_slugs.go index 7937688..26fdb47 100644 --- a/internal/api/management_slugs.go +++ b/internal/api/management_slugs.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "net/http" + "strconv" "strings" "time" @@ -12,15 +13,18 @@ import ( ) type slugResponse struct { - ID int64 `json:"id"` - Name string `json:"name"` - MaxActivations int `json:"max_activations"` - ExpirationType string `json:"expiration_type"` - ExpirationDays *int `json:"expiration_days"` - FixedExpiresAt *time.Time `json:"fixed_expires_at"` - IsDefault bool `json:"is_default"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID int64 `json:"id"` + Name string `json:"name"` + MaxActivations int `json:"max_activations"` + ExpirationType string `json:"expiration_type"` + ExpirationDays *int `json:"expiration_days"` + FixedExpiresAt *time.Time `json:"fixed_expires_at"` + OfflineEnabled bool `json:"offline_enabled"` + OfflineTokenLifetimeHours int `json:"offline_token_lifetime_hours"` + IsDefault bool `json:"is_default"` + DeletedAt *time.Time `json:"deleted_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type listSlugsResponse struct { @@ -28,23 +32,37 @@ type listSlugsResponse struct { } type createSlugRequest struct { - Name string `json:"name"` - MaxActivations int `json:"max_activations"` - ExpirationType string `json:"expiration_type"` - ExpirationDays *int `json:"expiration_days"` - FixedExpiresAt *string `json:"fixed_expires_at"` + Name string `json:"name"` + MaxActivations int `json:"max_activations"` + ExpirationType string `json:"expiration_type"` + ExpirationDays *int `json:"expiration_days"` + FixedExpiresAt *string `json:"fixed_expires_at"` + OfflineEnabled bool `json:"offline_enabled"` + OfflineTokenLifetimeHours *int `json:"offline_token_lifetime_hours"` } type updateSlugRequest struct { - Name *string `json:"name"` - MaxActivations *int `json:"max_activations"` - ExpirationType *string `json:"expiration_type"` - ExpirationDays *int `json:"expiration_days"` - FixedExpiresAt *string `json:"fixed_expires_at"` + Name *string `json:"name"` + MaxActivations *int `json:"max_activations"` + ExpirationType *string `json:"expiration_type"` + ExpirationDays *int `json:"expiration_days"` + FixedExpiresAt *string `json:"fixed_expires_at"` + OfflineEnabled *bool `json:"offline_enabled"` + OfflineTokenLifetimeHours *int `json:"offline_token_lifetime_hours"` } func (s *Server) handleListSlugs(w http.ResponseWriter, r *http.Request) { - items, err := s.service.ListSlugs(r.Context()) + includeArchived := false + if raw := strings.TrimSpace(r.URL.Query().Get("include_archived")); raw != "" { + parsed, parseErr := strconv.ParseBool(raw) + if parseErr != nil { + writeJSON(w, http.StatusBadRequest, errorResponse{Error: "include_archived must be a boolean"}) + return + } + includeArchived = parsed + } + + items, err := s.service.ListSlugs(r.Context(), includeArchived) if err != nil { s.writeUnexpectedError(w, "failed to list slugs", err) return @@ -99,12 +117,23 @@ func (s *Server) handleCreateSlug(w http.ResponseWriter, r *http.Request) { return } + offlineTokenLifetimeHours := storage.DefaultOfflineTokenLifetimeHours + if req.OfflineTokenLifetimeHours != nil { + offlineTokenLifetimeHours = *req.OfflineTokenLifetimeHours + } + if err := validateOfflineTokenLifetimeHours(offlineTokenLifetimeHours); err != nil { + writeJSON(w, http.StatusBadRequest, errorResponse{Error: err.Error()}) + return + } + record, err := s.service.CreateSlug(r.Context(), storage.CreateSlugParams{ - Name: req.Name, - MaxActivations: policy.MaxActivations(), - ExpirationType: policy.ExpirationType(), - ExpirationDays: policy.ExpirationDays(), - FixedExpiresAt: policy.FixedExpiresAt(), + Name: req.Name, + MaxActivations: policy.MaxActivations(), + ExpirationType: policy.ExpirationType(), + ExpirationDays: policy.ExpirationDays(), + FixedExpiresAt: policy.FixedExpiresAt(), + OfflineEnabled: req.OfflineEnabled, + OfflineTokenLifetimeSeconds: offlineTokenLifetimeHours * 3600, }) if err != nil { if errors.Is(err, storage.ErrConflict) { @@ -141,7 +170,7 @@ func (s *Server) handleUpdateSlug(w http.ResponseWriter, r *http.Request) { return } - if req.Name == nil && req.MaxActivations == nil && req.ExpirationType == nil && req.ExpirationDays == nil && req.FixedExpiresAt == nil { + if req.Name == nil && req.MaxActivations == nil && req.ExpirationType == nil && req.ExpirationDays == nil && req.FixedExpiresAt == nil && req.OfflineEnabled == nil && req.OfflineTokenLifetimeHours == nil { writeJSON(w, http.StatusBadRequest, errorResponse{Error: "at least one field must be provided"}) return } @@ -192,6 +221,16 @@ func (s *Server) handleUpdateSlug(w http.ResponseWriter, r *http.Request) { return } + var offlineTokenLifetimeSeconds *int + if req.OfflineTokenLifetimeHours != nil { + if err := validateOfflineTokenLifetimeHours(*req.OfflineTokenLifetimeHours); err != nil { + writeJSON(w, http.StatusBadRequest, errorResponse{Error: err.Error()}) + return + } + v := *req.OfflineTokenLifetimeHours * 3600 + offlineTokenLifetimeSeconds = &v + } + nameParam := resolvedName maxParam := policy.MaxActivations() expTypeParam := policy.ExpirationType() @@ -199,11 +238,13 @@ func (s *Server) handleUpdateSlug(w http.ResponseWriter, r *http.Request) { fixedParam := policy.FixedExpiresAt() record, err := s.service.UpdateSlugByName(r.Context(), name, storage.UpdateSlugParams{ - Name: &nameParam, - MaxActivations: &maxParam, - ExpirationType: &expTypeParam, - ExpirationDays: expDaysParam, - FixedExpiresAt: &fixedParam, + Name: &nameParam, + MaxActivations: &maxParam, + ExpirationType: &expTypeParam, + ExpirationDays: expDaysParam, + FixedExpiresAt: &fixedParam, + OfflineEnabled: req.OfflineEnabled, + OfflineTokenLifetimeSeconds: offlineTokenLifetimeSeconds, }) if err != nil { if errors.Is(err, storage.ErrNotFound) { @@ -247,15 +288,18 @@ func (s *Server) handleDeleteSlug(w http.ResponseWriter, r *http.Request) { func mapSlugResponse(record storage.SlugRecord) slugResponse { return slugResponse{ - ID: record.ID, - Name: record.Name, - MaxActivations: record.MaxActivations, - ExpirationType: record.ExpirationType, - ExpirationDays: record.ExpirationDays, - FixedExpiresAt: record.FixedExpiresAt, - IsDefault: record.IsDefault, - CreatedAt: record.CreatedAt, - UpdatedAt: record.UpdatedAt, + ID: record.ID, + Name: record.Name, + MaxActivations: record.MaxActivations, + ExpirationType: record.ExpirationType, + ExpirationDays: record.ExpirationDays, + FixedExpiresAt: record.FixedExpiresAt, + OfflineEnabled: record.OfflineEnabled, + OfflineTokenLifetimeHours: offlineTokenLifetimeHoursFromSeconds(record.OfflineTokenLifetimeSeconds), + IsDefault: record.IsDefault, + DeletedAt: record.DeletedAt, + CreatedAt: record.CreatedAt, + UpdatedAt: record.UpdatedAt, } } @@ -286,6 +330,22 @@ func resolveSlugPolicy(maxActivations int, expirationType string, expirationDays return slugdomain.NewPolicy(maxActivations, expirationType, expirationDays, fixedExpiresAt) } +func validateOfflineTokenLifetimeHours(value int) error { + if value <= 0 { + return fmt.Errorf("offline_token_lifetime_hours must be greater than 0") + } + + return nil +} + +func offlineTokenLifetimeHoursFromSeconds(seconds int) int { + if seconds <= 0 { + return storage.DefaultOfflineTokenLifetimeHours + } + + return (seconds + 3599) / 3600 +} + func parseOptionalRFC3339(raw string) (*time.Time, error) { trimmed := strings.TrimSpace(raw) if trimmed == "" { diff --git a/internal/api/management_webhooks.go b/internal/api/management_webhooks.go index b26369e..1d55cac 100644 --- a/internal/api/management_webhooks.go +++ b/internal/api/management_webhooks.go @@ -23,6 +23,26 @@ type listWebhookEndpointsResponse struct { Webhooks []webhookEndpointResponse `json:"webhooks"` } +type webhookDeliveryResponse struct { + ID int64 `json:"id"` + EndpointID int64 `json:"endpoint_id"` + EndpointName string `json:"endpoint_name"` + EndpointURL string `json:"endpoint_url"` + EventType string `json:"event_type"` + Status string `json:"status"` + Attempts int `json:"attempts"` + LastResponseStatus *int `json:"last_response_status"` + LastError *string `json:"last_error"` + NextAttemptAt time.Time `json:"next_attempt_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeliveredAt *time.Time `json:"delivered_at"` +} + +type listWebhookDeliveriesResponse struct { + Deliveries []webhookDeliveryResponse `json:"deliveries"` +} + type createWebhookRequest struct { Name string `json:"name"` URL string `json:"url"` @@ -52,6 +72,27 @@ func (s *Server) handleListWebhooks(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, listWebhookEndpointsResponse{Webhooks: response}) } +func (s *Server) handleListWebhookDeliveries(w http.ResponseWriter, r *http.Request) { + limit, err := parsePositiveIntQuery(r.URL.Query(), "limit", 25, maxListPageSize) + if err != nil { + writeJSON(w, http.StatusBadRequest, errorResponse{Error: err.Error()}) + return + } + + deliveries, err := s.service.ListWebhookDeliveries(r.Context(), limit) + if err != nil { + s.writeUnexpectedError(w, "failed to list webhook deliveries", err) + return + } + + response := make([]webhookDeliveryResponse, 0, len(deliveries)) + for _, delivery := range deliveries { + response = append(response, mapWebhookDeliveryResponse(delivery)) + } + + writeJSON(w, http.StatusOK, listWebhookDeliveriesResponse{Deliveries: response}) +} + func (s *Server) handleCreateWebhook(w http.ResponseWriter, r *http.Request) { var req createWebhookRequest if err := decodeJSON(r, &req); err != nil { @@ -197,3 +238,21 @@ func (s *Server) handleDeleteWebhook(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]any{"deleted": true}) } + +func mapWebhookDeliveryResponse(delivery storage.WebhookDeliveryLog) webhookDeliveryResponse { + return webhookDeliveryResponse{ + ID: delivery.ID, + EndpointID: delivery.EndpointID, + EndpointName: delivery.EndpointName, + EndpointURL: delivery.EndpointURL, + EventType: delivery.EventType, + Status: delivery.Status, + Attempts: delivery.Attempts, + LastResponseStatus: delivery.LastResponseStatus, + LastError: delivery.LastError, + NextAttemptAt: delivery.NextAttemptAt, + CreatedAt: delivery.CreatedAt, + UpdatedAt: delivery.UpdatedAt, + DeliveredAt: delivery.DeliveredAt, + } +} diff --git a/internal/api/offline_tokens.go b/internal/api/offline_tokens.go new file mode 100644 index 0000000..b0832a5 --- /dev/null +++ b/internal/api/offline_tokens.go @@ -0,0 +1,57 @@ +package api + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "simple-license-server/internal/offlinejwt" + "simple-license-server/internal/storage" +) + +func (s *Server) issueOfflineToken(ctx context.Context, licenseID, slugName, fingerprint string, licenseExpiresAt *time.Time, lifetimeSeconds int) (string, error) { + licenseID = strings.TrimSpace(licenseID) + slugName = strings.TrimSpace(slugName) + fingerprint = strings.TrimSpace(fingerprint) + if licenseID == "" || slugName == "" || fingerprint == "" { + return "", nil + } + if lifetimeSeconds <= 0 { + return "", fmt.Errorf("offline token lifetime must be greater than 0") + } + + activeKey, err := s.service.GetActiveSigningKey(ctx) + if err != nil { + if errors.Is(err, storage.ErrNotFound) { + return "", nil + } + return "", err + } + if activeKey.Algorithm != offlinejwt.AlgorithmEd25519 { + return "", fmt.Errorf("unsupported active signing key algorithm %q", activeKey.Algorithm) + } + if strings.TrimSpace(s.offlineSigningKey) == "" { + return "", fmt.Errorf("OFFLINE_SIGNING_ENCRYPTION_KEY is not configured") + } + + now := time.Now().UTC() + expiresAt := now.Add(time.Duration(lifetimeSeconds) * time.Second) + if licenseExpiresAt != nil && licenseExpiresAt.UTC().Before(expiresAt) { + expiresAt = licenseExpiresAt.UTC() + } + if !expiresAt.After(now) { + return "", nil + } + + return offlinejwt.SignEd25519JWT(activeKey.PrivateKeyEncrypted, s.offlineSigningKey, activeKey.Kid, offlinejwt.TokenParams{ + Issuer: s.offlineTokenIssuer, + Audience: s.offlineTokenAudience, + Subject: licenseID, + Slug: slugName, + Fingerprint: fingerprint, + ExpiresAt: expiresAt, + IssuedAt: now, + }) +} diff --git a/internal/api/server.go b/internal/api/server.go index e782f12..9a7570a 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -13,6 +13,9 @@ import ( "mime" "net/http" "net/url" + "os" + "path" + "path/filepath" "sort" "strconv" "strings" @@ -35,8 +38,11 @@ const ( maxMetadataDepth = 5 maxMetadataNodes = 512 maxAPIKeyNameLength = 128 + maxSigningKeyNameLength = 128 maxWebhookNameLength = 128 maxWebhookURLLength = 2048 + maxSearchQueryLength = 256 + maxListPageSize = 100 generateEndpointKey = "/generate" ) @@ -80,10 +86,17 @@ type Server struct { managementAPIKeyHashes [][]byte requestTimeout time.Duration rateLimiter *ipRateLimiter + uiEnabled bool + uiDistDir string + offlineSigningKey string + offlineTokenIssuer string + offlineTokenAudience string } type Options struct { RequestTimeout time.Duration + UIEnabled bool + UIDistDir string RateLimitEnabled bool RateLimitGlobalRPS float64 RateLimitGlobalBurst int @@ -92,11 +105,16 @@ type Options struct { RateLimitIPTTL time.Duration RateLimitMaxIPEntries int TrustProxyHeaders bool + OfflineSigningKey string + OfflineTokenIssuer string + OfflineTokenAudience string } func DefaultOptions() Options { return Options{ RequestTimeout: 15 * time.Second, + UIEnabled: false, + UIDistDir: "/app/ui", RateLimitEnabled: true, RateLimitGlobalRPS: 100, RateLimitGlobalBurst: 200, @@ -105,6 +123,7 @@ func DefaultOptions() Options { RateLimitIPTTL: 10 * time.Minute, RateLimitMaxIPEntries: 10000, TrustProxyHeaders: false, + OfflineTokenIssuer: "simple-license-server", } } @@ -114,11 +133,12 @@ type licenseService interface { IsAuthorizedServerAPIKey(ctx context.Context, candidate string) (bool, error) GenerateLicenseIdempotent(ctx context.Context, endpoint, idemKey, requestHash, slugName string, metadata map[string]any) (storage.GeneratedLicense, json.RawMessage, bool, error) GenerateLicense(ctx context.Context, slugName string, metadata map[string]any) (storage.GeneratedLicense, error) + ListLicenses(ctx context.Context, params storage.LicenseListParams) (storage.LicenseListResult, error) RevokeLicense(ctx context.Context, licenseKey string) (storage.RevokeResult, error) ActivateLicense(ctx context.Context, licenseKey, fingerprint string, metadata map[string]any) (storage.ActivationResult, error) ValidateLicense(ctx context.Context, licenseKey, fingerprint string) (storage.ValidationResult, error) DeactivateLicense(ctx context.Context, licenseKey, fingerprint, reason string) (storage.DeactivationResult, error) - ListSlugs(ctx context.Context) ([]storage.SlugRecord, error) + ListSlugs(ctx context.Context, includeArchived bool) ([]storage.SlugRecord, error) GetSlugByName(ctx context.Context, name string) (storage.SlugRecord, error) CreateSlug(ctx context.Context, params storage.CreateSlugParams) (storage.SlugRecord, error) UpdateSlugByName(ctx context.Context, name string, params storage.UpdateSlugParams) (storage.SlugRecord, error) @@ -127,9 +147,16 @@ type licenseService interface { CreateAPIKey(ctx context.Context, params storage.CreateAPIKeyParams) (storage.CreatedAPIKey, error) RevokeAPIKey(ctx context.Context, id int64) (storage.APIKeyRecord, error) ListWebhookEndpoints(ctx context.Context) ([]storage.WebhookEndpoint, error) + ListWebhookDeliveries(ctx context.Context, limit int) ([]storage.WebhookDeliveryLog, error) CreateWebhookEndpoint(ctx context.Context, params storage.CreateWebhookEndpointParams) (storage.WebhookEndpoint, error) UpdateWebhookEndpoint(ctx context.Context, id int64, params storage.UpdateWebhookEndpointParams) (storage.WebhookEndpoint, error) DeleteWebhookEndpoint(ctx context.Context, id int64) error + ListSigningKeys(ctx context.Context) ([]storage.SigningKeyRecord, error) + ListPublicSigningKeys(ctx context.Context) ([]storage.SigningKeyRecord, error) + GetActiveSigningKey(ctx context.Context) (storage.SigningKeyRecord, error) + CreateSigningKey(ctx context.Context, params storage.CreateSigningKeyParams) (storage.SigningKeyRecord, error) + ActivateSigningKey(ctx context.Context, id int64) (storage.SigningKeyRecord, error) + RetireSigningKey(ctx context.Context, id int64) (storage.SigningKeyRecord, error) } func NewServer(service licenseService, logger *slog.Logger, managementAPIKeys map[string]struct{}, requestTimeout time.Duration) *Server { @@ -167,11 +194,23 @@ func NewServerWithOptions(service licenseService, logger *slog.Logger, managemen opts.RateLimitMaxIPEntries = DefaultOptions().RateLimitMaxIPEntries } + if strings.TrimSpace(opts.UIDistDir) == "" { + opts.UIDistDir = DefaultOptions().UIDistDir + } + if strings.TrimSpace(opts.OfflineTokenIssuer) == "" { + opts.OfflineTokenIssuer = DefaultOptions().OfflineTokenIssuer + } + return &Server{ service: service, logger: logger, managementAPIKeyHashes: hashManagementAPIKeys(managementAPIKeys), requestTimeout: opts.RequestTimeout, + uiEnabled: opts.UIEnabled, + uiDistDir: opts.UIDistDir, + offlineSigningKey: strings.TrimSpace(opts.OfflineSigningKey), + offlineTokenIssuer: strings.TrimSpace(opts.OfflineTokenIssuer), + offlineTokenAudience: strings.TrimSpace(opts.OfflineTokenAudience), rateLimiter: newIPRateLimiter(rateLimiterConfig{ enabled: opts.RateLimitEnabled, globalRPS: opts.RateLimitGlobalRPS, @@ -202,10 +241,21 @@ func (s *Server) Routes() http.Handler { mux.Handle("GET /management/api-keys", s.requireManagementKey(http.HandlerFunc(s.handleListAPIKeys))) mux.Handle("POST /management/api-keys", s.requireManagementKey(http.HandlerFunc(s.handleCreateAPIKey))) mux.Handle("POST /management/api-keys/{id}/revoke", s.requireManagementKey(http.HandlerFunc(s.handleRevokeAPIKey))) + mux.Handle("GET /management/licenses", s.requireManagementKey(http.HandlerFunc(s.handleListLicenses))) mux.Handle("GET /management/webhooks", s.requireManagementKey(http.HandlerFunc(s.handleListWebhooks))) + mux.Handle("GET /management/webhooks/deliveries", s.requireManagementKey(http.HandlerFunc(s.handleListWebhookDeliveries))) mux.Handle("POST /management/webhooks", s.requireManagementKey(http.HandlerFunc(s.handleCreateWebhook))) mux.Handle("PATCH /management/webhooks/{id}", s.requireManagementKey(http.HandlerFunc(s.handleUpdateWebhook))) mux.Handle("DELETE /management/webhooks/{id}", s.requireManagementKey(http.HandlerFunc(s.handleDeleteWebhook))) + mux.Handle("GET /management/offline/signing-keys", s.requireManagementKey(http.HandlerFunc(s.handleListSigningKeys))) + mux.Handle("POST /management/offline/signing-keys", s.requireManagementKey(http.HandlerFunc(s.handleCreateSigningKey))) + mux.Handle("POST /management/offline/signing-keys/{id}/activate", s.requireManagementKey(http.HandlerFunc(s.handleActivateSigningKey))) + mux.Handle("POST /management/offline/signing-keys/{id}/retire", s.requireManagementKey(http.HandlerFunc(s.handleRetireSigningKey))) + mux.Handle("GET /management/offline/public-keys", s.requireManagementKey(http.HandlerFunc(s.handleListPublicSigningKeys))) + + if s.uiEnabled { + s.registerUIRoutes(mux) + } handler := s.requestTimeoutMiddleware(mux) handler = s.rateLimitMiddleware(handler) @@ -228,6 +278,11 @@ func (s *Server) requireProvisioningKey(next http.Handler) http.Handler { return } + if s.isAuthorizedManagementKey(apiKey) { + next.ServeHTTP(w, r) + return + } + authorized, err := s.service.IsAuthorizedServerAPIKey(r.Context(), apiKey) if err != nil { s.writeUnexpectedError(w, "failed provisioning api key auth", err) @@ -327,11 +382,98 @@ func (s *Server) securityHeadersMiddleware(next http.Handler) http.Handler { w.Header().Set("Referrer-Policy", "no-referrer") w.Header().Set("Cache-Control", "no-store") w.Header().Set("Pragma", "no-cache") - w.Header().Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'") + if strings.HasPrefix(r.URL.Path, "/ui") { + w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'") + } else { + w.Header().Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'") + } next.ServeHTTP(w, r) }) } +func (s *Server) registerUIRoutes(mux *http.ServeMux) { + mux.HandleFunc("GET /ui", func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "/ui/", http.StatusTemporaryRedirect) + }) + mux.HandleFunc("GET /ui/", s.handleUISPA) +} + +func (s *Server) handleUISPA(w http.ResponseWriter, r *http.Request) { + if !s.uiEnabled { + writeJSON(w, http.StatusNotFound, errorResponse{Error: "not found"}) + return + } + + requested := strings.TrimPrefix(r.URL.Path, "/ui/") + cleanPath := path.Clean("/" + requested) + if cleanPath == "/" { + s.serveUIFile(w, r, filepath.Join(s.uiDistDir, "index.html")) + return + } + + relPath := strings.TrimPrefix(cleanPath, "/") + fullPath := filepath.Join(s.uiDistDir, filepath.FromSlash(relPath)) + if !isPathInsideRoot(s.uiDistDir, fullPath) { + writeJSON(w, http.StatusBadRequest, errorResponse{Error: "invalid path"}) + return + } + + info, err := os.Stat(fullPath) + if err == nil && !info.IsDir() { + s.serveUIFile(w, r, fullPath) + return + } + + if errors.Is(err, os.ErrNotExist) || (err == nil && info.IsDir()) { + s.serveUIFile(w, r, filepath.Join(s.uiDistDir, "index.html")) + return + } + + writeJSON(w, http.StatusInternalServerError, errorResponse{Error: "ui asset lookup failed"}) +} + +func (s *Server) serveUIFile(w http.ResponseWriter, r *http.Request, fullPath string) { + if !isPathInsideRoot(s.uiDistDir, fullPath) { + writeJSON(w, http.StatusBadRequest, errorResponse{Error: "invalid path"}) + return + } + + _, err := os.Stat(fullPath) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + writeJSON(w, http.StatusNotFound, errorResponse{Error: "ui not found; build frontend assets first"}) + return + } + writeJSON(w, http.StatusInternalServerError, errorResponse{Error: "ui asset read failed"}) + return + } + + http.ServeFile(w, r, fullPath) +} + +func isPathInsideRoot(root, candidate string) bool { + absRoot, err := filepath.Abs(root) + if err != nil { + return false + } + + absCandidate, err := filepath.Abs(candidate) + if err != nil { + return false + } + + rel, err := filepath.Rel(absRoot, absCandidate) + if err != nil { + return false + } + + if rel == "." { + return true + } + + return !strings.HasPrefix(rel, "..") +} + func (s *Server) writeUnexpectedError(w http.ResponseWriter, logMsg string, err error) { switch { case errors.Is(err, context.DeadlineExceeded): @@ -632,6 +774,23 @@ func parsePathID(raw string) (int64, error) { return id, nil } +func parsePositiveIntQuery(values url.Values, name string, defaultValue, maxValue int) (int, error) { + raw := strings.TrimSpace(values.Get(name)) + if raw == "" { + return defaultValue, nil + } + + value, err := strconv.Atoi(raw) + if err != nil || value <= 0 { + return 0, fmt.Errorf("%s must be a positive integer", name) + } + if maxValue > 0 && value > maxValue { + return maxValue, nil + } + + return value, nil +} + func normalizeWebhookEvents(events []string) ([]string, error) { if len(events) == 0 { return nil, fmt.Errorf("events must contain at least one event") diff --git a/internal/api/server_test.go b/internal/api/server_test.go index d56014c..0233708 100644 --- a/internal/api/server_test.go +++ b/internal/api/server_test.go @@ -8,10 +8,13 @@ import ( "log/slog" "net/http" "net/http/httptest" + "os" + "path/filepath" "strings" "testing" "time" + "simple-license-server/internal/offlinejwt" "simple-license-server/internal/storage" ) @@ -21,11 +24,12 @@ type stubService struct { pingFn func(ctx context.Context) error generateLicenseFn func(ctx context.Context, slugName string, metadata map[string]any) (storage.GeneratedLicense, error) generateIdempotentFn func(ctx context.Context, endpoint, idemKey, requestHash, slugName string, metadata map[string]any) (storage.GeneratedLicense, json.RawMessage, bool, error) + listLicensesFn func(ctx context.Context, params storage.LicenseListParams) (storage.LicenseListResult, error) revokeLicenseFn func(ctx context.Context, licenseKey string) (storage.RevokeResult, error) activateLicenseFn func(ctx context.Context, licenseKey, fingerprint string, metadata map[string]any) (storage.ActivationResult, error) validateLicenseFn func(ctx context.Context, licenseKey, fingerprint string) (storage.ValidationResult, error) deactivateLicenseFn func(ctx context.Context, licenseKey, fingerprint, reason string) (storage.DeactivationResult, error) - listSlugsFn func(ctx context.Context) ([]storage.SlugRecord, error) + listSlugsFn func(ctx context.Context, includeArchived bool) ([]storage.SlugRecord, error) getSlugByNameFn func(ctx context.Context, name string) (storage.SlugRecord, error) createSlugFn func(ctx context.Context, params storage.CreateSlugParams) (storage.SlugRecord, error) updateSlugByNameFn func(ctx context.Context, name string, params storage.UpdateSlugParams) (storage.SlugRecord, error) @@ -34,9 +38,16 @@ type stubService struct { createAPIKeyFn func(ctx context.Context, params storage.CreateAPIKeyParams) (storage.CreatedAPIKey, error) revokeAPIKeyFn func(ctx context.Context, id int64) (storage.APIKeyRecord, error) listWebhooksFn func(ctx context.Context) ([]storage.WebhookEndpoint, error) + listWebhookDeliveriesFn func(ctx context.Context, limit int) ([]storage.WebhookDeliveryLog, error) createWebhookFn func(ctx context.Context, params storage.CreateWebhookEndpointParams) (storage.WebhookEndpoint, error) updateWebhookFn func(ctx context.Context, id int64, params storage.UpdateWebhookEndpointParams) (storage.WebhookEndpoint, error) deleteWebhookFn func(ctx context.Context, id int64) error + listSigningKeysFn func(ctx context.Context) ([]storage.SigningKeyRecord, error) + listPublicSigningKeysFn func(ctx context.Context) ([]storage.SigningKeyRecord, error) + getActiveSigningKeyFn func(ctx context.Context) (storage.SigningKeyRecord, error) + createSigningKeyFn func(ctx context.Context, params storage.CreateSigningKeyParams) (storage.SigningKeyRecord, error) + activateSigningKeyFn func(ctx context.Context, id int64) (storage.SigningKeyRecord, error) + retireSigningKeyFn func(ctx context.Context, id int64) (storage.SigningKeyRecord, error) } func (s stubService) EnqueueWebhookEvent(ctx context.Context, eventType string, payload map[string]any) error { @@ -75,6 +86,13 @@ func (s stubService) GenerateLicense(ctx context.Context, slugName string, metad return storage.GeneratedLicense{}, nil } +func (s stubService) ListLicenses(ctx context.Context, params storage.LicenseListParams) (storage.LicenseListResult, error) { + if s.listLicensesFn != nil { + return s.listLicensesFn(ctx, params) + } + return storage.LicenseListResult{}, nil +} + func (s stubService) RevokeLicense(ctx context.Context, licenseKey string) (storage.RevokeResult, error) { if s.revokeLicenseFn != nil { return s.revokeLicenseFn(ctx, licenseKey) @@ -103,9 +121,9 @@ func (s stubService) DeactivateLicense(ctx context.Context, licenseKey, fingerpr return storage.DeactivationResult{}, nil } -func (s stubService) ListSlugs(ctx context.Context) ([]storage.SlugRecord, error) { +func (s stubService) ListSlugs(ctx context.Context, includeArchived bool) ([]storage.SlugRecord, error) { if s.listSlugsFn != nil { - return s.listSlugsFn(ctx) + return s.listSlugsFn(ctx, includeArchived) } return []storage.SlugRecord{}, nil } @@ -166,6 +184,13 @@ func (s stubService) ListWebhookEndpoints(ctx context.Context) ([]storage.Webhoo return []storage.WebhookEndpoint{}, nil } +func (s stubService) ListWebhookDeliveries(ctx context.Context, limit int) ([]storage.WebhookDeliveryLog, error) { + if s.listWebhookDeliveriesFn != nil { + return s.listWebhookDeliveriesFn(ctx, limit) + } + return []storage.WebhookDeliveryLog{}, nil +} + func (s stubService) CreateWebhookEndpoint(ctx context.Context, params storage.CreateWebhookEndpointParams) (storage.WebhookEndpoint, error) { if s.createWebhookFn != nil { return s.createWebhookFn(ctx, params) @@ -187,6 +212,48 @@ func (s stubService) DeleteWebhookEndpoint(ctx context.Context, id int64) error return nil } +func (s stubService) ListSigningKeys(ctx context.Context) ([]storage.SigningKeyRecord, error) { + if s.listSigningKeysFn != nil { + return s.listSigningKeysFn(ctx) + } + return []storage.SigningKeyRecord{}, nil +} + +func (s stubService) ListPublicSigningKeys(ctx context.Context) ([]storage.SigningKeyRecord, error) { + if s.listPublicSigningKeysFn != nil { + return s.listPublicSigningKeysFn(ctx) + } + return []storage.SigningKeyRecord{}, nil +} + +func (s stubService) GetActiveSigningKey(ctx context.Context) (storage.SigningKeyRecord, error) { + if s.getActiveSigningKeyFn != nil { + return s.getActiveSigningKeyFn(ctx) + } + return storage.SigningKeyRecord{}, storage.ErrNotFound +} + +func (s stubService) CreateSigningKey(ctx context.Context, params storage.CreateSigningKeyParams) (storage.SigningKeyRecord, error) { + if s.createSigningKeyFn != nil { + return s.createSigningKeyFn(ctx, params) + } + return storage.SigningKeyRecord{}, nil +} + +func (s stubService) ActivateSigningKey(ctx context.Context, id int64) (storage.SigningKeyRecord, error) { + if s.activateSigningKeyFn != nil { + return s.activateSigningKeyFn(ctx, id) + } + return storage.SigningKeyRecord{}, nil +} + +func (s stubService) RetireSigningKey(ctx context.Context, id int64) (storage.SigningKeyRecord, error) { + if s.retireSigningKeyFn != nil { + return s.retireSigningKeyFn(ctx, id) + } + return storage.SigningKeyRecord{}, nil +} + func TestGenerateRequiresServerKey(t *testing.T) { s := NewServer(stubService{}, slog.New(slog.NewTextHandler(io.Discard, nil)), map[string]struct{}{"server_key_dev_123": {}}, 15*time.Second) @@ -247,22 +314,130 @@ func TestGenerateWithBearerKey(t *testing.T) { } } -func TestGenerateRejectsManagementKeyForProvisioning(t *testing.T) { +func TestGenerateAcceptsManagementKeyForProvisioning(t *testing.T) { + createdAt := time.Date(2026, 4, 20, 12, 0, 0, 0, time.UTC) + called := false s := NewServer(stubService{ isAuthorizedServerAPIKeyFn: func(ctx context.Context, candidate string) (bool, error) { return candidate == "server_key_db_123456", nil }, + generateLicenseFn: func(ctx context.Context, slugName string, metadata map[string]any) (storage.GeneratedLicense, error) { + called = true + return storage.GeneratedLicense{ + LicenseKey: "LNC-8821-X99B-4421", + Slug: slugName, + Status: "inactive", + Metadata: metadata, + CreatedAt: createdAt, + }, nil + }, }, slog.New(slog.NewTextHandler(io.Discard, nil)), map[string]struct{}{"management_key_dev_123456": {}}, 15*time.Second) - req := httptest.NewRequest(http.MethodPost, "/generate", strings.NewReader(`{"slug":"default"}`)) + req := httptest.NewRequest(http.MethodPost, "/generate", strings.NewReader(`{"slug":"default","metadata":{"email":"user@example.com"}}`)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer management_key_dev_123456") rr := httptest.NewRecorder() s.Routes().ServeHTTP(rr, req) - if rr.Code != http.StatusUnauthorized { - t.Fatalf("expected status 401, got %d body=%s", rr.Code, rr.Body.String()) + if rr.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d body=%s", rr.Code, rr.Body.String()) + } + if !called { + t.Fatalf("expected service GenerateLicense to be called") + } +} + +func TestRevokeAcceptsManagementKeyForProvisioning(t *testing.T) { + revokedAt := time.Date(2026, 4, 20, 12, 0, 0, 0, time.UTC) + called := false + s := NewServer(stubService{ + isAuthorizedServerAPIKeyFn: func(ctx context.Context, candidate string) (bool, error) { + return candidate == "server_key_db_123456", nil + }, + revokeLicenseFn: func(ctx context.Context, licenseKey string) (storage.RevokeResult, error) { + called = true + return storage.RevokeResult{ + Valid: false, + Status: "revoked", + LicenseKey: licenseKey, + RevokedAt: revokedAt, + }, nil + }, + }, slog.New(slog.NewTextHandler(io.Discard, nil)), map[string]struct{}{"management_key_dev_123456": {}}, 15*time.Second) + + req := httptest.NewRequest(http.MethodPost, "/revoke", strings.NewReader(`{"license_key":"LNC-8821-X99B-4421"}`)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer management_key_dev_123456") + rr := httptest.NewRecorder() + + s.Routes().ServeHTTP(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d body=%s", rr.Code, rr.Body.String()) + } + if !called { + t.Fatalf("expected service RevokeLicense to be called") + } +} + +func TestValidateRefreshesOfflineTokenWhenSlugAllowsOffline(t *testing.T) { + secret := "test_offline_signing_secret_32_chars" + keyPair, err := offlinejwt.GenerateEd25519KeyPair() + if err != nil { + t.Fatalf("generate key pair: %v", err) + } + encryptedPrivateKey, err := offlinejwt.EncryptPrivateKey(keyPair.PrivatePEM, secret) + if err != nil { + t.Fatalf("encrypt private key: %v", err) + } + + s := NewServerWithOptions(stubService{ + validateLicenseFn: func(ctx context.Context, licenseKey, fingerprint string) (storage.ValidationResult, error) { + return storage.ValidationResult{ + Valid: true, + Status: "active", + LicenseID: "8c50f4f7-761d-4bf1-8e09-27a5f7ea0a12", + Slug: "default", + OfflineEnabled: true, + OfflineTokenLifetimeSeconds: 86400, + }, nil + }, + getActiveSigningKeyFn: func(ctx context.Context) (storage.SigningKeyRecord, error) { + return storage.SigningKeyRecord{ + Kid: keyPair.Kid, + Algorithm: keyPair.Algorithm, + Status: "active", + PrivateKeyEncrypted: encryptedPrivateKey, + PublicKeyPEM: keyPair.PublicPEM, + }, nil + }, + }, slog.New(slog.NewTextHandler(io.Discard, nil)), map[string]struct{}{"management_key_dev_123456": {}}, Options{ + RequestTimeout: 15 * time.Second, + RateLimitEnabled: false, + OfflineSigningKey: secret, + OfflineTokenIssuer: "test-issuer", + }) + + req := httptest.NewRequest(http.MethodPost, "/validate", strings.NewReader(`{"license_key":"LNC-8821-X99B-4421","fingerprint":"device-1"}`)) + req.Header.Set("Content-Type", "application/json") + rr := httptest.NewRecorder() + + s.Routes().ServeHTTP(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d body=%s", rr.Code, rr.Body.String()) + } + + var response validateResponse + if err := json.Unmarshal(rr.Body.Bytes(), &response); err != nil { + t.Fatalf("decode response: %v", err) + } + if response.Token == "" { + t.Fatalf("expected refreshed offline token") + } + if strings.Count(response.Token, ".") != 2 { + t.Fatalf("expected JWT-shaped token, got %q", response.Token) } } @@ -297,6 +472,111 @@ func TestManagementEndpointWithManagementKey(t *testing.T) { } } +func TestListLicensesUsesPaginationSearchAndStatus(t *testing.T) { + createdAt := time.Date(2026, 4, 20, 12, 0, 0, 0, time.UTC) + var gotParams storage.LicenseListParams + s := NewServer(stubService{ + listLicensesFn: func(ctx context.Context, params storage.LicenseListParams) (storage.LicenseListResult, error) { + gotParams = params + return storage.LicenseListResult{ + Licenses: []storage.LicenseRow{ + { + ID: "license-id-1", + Key: "LNC-8821-X99B-4421", + Status: "active", + SlugName: "default", + Metadata: map[string]any{"email": "user@example.com"}, + MaxActivations: 3, + ActiveSeats: 2, + CreatedAt: createdAt, + }, + }, + Total: 42, + Counts: storage.LicenseStatusCounts{ + Total: 1284, + Active: 942, + Inactive: 330, + Revoked: 12, + }, + }, nil + }, + }, slog.New(slog.NewTextHandler(io.Discard, nil)), map[string]struct{}{"management_key_dev_123456": {}}, 15*time.Second) + + req := httptest.NewRequest(http.MethodGet, "/management/licenses?page=2&page_size=25&q=LNC&status=active", nil) + req.Header.Set("Authorization", "Bearer management_key_dev_123456") + rr := httptest.NewRecorder() + + s.Routes().ServeHTTP(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d body=%s", rr.Code, rr.Body.String()) + } + if gotParams.Page != 2 || gotParams.PageSize != 25 || gotParams.Search != "LNC" || gotParams.Status != "active" { + t.Fatalf("unexpected list params: %+v", gotParams) + } + if !strings.Contains(rr.Body.String(), "LNC-8821-X99B-4421") || !strings.Contains(rr.Body.String(), `"total_pages":2`) { + t.Fatalf("expected license list payload, got %s", rr.Body.String()) + } +} + +func TestListLicensesRejectsInvalidStatus(t *testing.T) { + s := NewServer(stubService{}, slog.New(slog.NewTextHandler(io.Discard, nil)), map[string]struct{}{"management_key_dev_123456": {}}, 15*time.Second) + + req := httptest.NewRequest(http.MethodGet, "/management/licenses?status=pending", nil) + req.Header.Set("Authorization", "Bearer management_key_dev_123456") + rr := httptest.NewRecorder() + + s.Routes().ServeHTTP(rr, req) + + if rr.Code != http.StatusBadRequest { + t.Fatalf("expected status 400, got %d body=%s", rr.Code, rr.Body.String()) + } +} + +func TestListWebhookDeliveriesUsesLimit(t *testing.T) { + now := time.Date(2026, 4, 20, 12, 0, 0, 0, time.UTC) + statusCode := 500 + lastError := "webhook responded with status 500" + var gotLimit int + s := NewServer(stubService{ + listWebhookDeliveriesFn: func(ctx context.Context, limit int) ([]storage.WebhookDeliveryLog, error) { + gotLimit = limit + return []storage.WebhookDeliveryLog{ + { + ID: 42, + EndpointID: 7, + EndpointName: "audit-sync", + EndpointURL: "https://example.com/hooks/license", + EventType: "license.generated", + Status: "failed", + Attempts: 3, + LastResponseStatus: &statusCode, + LastError: &lastError, + NextAttemptAt: now, + CreatedAt: now, + UpdatedAt: now, + }, + }, nil + }, + }, slog.New(slog.NewTextHandler(io.Discard, nil)), map[string]struct{}{"management_key_dev_123456": {}}, 15*time.Second) + + req := httptest.NewRequest(http.MethodGet, "/management/webhooks/deliveries?limit=5", nil) + req.Header.Set("Authorization", "Bearer management_key_dev_123456") + rr := httptest.NewRecorder() + + s.Routes().ServeHTTP(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d body=%s", rr.Code, rr.Body.String()) + } + if gotLimit != 5 { + t.Fatalf("expected limit 5, got %d", gotLimit) + } + if !strings.Contains(rr.Body.String(), "audit-sync") || !strings.Contains(rr.Body.String(), "license.generated") { + t.Fatalf("expected delivery log payload, got %s", rr.Body.String()) + } +} + func TestManagementEndpointRejectsProvisioningKey(t *testing.T) { s := NewServer( stubService{ @@ -436,3 +716,46 @@ func TestHashGenerateRequestStableAcrossMetadataOrder(t *testing.T) { t.Fatalf("expected stable hashes, got %q != %q", hashA, hashB) } } + +func TestUIRoutesDisabledByDefault(t *testing.T) { + s := NewServerWithOptions( + stubService{}, + slog.New(slog.NewTextHandler(io.Discard, nil)), + map[string]struct{}{"management_key_dev_123456": {}}, + Options{RequestTimeout: 15 * time.Second}, + ) + + req := httptest.NewRequest(http.MethodGet, "/ui/", nil) + rr := httptest.NewRecorder() + s.Routes().ServeHTTP(rr, req) + + if rr.Code != http.StatusNotFound { + t.Fatalf("expected status 404 when UI disabled, got %d", rr.Code) + } +} + +func TestUIRoutesServeIndexWhenEnabled(t *testing.T) { + tmpDir := t.TempDir() + if err := os.WriteFile(filepath.Join(tmpDir, "index.html"), []byte("ui ok"), 0o644); err != nil { + t.Fatalf("write index: %v", err) + } + + s := NewServerWithOptions( + stubService{}, + slog.New(slog.NewTextHandler(io.Discard, nil)), + map[string]struct{}{"management_key_dev_123456": {}}, + Options{RequestTimeout: 15 * time.Second, UIEnabled: true, UIDistDir: tmpDir}, + ) + + req := httptest.NewRequest(http.MethodGet, "/ui/", nil) + rr := httptest.NewRecorder() + s.Routes().ServeHTTP(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d body=%s", rr.Code, rr.Body.String()) + } + + if !strings.Contains(rr.Body.String(), "ui ok") { + t.Fatalf("expected UI index content, got: %s", rr.Body.String()) + } +} diff --git a/internal/config/config.go b/internal/config/config.go index 0d34b56..0a5476a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -12,6 +12,7 @@ type Config struct { Port string DatabaseURL string ManagementAPIKeys map[string]struct{} + UIEnabled bool RequestTimeout time.Duration ShutdownTimeout time.Duration ReadTimeout time.Duration @@ -25,12 +26,16 @@ type Config struct { RateLimitIPTTL time.Duration RateLimitMaxIPEntries int TrustProxyHeaders bool + OfflineSigningKey string + OfflineTokenIssuer string + OfflineTokenAudience string } func Load() (Config, error) { cfg := Config{ Port: strings.TrimSpace(getEnv("PORT", "8080")), DatabaseURL: strings.TrimSpace(os.Getenv("DATABASE_URL")), + UIEnabled: false, RequestTimeout: 15 * time.Second, ShutdownTimeout: 10 * time.Second, ReadTimeout: 15 * time.Second, @@ -44,6 +49,7 @@ func Load() (Config, error) { RateLimitIPTTL: 10 * time.Minute, RateLimitMaxIPEntries: 10000, TrustProxyHeaders: false, + OfflineTokenIssuer: "simple-license-server", } if cfg.DatabaseURL == "" { @@ -157,6 +163,15 @@ func Load() (Config, error) { return Config{}, err } + cfg.UIEnabled, err = parseBoolEnv("UI_ENABLED", cfg.UIEnabled) + if err != nil { + return Config{}, err + } + + cfg.OfflineSigningKey = strings.TrimSpace(os.Getenv("OFFLINE_SIGNING_ENCRYPTION_KEY")) + cfg.OfflineTokenIssuer = strings.TrimSpace(getEnv("OFFLINE_TOKEN_ISSUER", cfg.OfflineTokenIssuer)) + cfg.OfflineTokenAudience = strings.TrimSpace(os.Getenv("OFFLINE_TOKEN_AUDIENCE")) + if cfg.RateLimitEnabled { if cfg.RateLimitGlobalRPS <= 0 || cfg.RateLimitPerIPRPS <= 0 { return Config{}, fmt.Errorf("RATE_LIMIT_*_RPS values must be greater than 0") diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 6035a14..0cd22c7 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -21,6 +21,10 @@ func clearOptionalTimeoutEnv(t *testing.T) { t.Setenv("RATE_LIMIT_IP_TTL", "") t.Setenv("RATE_LIMIT_MAX_IP_ENTRIES", "") t.Setenv("TRUST_PROXY_HEADERS", "") + t.Setenv("UI_ENABLED", "") + t.Setenv("OFFLINE_SIGNING_ENCRYPTION_KEY", "") + t.Setenv("OFFLINE_TOKEN_ISSUER", "") + t.Setenv("OFFLINE_TOKEN_AUDIENCE", "") } func TestLoadRequiresDatabaseURL(t *testing.T) { @@ -70,6 +74,10 @@ func TestLoadParsesServerKeysAndTimeouts(t *testing.T) { t.Setenv("RATE_LIMIT_IP_TTL", "11m") t.Setenv("RATE_LIMIT_MAX_IP_ENTRIES", "15000") t.Setenv("TRUST_PROXY_HEADERS", "true") + t.Setenv("UI_ENABLED", "true") + t.Setenv("OFFLINE_SIGNING_ENCRYPTION_KEY", "") + t.Setenv("OFFLINE_TOKEN_ISSUER", "") + t.Setenv("OFFLINE_TOKEN_AUDIENCE", "") cfg, err := Load() if err != nil { @@ -126,6 +134,9 @@ func TestLoadParsesServerKeysAndTimeouts(t *testing.T) { if !cfg.TrustProxyHeaders { t.Fatalf("expected trust proxy headers enabled") } + if !cfg.UIEnabled { + t.Fatalf("expected ui enabled") + } } func TestLoadRejectsInvalidTimeout(t *testing.T) { diff --git a/internal/offlinejwt/offlinejwt.go b/internal/offlinejwt/offlinejwt.go new file mode 100644 index 0000000..394e84f --- /dev/null +++ b/internal/offlinejwt/offlinejwt.go @@ -0,0 +1,210 @@ +package offlinejwt + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/ed25519" + "crypto/rand" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/hex" + "encoding/json" + "encoding/pem" + "fmt" + "strings" + "time" +) + +const AlgorithmEd25519 = "Ed25519" + +type GeneratedKeyPair struct { + Kid string + Algorithm string + PrivatePEM []byte + PublicPEM string +} + +type TokenParams struct { + Issuer string + Audience string + Subject string + Slug string + Fingerprint string + ExpiresAt time.Time + IssuedAt time.Time +} + +func GenerateEd25519KeyPair() (GeneratedKeyPair, error) { + publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return GeneratedKeyPair{}, fmt.Errorf("generate ed25519 key pair: %w", err) + } + + privateDER, err := x509.MarshalPKCS8PrivateKey(privateKey) + if err != nil { + return GeneratedKeyPair{}, fmt.Errorf("marshal private key: %w", err) + } + publicDER, err := x509.MarshalPKIXPublicKey(publicKey) + if err != nil { + return GeneratedKeyPair{}, fmt.Errorf("marshal public key: %w", err) + } + + kidBytes := make([]byte, 12) + if _, err := rand.Read(kidBytes); err != nil { + return GeneratedKeyPair{}, fmt.Errorf("generate key id: %w", err) + } + + return GeneratedKeyPair{ + Kid: hex.EncodeToString(kidBytes), + Algorithm: AlgorithmEd25519, + PrivatePEM: pem.EncodeToMemory(&pem.Block{ + Type: "PRIVATE KEY", + Bytes: privateDER, + }), + PublicPEM: string(pem.EncodeToMemory(&pem.Block{ + Type: "PUBLIC KEY", + Bytes: publicDER, + })), + }, nil +} + +func EncryptPrivateKey(privatePEM []byte, encryptionSecret string) (string, error) { + if len(privatePEM) == 0 { + return "", fmt.Errorf("private key is required") + } + + aead, err := encryptionAEAD(encryptionSecret) + if err != nil { + return "", err + } + + nonce := make([]byte, aead.NonceSize()) + if _, err := rand.Read(nonce); err != nil { + return "", fmt.Errorf("generate encryption nonce: %w", err) + } + + sealed := aead.Seal(nil, nonce, privatePEM, nil) + blob := append(nonce, sealed...) + return base64.RawURLEncoding.EncodeToString(blob), nil +} + +func DecryptPrivateKey(encryptedPrivateKey, encryptionSecret string) ([]byte, error) { + aead, err := encryptionAEAD(encryptionSecret) + if err != nil { + return nil, err + } + + blob, err := base64.RawURLEncoding.DecodeString(strings.TrimSpace(encryptedPrivateKey)) + if err != nil { + return nil, fmt.Errorf("decode encrypted private key: %w", err) + } + if len(blob) <= aead.NonceSize() { + return nil, fmt.Errorf("encrypted private key is malformed") + } + + nonce := blob[:aead.NonceSize()] + ciphertext := blob[aead.NonceSize():] + plain, err := aead.Open(nil, nonce, ciphertext, nil) + if err != nil { + return nil, fmt.Errorf("decrypt private key: %w", err) + } + + return plain, nil +} + +func SignEd25519JWT(encryptedPrivateKey, encryptionSecret, kid string, params TokenParams) (string, error) { + privatePEM, err := DecryptPrivateKey(encryptedPrivateKey, encryptionSecret) + if err != nil { + return "", err + } + + privateKey, err := parseEd25519PrivateKey(privatePEM) + if err != nil { + return "", err + } + + now := params.IssuedAt.UTC() + if now.IsZero() { + now = time.Now().UTC() + } + expiresAt := params.ExpiresAt.UTC() + if !expiresAt.After(now) { + return "", fmt.Errorf("token expiration must be after issued time") + } + + jtiBytes := make([]byte, 16) + if _, err := rand.Read(jtiBytes); err != nil { + return "", fmt.Errorf("generate token id: %w", err) + } + + header := map[string]any{ + "alg": "EdDSA", + "kid": strings.TrimSpace(kid), + "typ": "JWT", + } + claims := map[string]any{ + "iss": strings.TrimSpace(params.Issuer), + "sub": strings.TrimSpace(params.Subject), + "slug": strings.TrimSpace(params.Slug), + "fingerprint": strings.TrimSpace(params.Fingerprint), + "iat": now.Unix(), + "nbf": now.Unix(), + "exp": expiresAt.Unix(), + "jti": hex.EncodeToString(jtiBytes), + } + if aud := strings.TrimSpace(params.Audience); aud != "" { + claims["aud"] = aud + } + + headerJSON, err := json.Marshal(header) + if err != nil { + return "", fmt.Errorf("marshal jwt header: %w", err) + } + claimsJSON, err := json.Marshal(claims) + if err != nil { + return "", fmt.Errorf("marshal jwt claims: %w", err) + } + + signingInput := base64.RawURLEncoding.EncodeToString(headerJSON) + "." + base64.RawURLEncoding.EncodeToString(claimsJSON) + signature := ed25519.Sign(privateKey, []byte(signingInput)) + return signingInput + "." + base64.RawURLEncoding.EncodeToString(signature), nil +} + +func encryptionAEAD(secret string) (cipher.AEAD, error) { + trimmed := strings.TrimSpace(secret) + if len(trimmed) < 32 { + return nil, fmt.Errorf("OFFLINE_SIGNING_ENCRYPTION_KEY must be at least 32 characters") + } + + key := sha256.Sum256([]byte(trimmed)) + block, err := aes.NewCipher(key[:]) + if err != nil { + return nil, fmt.Errorf("create encryption cipher: %w", err) + } + aead, err := cipher.NewGCM(block) + if err != nil { + return nil, fmt.Errorf("create gcm cipher: %w", err) + } + + return aead, nil +} + +func parseEd25519PrivateKey(privatePEM []byte) (ed25519.PrivateKey, error) { + block, _ := pem.Decode(privatePEM) + if block == nil { + return nil, fmt.Errorf("private key PEM is malformed") + } + + parsed, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("parse private key: %w", err) + } + + privateKey, ok := parsed.(ed25519.PrivateKey) + if !ok { + return nil, fmt.Errorf("private key is not Ed25519") + } + + return privateKey, nil +} diff --git a/internal/storage/license_management.go b/internal/storage/license_management.go new file mode 100644 index 0000000..880d52d --- /dev/null +++ b/internal/storage/license_management.go @@ -0,0 +1,212 @@ +package storage + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "strings" +) + +func (s *Store) ListLicenses(ctx context.Context, params LicenseListParams) (LicenseListResult, error) { + page := params.Page + if page <= 0 { + page = 1 + } + + pageSize := params.PageSize + if pageSize <= 0 { + pageSize = 10 + } + + searchPattern := "" + if search := strings.TrimSpace(params.Search); search != "" { + searchPattern = "%" + escapeLikePattern(search) + "%" + } + + status := strings.TrimSpace(params.Status) + limit := pageSize + offset := (page - 1) * pageSize + + total, err := s.countLicenses(ctx, searchPattern, status) + if err != nil { + return LicenseListResult{}, err + } + + counts, err := s.countLicenseStatuses(ctx) + if err != nil { + return LicenseListResult{}, err + } + + rows, err := s.db.Query(ctx, ` + SELECT l.id, + l.key, + l.status, + l.expires_at, + l.created_at, + l.activated_at, + ( + SELECT MAX(a.last_validated_at) + FROM activations a + WHERE a.license_id = l.id + ) AS last_validated_at, + l.revoked_at, + l.metadata, + s.name, + s.offline_enabled, + s.offline_token_lifetime_seconds, + l.max_activations, + ( + SELECT COUNT(*) + FROM activations a + WHERE a.license_id = l.id + AND a.deactivated_at IS NULL + ) AS active_seats + FROM licenses l + JOIN slugs s ON s.id = l.slug_id + WHERE ($1 = '' OR l.key ILIKE $1 ESCAPE '\' OR s.name ILIKE $1 ESCAPE '\' OR l.metadata::text ILIKE $1 ESCAPE '\') + AND ($2 = '' OR ( + CASE + WHEN l.status <> 'revoked' AND l.expires_at IS NOT NULL AND l.expires_at <= NOW() THEN 'expired' + ELSE l.status + END + ) = $2) + ORDER BY l.created_at DESC, l.id DESC + LIMIT $3 OFFSET $4 + `, searchPattern, status, limit, offset) + if err != nil { + return LicenseListResult{}, fmt.Errorf("list licenses: %w", err) + } + defer rows.Close() + + licenses := make([]LicenseRow, 0) + for rows.Next() { + license, err := scanLicenseListRow(rows) + if err != nil { + return LicenseListResult{}, err + } + + licenses = append(licenses, license) + } + + if err := rows.Err(); err != nil { + return LicenseListResult{}, fmt.Errorf("iterate licenses: %w", err) + } + + return LicenseListResult{ + Licenses: licenses, + Total: total, + Counts: counts, + }, nil +} + +func (s *Store) countLicenses(ctx context.Context, searchPattern, status string) (int, error) { + var total int + err := s.db.QueryRow(ctx, ` + SELECT COUNT(*) + FROM licenses l + JOIN slugs s ON s.id = l.slug_id + WHERE ($1 = '' OR l.key ILIKE $1 ESCAPE '\' OR s.name ILIKE $1 ESCAPE '\' OR l.metadata::text ILIKE $1 ESCAPE '\') + AND ($2 = '' OR ( + CASE + WHEN l.status <> 'revoked' AND l.expires_at IS NOT NULL AND l.expires_at <= NOW() THEN 'expired' + ELSE l.status + END + ) = $2) + `, searchPattern, status).Scan(&total) + if err != nil { + return 0, fmt.Errorf("count licenses: %w", err) + } + + return total, nil +} + +func (s *Store) countLicenseStatuses(ctx context.Context) (LicenseStatusCounts, error) { + var counts LicenseStatusCounts + err := s.db.QueryRow(ctx, ` + SELECT COUNT(*), + COUNT(*) FILTER (WHERE status = 'active' AND NOT expired), + COUNT(*) FILTER (WHERE status = 'inactive' AND NOT expired), + COUNT(*) FILTER (WHERE status = 'revoked'), + COUNT(*) FILTER (WHERE expired) + FROM ( + SELECT status, + status <> 'revoked' AND expires_at IS NOT NULL AND expires_at <= NOW() AS expired + FROM licenses + ) license_statuses + `).Scan(&counts.Total, &counts.Active, &counts.Inactive, &counts.Revoked, &counts.Expired) + if err != nil { + return LicenseStatusCounts{}, fmt.Errorf("count license statuses: %w", err) + } + + return counts, nil +} + +type licenseListScanner interface { + Scan(dest ...any) error +} + +func scanLicenseListRow(row licenseListScanner) (LicenseRow, error) { + var ( + license LicenseRow + expiresAt sql.NullTime + activatedAt sql.NullTime + lastValidatedAt sql.NullTime + revokedAt sql.NullTime + metadataJSON []byte + ) + + err := row.Scan( + &license.ID, + &license.Key, + &license.Status, + &expiresAt, + &license.CreatedAt, + &activatedAt, + &lastValidatedAt, + &revokedAt, + &metadataJSON, + &license.SlugName, + &license.OfflineEnabled, + &license.OfflineTokenLifetimeSeconds, + &license.MaxActivations, + &license.ActiveSeats, + ) + if err != nil { + return LicenseRow{}, fmt.Errorf("scan license row: %w", err) + } + + if expiresAt.Valid { + v := expiresAt.Time.UTC() + license.ExpiresAt = &v + } + if activatedAt.Valid { + v := activatedAt.Time.UTC() + license.ActivatedAt = &v + } + if lastValidatedAt.Valid { + v := lastValidatedAt.Time.UTC() + license.LastValidatedAt = &v + } + if revokedAt.Valid { + v := revokedAt.Time.UTC() + license.RevokedAt = &v + } + + license.CreatedAt = license.CreatedAt.UTC() + if len(metadataJSON) == 0 { + license.Metadata = map[string]any{} + return license, nil + } + + if err := json.Unmarshal(metadataJSON, &license.Metadata); err != nil { + return LicenseRow{}, fmt.Errorf("decode license metadata: %w", err) + } + + return license, nil +} + +func escapeLikePattern(value string) string { + replacer := strings.NewReplacer(`\`, `\\`, `%`, `\%`, `_`, `\_`) + return replacer.Replace(value) +} diff --git a/internal/storage/schema.sql b/internal/storage/schema.sql index b7f930e..f0908a6 100644 --- a/internal/storage/schema.sql +++ b/internal/storage/schema.sql @@ -7,7 +7,10 @@ CREATE TABLE IF NOT EXISTS slugs ( expiration_type TEXT NOT NULL CHECK (expiration_type IN ('forever', 'duration', 'fixed_date')), expiration_days INTEGER, fixed_expires_at TIMESTAMPTZ, + offline_enabled BOOLEAN NOT NULL DEFAULT FALSE, + offline_token_lifetime_seconds INTEGER NOT NULL DEFAULT 86400, is_default BOOLEAN NOT NULL DEFAULT FALSE, + deleted_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CHECK ( @@ -31,8 +34,41 @@ BEGIN END IF; END $$; +ALTER TABLE slugs + ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; + +ALTER TABLE slugs + ADD COLUMN IF NOT EXISTS offline_token_lifetime_seconds INTEGER NOT NULL DEFAULT 86400; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'chk_slugs_offline_token_lifetime_positive' + AND conrelid = 'slugs'::regclass + ) THEN + ALTER TABLE slugs + ADD CONSTRAINT chk_slugs_offline_token_lifetime_positive + CHECK (offline_token_lifetime_seconds > 0); + END IF; +END $$; + CREATE UNIQUE INDEX IF NOT EXISTS uq_slugs_default_true ON slugs (is_default) WHERE is_default; +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_name = 'slugs' + AND column_name = 'offline_enabled' + ) THEN + ALTER TABLE slugs + ADD COLUMN offline_enabled BOOLEAN NOT NULL DEFAULT FALSE; + END IF; +END $$; + CREATE TABLE IF NOT EXISTS licenses ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), key TEXT NOT NULL UNIQUE, @@ -163,9 +199,26 @@ CREATE TABLE IF NOT EXISTS webhook_deliveries ( CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_pending ON webhook_deliveries (status, next_attempt_at, id); +CREATE TABLE IF NOT EXISTS signing_keys ( + id BIGSERIAL PRIMARY KEY, + name TEXT NOT NULL, + kid TEXT NOT NULL UNIQUE, + algorithm TEXT NOT NULL CHECK (algorithm IN ('Ed25519')), + status TEXT NOT NULL CHECK (status IN ('active', 'verify_only', 'retired')), + private_key_encrypted TEXT NOT NULL, + public_key_pem TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + activated_at TIMESTAMPTZ, + retired_at TIMESTAMPTZ +); + +CREATE UNIQUE INDEX IF NOT EXISTS uq_signing_keys_active +ON signing_keys ((status)) +WHERE status = 'active'; + ALTER TABLE idempotency_records ALTER COLUMN response_body DROP NOT NULL; -INSERT INTO slugs (name, max_activations, expiration_type, is_default) -VALUES ('default', 1, 'forever', TRUE) +INSERT INTO slugs (name, max_activations, expiration_type, offline_enabled, offline_token_lifetime_seconds, is_default) +VALUES ('default', 1, 'forever', FALSE, 86400, TRUE) ON CONFLICT (name) DO NOTHING; diff --git a/internal/storage/signing_keys.go b/internal/storage/signing_keys.go new file mode 100644 index 0000000..e95108d --- /dev/null +++ b/internal/storage/signing_keys.go @@ -0,0 +1,241 @@ +package storage + +import ( + "context" + "database/sql" + "errors" + "fmt" + "strings" + "time" + + "github.com/jackc/pgx/v5" +) + +type SigningKeyRecord struct { + ID int64 + Name string + Kid string + Algorithm string + Status string + PrivateKeyEncrypted string + PublicKeyPEM string + CreatedAt time.Time + ActivatedAt *time.Time + RetiredAt *time.Time +} + +type CreateSigningKeyParams struct { + Name string + Kid string + Algorithm string + PrivateKeyEncrypted string + PublicKeyPEM string +} + +func (s *Store) ListSigningKeys(ctx context.Context) ([]SigningKeyRecord, error) { + rows, err := s.db.Query(ctx, ` + SELECT id, name, kid, algorithm, status, private_key_encrypted, public_key_pem, created_at, activated_at, retired_at + FROM signing_keys + ORDER BY created_at DESC, id DESC + `) + if err != nil { + return nil, fmt.Errorf("list signing keys: %w", err) + } + defer rows.Close() + + keys := make([]SigningKeyRecord, 0) + for rows.Next() { + key, err := scanSigningKey(rows) + if err != nil { + return nil, err + } + keys = append(keys, key) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("iterate signing keys: %w", err) + } + + return keys, nil +} + +func (s *Store) ListPublicSigningKeys(ctx context.Context) ([]SigningKeyRecord, error) { + rows, err := s.db.Query(ctx, ` + SELECT id, name, kid, algorithm, status, private_key_encrypted, public_key_pem, created_at, activated_at, retired_at + FROM signing_keys + WHERE status IN ('active', 'verify_only') + ORDER BY status ASC, activated_at DESC NULLS LAST, created_at DESC + `) + if err != nil { + return nil, fmt.Errorf("list public signing keys: %w", err) + } + defer rows.Close() + + keys := make([]SigningKeyRecord, 0) + for rows.Next() { + key, err := scanSigningKey(rows) + if err != nil { + return nil, err + } + keys = append(keys, key) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("iterate public signing keys: %w", err) + } + + return keys, nil +} + +func (s *Store) GetActiveSigningKey(ctx context.Context) (SigningKeyRecord, error) { + row := s.db.QueryRow(ctx, ` + SELECT id, name, kid, algorithm, status, private_key_encrypted, public_key_pem, created_at, activated_at, retired_at + FROM signing_keys + WHERE status = 'active' + LIMIT 1 + `) + + key, err := scanSigningKey(row) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return SigningKeyRecord{}, ErrNotFound + } + return SigningKeyRecord{}, err + } + + return key, nil +} + +func (s *Store) CreateSigningKey(ctx context.Context, params CreateSigningKeyParams) (SigningKeyRecord, error) { + name := strings.TrimSpace(params.Name) + if name == "" { + name = "unnamed" + } + + algorithm := strings.TrimSpace(params.Algorithm) + if algorithm == "" { + algorithm = "Ed25519" + } + if algorithm != "Ed25519" { + return SigningKeyRecord{}, fmt.Errorf("unsupported signing key algorithm %q", algorithm) + } + + row := s.db.QueryRow(ctx, ` + INSERT INTO signing_keys (name, kid, algorithm, status, private_key_encrypted, public_key_pem) + VALUES ($1, $2, $3, 'verify_only', $4, $5) + RETURNING id, name, kid, algorithm, status, private_key_encrypted, public_key_pem, created_at, activated_at, retired_at + `, name, strings.TrimSpace(params.Kid), algorithm, params.PrivateKeyEncrypted, params.PublicKeyPEM) + + key, err := scanSigningKey(row) + if err != nil { + if isUniqueViolation(err) { + return SigningKeyRecord{}, ErrConflict + } + return SigningKeyRecord{}, fmt.Errorf("create signing key: %w", err) + } + + return key, nil +} + +func (s *Store) ActivateSigningKey(ctx context.Context, id int64) (SigningKeyRecord, error) { + tx, err := s.db.BeginTx(ctx, pgx.TxOptions{}) + if err != nil { + return SigningKeyRecord{}, fmt.Errorf("begin activate signing key tx: %w", err) + } + defer tx.Rollback(ctx) + + var status string + if err := tx.QueryRow(ctx, `SELECT status FROM signing_keys WHERE id = $1 FOR UPDATE`, id).Scan(&status); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return SigningKeyRecord{}, ErrNotFound + } + return SigningKeyRecord{}, fmt.Errorf("query signing key before activate: %w", err) + } + if status == "retired" { + return SigningKeyRecord{}, ErrConflict + } + + if _, err := tx.Exec(ctx, ` + UPDATE signing_keys + SET status = 'verify_only' + WHERE status = 'active' + `); err != nil { + return SigningKeyRecord{}, fmt.Errorf("demote current active signing key: %w", err) + } + + row := tx.QueryRow(ctx, ` + UPDATE signing_keys + SET status = 'active', activated_at = COALESCE(activated_at, NOW()) + WHERE id = $1 + RETURNING id, name, kid, algorithm, status, private_key_encrypted, public_key_pem, created_at, activated_at, retired_at + `, id) + + key, err := scanSigningKey(row) + if err != nil { + return SigningKeyRecord{}, fmt.Errorf("activate signing key: %w", err) + } + + if err := tx.Commit(ctx); err != nil { + return SigningKeyRecord{}, fmt.Errorf("commit activate signing key tx: %w", err) + } + + return key, nil +} + +func (s *Store) RetireSigningKey(ctx context.Context, id int64) (SigningKeyRecord, error) { + row := s.db.QueryRow(ctx, ` + UPDATE signing_keys + SET status = 'retired', retired_at = COALESCE(retired_at, NOW()) + WHERE id = $1 + RETURNING id, name, kid, algorithm, status, private_key_encrypted, public_key_pem, created_at, activated_at, retired_at + `, id) + + key, err := scanSigningKey(row) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return SigningKeyRecord{}, ErrNotFound + } + return SigningKeyRecord{}, fmt.Errorf("retire signing key: %w", err) + } + + return key, nil +} + +type signingKeyScanner interface { + Scan(dest ...any) error +} + +func scanSigningKey(row signingKeyScanner) (SigningKeyRecord, error) { + var ( + key SigningKeyRecord + activatedAt sql.NullTime + retiredAt sql.NullTime + ) + + if err := row.Scan( + &key.ID, + &key.Name, + &key.Kid, + &key.Algorithm, + &key.Status, + &key.PrivateKeyEncrypted, + &key.PublicKeyPEM, + &key.CreatedAt, + &activatedAt, + &retiredAt, + ); err != nil { + return SigningKeyRecord{}, err + } + + key.CreatedAt = key.CreatedAt.UTC() + if activatedAt.Valid { + v := activatedAt.Time.UTC() + key.ActivatedAt = &v + } + if retiredAt.Valid { + v := retiredAt.Time.UTC() + key.RetiredAt = &v + } + + return key, nil +} diff --git a/internal/storage/slug_management.go b/internal/storage/slug_management.go index 10fd60b..ca63e2e 100644 --- a/internal/storage/slug_management.go +++ b/internal/storage/slug_management.go @@ -13,35 +13,47 @@ import ( slugdomain "simple-license-server/internal/domain/slug" ) +const ( + DefaultOfflineTokenLifetimeHours = 24 + DefaultOfflineTokenLifetimeSeconds = DefaultOfflineTokenLifetimeHours * 3600 +) + type SlugRecord struct { - ID int64 - Name string - MaxActivations int - ExpirationType string - ExpirationDays *int - FixedExpiresAt *time.Time - IsDefault bool - CreatedAt time.Time - UpdatedAt time.Time + ID int64 + Name string + MaxActivations int + ExpirationType string + ExpirationDays *int + FixedExpiresAt *time.Time + OfflineEnabled bool + OfflineTokenLifetimeSeconds int + IsDefault bool + DeletedAt *time.Time + CreatedAt time.Time + UpdatedAt time.Time } type CreateSlugParams struct { - Name string - MaxActivations int - ExpirationType string - ExpirationDays *int - FixedExpiresAt *time.Time + Name string + MaxActivations int + ExpirationType string + ExpirationDays *int + FixedExpiresAt *time.Time + OfflineEnabled bool + OfflineTokenLifetimeSeconds int } type UpdateSlugParams struct { - Name *string - MaxActivations *int - ExpirationType *string - ExpirationDays *int - FixedExpiresAt **time.Time + Name *string + MaxActivations *int + ExpirationType *string + ExpirationDays *int + FixedExpiresAt **time.Time + OfflineEnabled *bool + OfflineTokenLifetimeSeconds *int } -func (s *Store) ListSlugs(ctx context.Context) ([]SlugRecord, error) { +func (s *Store) ListSlugs(ctx context.Context, includeArchived bool) ([]SlugRecord, error) { rows, err := s.db.Query(ctx, ` SELECT id, name, @@ -49,12 +61,16 @@ func (s *Store) ListSlugs(ctx context.Context) ([]SlugRecord, error) { expiration_type, expiration_days, fixed_expires_at, + offline_enabled, + offline_token_lifetime_seconds, is_default, + deleted_at, created_at, updated_at FROM slugs + WHERE ($1 OR deleted_at IS NULL) ORDER BY created_at ASC - `) + `, includeArchived) if err != nil { return nil, fmt.Errorf("list slugs: %w", err) } @@ -90,11 +106,15 @@ func (s *Store) GetSlugByName(ctx context.Context, name string) (SlugRecord, err expiration_type, expiration_days, fixed_expires_at, + offline_enabled, + offline_token_lifetime_seconds, is_default, + deleted_at, created_at, updated_at FROM slugs WHERE name = $1 + AND deleted_at IS NULL `, slugName.String()) record, err := scanSlugRecord(row) @@ -129,16 +149,24 @@ func (s *Store) CreateSlug(ctx context.Context, params CreateSlugParams) (SlugRe fixedExpiresAt = sql.NullTime{Time: fixed.UTC(), Valid: true} } + offlineTokenLifetimeSeconds := params.OfflineTokenLifetimeSeconds + if offlineTokenLifetimeSeconds <= 0 { + offlineTokenLifetimeSeconds = DefaultOfflineTokenLifetimeSeconds + } + row := s.db.QueryRow(ctx, ` - INSERT INTO slugs (name, max_activations, expiration_type, expiration_days, fixed_expires_at, is_default) - VALUES ($1, $2, $3, $4, $5, FALSE) + INSERT INTO slugs (name, max_activations, expiration_type, expiration_days, fixed_expires_at, offline_enabled, offline_token_lifetime_seconds, is_default) + VALUES ($1, $2, $3, $4, $5, $6, $7, FALSE) RETURNING id, name, max_activations, expiration_type, expiration_days, fixed_expires_at, + offline_enabled, + offline_token_lifetime_seconds, is_default, + deleted_at, created_at, updated_at `, @@ -147,6 +175,8 @@ func (s *Store) CreateSlug(ctx context.Context, params CreateSlugParams) (SlugRe policy.ExpirationType(), expirationDays, fixedExpiresAt, + params.OfflineEnabled, + offlineTokenLifetimeSeconds, ) record, err := scanSlugRecord(row) @@ -181,6 +211,19 @@ func (s *Store) UpdateSlugByName(ctx context.Context, currentName string, params expirationType = strings.TrimSpace(*params.ExpirationType) } + offlineEnabled := current.OfflineEnabled + if params.OfflineEnabled != nil { + offlineEnabled = *params.OfflineEnabled + } + + offlineTokenLifetimeSeconds := current.OfflineTokenLifetimeSeconds + if params.OfflineTokenLifetimeSeconds != nil { + offlineTokenLifetimeSeconds = *params.OfflineTokenLifetimeSeconds + } + if offlineTokenLifetimeSeconds <= 0 { + return SlugRecord{}, fmt.Errorf("offline_token_lifetime_seconds must be greater than 0") + } + var expirationDays sql.NullInt32 if params.ExpirationDays != nil { expirationDays = sql.NullInt32{Int32: int32(*params.ExpirationDays), Valid: true} @@ -236,15 +279,20 @@ func (s *Store) UpdateSlugByName(ctx context.Context, currentName string, params expiration_type = $3, expiration_days = $4, fixed_expires_at = $5, + offline_enabled = $6, + offline_token_lifetime_seconds = $7, updated_at = NOW() - WHERE id = $6 + WHERE id = $8 RETURNING id, name, max_activations, expiration_type, expiration_days, fixed_expires_at, + offline_enabled, + offline_token_lifetime_seconds, is_default, + deleted_at, created_at, updated_at `, @@ -253,6 +301,8 @@ func (s *Store) UpdateSlugByName(ctx context.Context, currentName string, params policy.ExpirationType(), expirationDays, fixedExpiresAt, + offlineEnabled, + offlineTokenLifetimeSeconds, current.ID, ) @@ -287,6 +337,7 @@ func (s *Store) DeleteSlugByName(ctx context.Context, name string) error { SELECT id, is_default FROM slugs WHERE name = $1 + AND deleted_at IS NULL `, name).Scan(&slugID, &isDefault) if err != nil { if errors.Is(err, pgx.ErrNoRows) { @@ -304,6 +355,8 @@ func (s *Store) DeleteSlugByName(ctx context.Context, name string) error { SELECT COUNT(*) FROM licenses WHERE slug_id = $1 + AND status <> 'revoked' + AND (expires_at IS NULL OR expires_at > NOW()) `, slugID).Scan(&licenseCount) if err != nil { return fmt.Errorf("count slug licenses: %w", err) @@ -314,8 +367,11 @@ func (s *Store) DeleteSlugByName(ctx context.Context, name string) error { } result, err := s.db.Exec(ctx, ` - DELETE FROM slugs + UPDATE slugs + SET deleted_at = NOW(), + updated_at = NOW() WHERE id = $1 + AND deleted_at IS NULL `, slugID) if err != nil { return fmt.Errorf("delete slug: %w", err) @@ -337,6 +393,7 @@ func scanSlugRecord(row slugScanner) (SlugRecord, error) { record SlugRecord expirationDays sql.NullInt32 fixedExpiresAt sql.NullTime + deletedAt sql.NullTime ) err := row.Scan( @@ -346,7 +403,10 @@ func scanSlugRecord(row slugScanner) (SlugRecord, error) { &record.ExpirationType, &expirationDays, &fixedExpiresAt, + &record.OfflineEnabled, + &record.OfflineTokenLifetimeSeconds, &record.IsDefault, + &deletedAt, &record.CreatedAt, &record.UpdatedAt, ) @@ -364,6 +424,11 @@ func scanSlugRecord(row slugScanner) (SlugRecord, error) { record.FixedExpiresAt = &v } + if deletedAt.Valid { + v := deletedAt.Time.UTC() + record.DeletedAt = &v + } + record.CreatedAt = record.CreatedAt.UTC() record.UpdatedAt = record.UpdatedAt.UTC() diff --git a/internal/storage/store.go b/internal/storage/store.go index c9f0d04..adc53b4 100644 --- a/internal/storage/store.go +++ b/internal/storage/store.go @@ -48,6 +48,27 @@ type GeneratedLicense struct { CreatedAt time.Time } +type LicenseListParams struct { + Page int + PageSize int + Search string + Status string +} + +type LicenseStatusCounts struct { + Total int + Active int + Inactive int + Revoked int + Expired int +} + +type LicenseListResult struct { + Licenses []LicenseRow + Total int + Counts LicenseStatusCounts +} + type RevokeResult struct { Valid bool Status string @@ -56,19 +77,27 @@ type RevokeResult struct { } type ActivationResult struct { - Valid bool - Status string - LicenseKey string - Fingerprint string - ExpiresAt *time.Time - Reason string + Valid bool + Status string + LicenseID string + LicenseKey string + Slug string + Fingerprint string + ExpiresAt *time.Time + OfflineEnabled bool + OfflineTokenLifetimeSeconds int + Reason string } type ValidationResult struct { - Valid bool - Status string - ExpiresAt *time.Time - Reason string + Valid bool + Status string + LicenseID string + Slug string + ExpiresAt *time.Time + OfflineEnabled bool + OfflineTokenLifetimeSeconds int + Reason string } type DeactivationResult struct { @@ -132,25 +161,47 @@ type WebhookDelivery struct { CreatedAt time.Time } -type licenseRow struct { - ID string - Key string - Status string - ExpiresAt *time.Time - CreatedAt time.Time - ActivatedAt *time.Time - RevokedAt *time.Time - Metadata map[string]any - SlugName string - MaxActivations int +type WebhookDeliveryLog struct { + ID int64 + EndpointID int64 + EndpointName string + EndpointURL string + EventType string + Status string + Attempts int + LastResponseStatus *int + LastError *string + NextAttemptAt time.Time + CreatedAt time.Time + UpdatedAt time.Time + DeliveredAt *time.Time +} + +type LicenseRow struct { + ID string + Key string + Status string + ExpiresAt *time.Time + CreatedAt time.Time + ActivatedAt *time.Time + LastValidatedAt *time.Time + RevokedAt *time.Time + Metadata map[string]any + SlugName string + OfflineEnabled bool + OfflineTokenLifetimeSeconds int + MaxActivations int + ActiveSeats int } type slugOptions struct { - SlugID int64 - ExpirationType string - ExpirationDays sql.NullInt32 - FixedExpiresAt sql.NullTime - MaxActivations int + SlugID int64 + ExpirationType string + ExpirationDays sql.NullInt32 + FixedExpiresAt sql.NullTime + MaxActivations int + OfflineEnabled bool + OfflineTokenLifetimeSeconds int } func New(ctx context.Context, databaseURL string) (*Store, error) { @@ -287,15 +338,18 @@ func generateLicenseWithQuerier(ctx context.Context, q queryable, slugName strin func loadSlugOptionsByName(ctx context.Context, q queryable, slugName string) (slugOptions, error) { var options slugOptions err := q.QueryRow(ctx, ` - SELECT id, expiration_type, expiration_days, fixed_expires_at, max_activations + SELECT id, expiration_type, expiration_days, fixed_expires_at, max_activations, offline_enabled, offline_token_lifetime_seconds FROM slugs WHERE name = $1 + AND deleted_at IS NULL `, slugName).Scan( &options.SlugID, &options.ExpirationType, &options.ExpirationDays, &options.FixedExpiresAt, &options.MaxActivations, + &options.OfflineEnabled, + &options.OfflineTokenLifetimeSeconds, ) if err != nil { if errors.Is(err, pgx.ErrNoRows) { @@ -476,12 +530,16 @@ func (s *Store) ActivateLicense(ctx context.Context, licenseKey, fingerprint str if !decision.Valid { return ActivationResult{ - Valid: false, - Status: string(decision.Status), - LicenseKey: license.Key, - Fingerprint: fingerprint, - ExpiresAt: license.ExpiresAt, - Reason: string(decision.Reason), + Valid: false, + Status: string(decision.Status), + LicenseID: license.ID, + LicenseKey: license.Key, + Slug: license.SlugName, + Fingerprint: fingerprint, + ExpiresAt: license.ExpiresAt, + OfflineEnabled: license.OfflineEnabled, + OfflineTokenLifetimeSeconds: license.OfflineTokenLifetimeSeconds, + Reason: string(decision.Reason), }, nil } @@ -551,11 +609,15 @@ func (s *Store) ActivateLicense(ctx context.Context, licenseKey, fingerprint str } return ActivationResult{ - Valid: true, - Status: string(aggregate.Status()), - LicenseKey: license.Key, - Fingerprint: fingerprint, - ExpiresAt: license.ExpiresAt, + Valid: true, + Status: string(aggregate.Status()), + LicenseID: license.ID, + LicenseKey: license.Key, + Slug: license.SlugName, + Fingerprint: fingerprint, + ExpiresAt: license.ExpiresAt, + OfflineEnabled: license.OfflineEnabled, + OfflineTokenLifetimeSeconds: license.OfflineTokenLifetimeSeconds, }, nil } @@ -589,10 +651,14 @@ func (s *Store) ValidateLicense(ctx context.Context, licenseKey, fingerprint str decision := aggregate.Validate(now, activeActivationID > 0) if !decision.Valid { return ValidationResult{ - Valid: decision.Valid, - Status: string(decision.Status), - ExpiresAt: license.ExpiresAt, - Reason: string(decision.Reason), + Valid: decision.Valid, + Status: string(decision.Status), + LicenseID: license.ID, + Slug: license.SlugName, + ExpiresAt: license.ExpiresAt, + OfflineEnabled: license.OfflineEnabled, + OfflineTokenLifetimeSeconds: license.OfflineTokenLifetimeSeconds, + Reason: string(decision.Reason), }, nil } @@ -629,9 +695,13 @@ func (s *Store) ValidateLicense(ctx context.Context, licenseKey, fingerprint str } return ValidationResult{ - Valid: decision.Valid, - Status: string(aggregate.Status()), - ExpiresAt: license.ExpiresAt, + Valid: decision.Valid, + Status: string(aggregate.Status()), + LicenseID: license.ID, + Slug: license.SlugName, + ExpiresAt: license.ExpiresAt, + OfflineEnabled: license.OfflineEnabled, + OfflineTokenLifetimeSeconds: license.OfflineTokenLifetimeSeconds, }, nil } @@ -1143,6 +1213,88 @@ func (s *Store) MarkWebhookDeliveryFailed(ctx context.Context, deliveryID int64, return nil } +func (s *Store) ListWebhookDeliveries(ctx context.Context, limit int) ([]WebhookDeliveryLog, error) { + if limit <= 0 { + limit = 25 + } + + rows, err := s.db.Query(ctx, ` + SELECT d.id, + d.endpoint_id, + w.name, + w.url, + d.event_type, + d.status, + d.attempts, + d.last_response_status, + d.last_error, + d.next_attempt_at, + d.created_at, + d.updated_at, + d.delivered_at + FROM webhook_deliveries d + JOIN webhook_endpoints w ON w.id = d.endpoint_id + ORDER BY d.updated_at DESC, d.id DESC + LIMIT $1 + `, limit) + if err != nil { + return nil, fmt.Errorf("list webhook deliveries: %w", err) + } + defer rows.Close() + + deliveries := make([]WebhookDeliveryLog, 0) + for rows.Next() { + var ( + delivery WebhookDeliveryLog + lastResponseStatus sql.NullInt32 + lastError sql.NullString + deliveredAt sql.NullTime + ) + + if err := rows.Scan( + &delivery.ID, + &delivery.EndpointID, + &delivery.EndpointName, + &delivery.EndpointURL, + &delivery.EventType, + &delivery.Status, + &delivery.Attempts, + &lastResponseStatus, + &lastError, + &delivery.NextAttemptAt, + &delivery.CreatedAt, + &delivery.UpdatedAt, + &deliveredAt, + ); err != nil { + return nil, fmt.Errorf("scan webhook delivery log: %w", err) + } + + if lastResponseStatus.Valid { + v := int(lastResponseStatus.Int32) + delivery.LastResponseStatus = &v + } + if lastError.Valid { + v := lastError.String + delivery.LastError = &v + } + if deliveredAt.Valid { + v := deliveredAt.Time.UTC() + delivery.DeliveredAt = &v + } + + delivery.NextAttemptAt = delivery.NextAttemptAt.UTC() + delivery.CreatedAt = delivery.CreatedAt.UTC() + delivery.UpdatedAt = delivery.UpdatedAt.UTC() + deliveries = append(deliveries, delivery) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("iterate webhook delivery logs: %w", err) + } + + return deliveries, nil +} + func (s *Store) getWebhookEndpointByID(ctx context.Context, id int64) (WebhookEndpoint, error) { var ( endpoint WebhookEndpoint @@ -1178,7 +1330,7 @@ func (s *Store) getWebhookEndpointByID(ctx context.Context, id int64) (WebhookEn return endpoint, nil } -func loadLicenseByKey(ctx context.Context, tx pgx.Tx, key string, forUpdate bool) (licenseRow, error) { +func loadLicenseByKey(ctx context.Context, tx pgx.Tx, key string, forUpdate bool) (LicenseRow, error) { query := ` SELECT l.id, l.key, @@ -1189,7 +1341,9 @@ func loadLicenseByKey(ctx context.Context, tx pgx.Tx, key string, forUpdate bool l.activated_at, l.revoked_at, l.metadata, - s.name + s.name, + s.offline_enabled, + s.offline_token_lifetime_seconds FROM licenses l JOIN slugs s ON s.id = l.slug_id WHERE l.key = $1 @@ -1200,7 +1354,7 @@ func loadLicenseByKey(ctx context.Context, tx pgx.Tx, key string, forUpdate bool var ( metadataBytes []byte - row licenseRow + row LicenseRow ) err := tx.QueryRow(ctx, query, key).Scan( @@ -1214,12 +1368,14 @@ func loadLicenseByKey(ctx context.Context, tx pgx.Tx, key string, forUpdate bool &row.RevokedAt, &metadataBytes, &row.SlugName, + &row.OfflineEnabled, + &row.OfflineTokenLifetimeSeconds, ) if err != nil { if errors.Is(err, pgx.ErrNoRows) { - return licenseRow{}, ErrNotFound + return LicenseRow{}, ErrNotFound } - return licenseRow{}, fmt.Errorf("query license by key: %w", err) + return LicenseRow{}, fmt.Errorf("query license by key: %w", err) } if len(metadataBytes) == 0 { @@ -1228,7 +1384,7 @@ func loadLicenseByKey(ctx context.Context, tx pgx.Tx, key string, forUpdate bool } if err := json.Unmarshal(metadataBytes, &row.Metadata); err != nil { - return licenseRow{}, fmt.Errorf("decode metadata json: %w", err) + return LicenseRow{}, fmt.Errorf("decode metadata json: %w", err) } return row, nil From fcc0bc624ec53a3998e3448f0de8dc606ea3ab34 Mon Sep 17 00:00:00 2001 From: Alex Wichmann Date: Mon, 4 May 2026 20:44:05 +0200 Subject: [PATCH 2/5] Update .gitignore for Node.js environment (#7) Added Node.js specific entries to .gitignore. --- .gitignore | 145 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) diff --git a/.gitignore b/.gitignore index aaadf73..d1ecee9 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,148 @@ go.work.sum # Editor/IDE # .idea/ # .vscode/ +# Created by https://www.toptal.com/developers/gitignore/api/node +# Edit at https://www.toptal.com/developers/gitignore?templates=node + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit + +# End of https://www.toptal.com/developers/gitignore/api/node + From 7980a9d285f32953e639d6d131bebbe04f6f254d Mon Sep 17 00:00:00 2001 From: Alex Wichmann Date: Mon, 4 May 2026 20:50:25 +0200 Subject: [PATCH 3/5] add ui --- ui/index.html | 13 + ui/package-lock.json | 3194 +++++++++++++++++++++++ ui/package.json | 26 + ui/postcss.config.js | 6 + ui/public/favicon.svg | 9 + ui/public/icons.svg | 24 + ui/src/App.vue | 124 + ui/src/components/Layout/AppHeader.vue | 69 + ui/src/components/Layout/AppSidebar.vue | 52 + ui/src/main.js | 10 + ui/src/router/index.ts | 59 + ui/src/stores/api.ts | 113 + ui/src/stores/auth.ts | 33 + ui/src/style.css | 137 + ui/src/views/ApiKeysView.vue | 123 + ui/src/views/LicensesView.vue | 592 +++++ ui/src/views/OfflineLicensesView.vue | 183 ++ ui/src/views/SlugsView.vue | 288 ++ ui/src/views/WebhooksView.vue | 275 ++ ui/tailwind.config.js | 26 + ui/vite.config.js | 16 + 21 files changed, 5372 insertions(+) create mode 100644 ui/index.html create mode 100644 ui/package-lock.json create mode 100644 ui/package.json create mode 100644 ui/postcss.config.js create mode 100644 ui/public/favicon.svg create mode 100644 ui/public/icons.svg create mode 100644 ui/src/App.vue create mode 100644 ui/src/components/Layout/AppHeader.vue create mode 100644 ui/src/components/Layout/AppSidebar.vue create mode 100644 ui/src/main.js create mode 100644 ui/src/router/index.ts create mode 100644 ui/src/stores/api.ts create mode 100644 ui/src/stores/auth.ts create mode 100644 ui/src/style.css create mode 100644 ui/src/views/ApiKeysView.vue create mode 100644 ui/src/views/LicensesView.vue create mode 100644 ui/src/views/OfflineLicensesView.vue create mode 100644 ui/src/views/SlugsView.vue create mode 100644 ui/src/views/WebhooksView.vue create mode 100644 ui/tailwind.config.js create mode 100644 ui/vite.config.js diff --git a/ui/index.html b/ui/index.html new file mode 100644 index 0000000..28e55c0 --- /dev/null +++ b/ui/index.html @@ -0,0 +1,13 @@ + + + + + + + ui + + +
+ + + diff --git a/ui/package-lock.json b/ui/package-lock.json new file mode 100644 index 0000000..ee6da4a --- /dev/null +++ b/ui/package-lock.json @@ -0,0 +1,3194 @@ +{ + "name": "ui", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ui", + "version": "0.0.0", + "dependencies": { + "axios": "^1.15.2", + "lucide-vue-next": "^1.0.0", + "pinia": "^3.0.4", + "vue": "^3.5.32", + "vue-router": "^4.6.4" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.6", + "autoprefixer": "^10.4.20", + "esbuild": "^0.28.0", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.17", + "vite": "^8.0.10" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "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/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "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/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "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/@oxc-project/types": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", + "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz", + "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz", + "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.13.tgz", + "integrity": "sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.6.tgz", + "integrity": "sha512-u9HHgfrq3AjXlysn0eINFnWQOJQLO9WN6VprZ8FXl7A2bYisv3Hui9Ij+7QZ41F/WYWarHjwBbXtD7dKg3uxbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.13" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.33.tgz", + "integrity": "sha512-3PZLQwFw4Za3TC8t0FvTy3wI16Kt+pmwcgNZca4Pj9iWL2E72a/gZlpBtAJvEdDMdCxdG/qq0C7PN0bsJuv0Rw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/shared": "3.5.33", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.33.tgz", + "integrity": "sha512-PXq0yrfCLzzL07rbXO4awtXY1Z06LG2eu6Adg3RJFa/j3Cii217XxxLXG22N330gw7GmALCY0Z8RgXEviwgpjA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.33", + "@vue/shared": "3.5.33" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.33.tgz", + "integrity": "sha512-UTUvRO9cY+rROrx/pvN9P5Z7FgA6QGfokUCfhQE4EnmUj3rVnK+CHI0LsEO1pg+I7//iRYMUfcNcCPe7tg0CoA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/compiler-core": "3.5.33", + "@vue/compiler-dom": "3.5.33", + "@vue/compiler-ssr": "3.5.33", + "@vue/shared": "3.5.33", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.10", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-sfc/node_modules/postcss": { + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.13.tgz", + "integrity": "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==", + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.33.tgz", + "integrity": "sha512-IErjYdnj1qIupG5xxiVIYiiRvDhGWV4zuh/RCrwfYpuL+HWQzeU6lCk/nF9r7olWMnjKxCAkOctT2qFWFkzb1A==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.33", + "@vue/shared": "3.5.33" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", + "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.9" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", + "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.9", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", + "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==", + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.33.tgz", + "integrity": "sha512-p8UfIqyIhb0rYGlSgSBV+lPhF2iUSBcRy7enhTmPqKWadHy9kcOFYF1AejYBP9P+avnd3OBbD49DU4pLWX/94A==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.33" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.33.tgz", + "integrity": "sha512-UpFF45RI9//a7rvq7RdOQblb4tup7hHG9QsmIrxkFQLzQ7R8/iNQ5LE15NhLZ1/WcHMU2b47u6P33CPUelHyIQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.33", + "@vue/shared": "3.5.33" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.33.tgz", + "integrity": "sha512-IOxMsAOwquhfITgmOgaPYl7/j8gKUxUFoflRc+u4LxyD3+783xne8vNta1PONVCvCV9A0w7hkyEepINDqfO0tw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.33", + "@vue/runtime-core": "3.5.33", + "@vue/shared": "3.5.33", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.33.tgz", + "integrity": "sha512-0xylq/8/h44lVG0pZFknv1XIdEgymq2E9n59uTWJBG+dIgiT0TMCSsxrN7nO16Z0MU0MPjFcguBbZV8Itk52Hw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.33", + "@vue/shared": "3.5.33" + }, + "peerDependencies": { + "vue": "3.5.33" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.33.tgz", + "integrity": "sha512-5vR2QIlmaLG77Ygd4pMP6+SGQ5yox9VhtnbDWTy9DzMzdmeLxZ1QqxrywEZ9sa1AVubfIJyaCG3ytyWU81ufcQ==", + "license": "MIT" + }, + "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/anymatch/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/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/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/autoprefixer": { + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "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.23.3", + "caniuse-lite": "^1.0.30001646", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz", + "integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.21", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.21.tgz", + "integrity": "sha512-Q+rUQ7Uz8AHM7DEaNdwvfFCTq7a43lNTzuS94eiWqwyxfV/wJv+oUivef51T91mmRY4d4A1u9rcSvkeufCVXlA==", + "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/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "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.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/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/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.30001790", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001790.tgz", + "integrity": "sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==", + "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/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/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/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/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "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/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "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/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.344", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz", + "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "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-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "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.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, + "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": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "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/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "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/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.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "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/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-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.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "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-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-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-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "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/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "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/lucide-vue-next": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lucide-vue-next/-/lucide-vue-next-1.0.0.tgz", + "integrity": "sha512-V6SPvx1IHTj/UY+FrIYWV5faISsPSb8BnWSFDxAtezWKvWc9ZZ40PDrdu1/Qb5vg4lHWr1hs1BAMGVGm6V1Xdg==", + "license": "ISC", + "peerDependencies": { + "vue": ">=3.0.1" + } + }, + "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==", + "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/micromatch/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/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/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "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.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "dev": true, + "license": "MIT" + }, + "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/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "dev": true, + "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/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/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinia": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz", + "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^7.7.7" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.5.0", + "vue": "^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "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/postcss": { + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "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.7", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "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": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "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.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "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/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/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/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "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/readdirp/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/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "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/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/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", + "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.127.0", + "@rolldown/pluginutils": "1.0.0-rc.17" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-x64": "1.0.0-rc.17", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz", + "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==", + "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/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, + "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/tailwindcss": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "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.6", + "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", + "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/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "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/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/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/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/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": "8.0.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz", + "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.10", + "rolldown": "1.0.0-rc.17", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/postcss": { + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.13.tgz", + "integrity": "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==", + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/vue": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.33.tgz", + "integrity": "sha512-1AgChhx5w3ALgT4oK3acm2Es/7jyZhWSVUfs3rOBlGQC0rjEDkS7G4lWlJJGGNQD+BV3reCwbQrOe1mPNwKHBQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.33", + "@vue/compiler-sfc": "3.5.33", + "@vue/runtime-dom": "3.5.33", + "@vue/server-renderer": "3.5.33", + "@vue/shared": "3.5.33" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/vue-router/node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + } + } +} diff --git a/ui/package.json b/ui/package.json new file mode 100644 index 0000000..082c367 --- /dev/null +++ b/ui/package.json @@ -0,0 +1,26 @@ +{ + "name": "ui", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "axios": "^1.15.2", + "lucide-vue-next": "^1.0.0", + "pinia": "^3.0.4", + "vue": "^3.5.32", + "vue-router": "^4.6.4" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.6", + "autoprefixer": "^10.4.20", + "esbuild": "^0.28.0", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.17", + "vite": "^8.0.10" + } +} diff --git a/ui/postcss.config.js b/ui/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/ui/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/ui/public/favicon.svg b/ui/public/favicon.svg new file mode 100644 index 0000000..e6526e7 --- /dev/null +++ b/ui/public/favicon.svg @@ -0,0 +1,9 @@ + + + + diff --git a/ui/public/icons.svg b/ui/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/ui/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/src/App.vue b/ui/src/App.vue new file mode 100644 index 0000000..d24c4ac --- /dev/null +++ b/ui/src/App.vue @@ -0,0 +1,124 @@ + + + diff --git a/ui/src/components/Layout/AppHeader.vue b/ui/src/components/Layout/AppHeader.vue new file mode 100644 index 0000000..284fafe --- /dev/null +++ b/ui/src/components/Layout/AppHeader.vue @@ -0,0 +1,69 @@ + + + diff --git a/ui/src/components/Layout/AppSidebar.vue b/ui/src/components/Layout/AppSidebar.vue new file mode 100644 index 0000000..877dbb3 --- /dev/null +++ b/ui/src/components/Layout/AppSidebar.vue @@ -0,0 +1,52 @@ + + + diff --git a/ui/src/main.js b/ui/src/main.js new file mode 100644 index 0000000..501dee0 --- /dev/null +++ b/ui/src/main.js @@ -0,0 +1,10 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import App from './App.vue' +import router from './router' +import './style.css' + +const app = createApp(App) +app.use(createPinia()) +app.use(router) +app.mount('#app') diff --git a/ui/src/router/index.ts b/ui/src/router/index.ts new file mode 100644 index 0000000..88a520e --- /dev/null +++ b/ui/src/router/index.ts @@ -0,0 +1,59 @@ +import { createRouter, createWebHistory } from 'vue-router' +import { useAuthStore } from '@/stores/auth' + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes: [ + { + path: '/', + redirect: '/licenses', + }, + { + path: '/slugs', + name: 'slugs', + component: () => import('@/views/SlugsView.vue'), + meta: { title: 'Slugs' }, + }, + { + path: '/licenses', + name: 'licenses', + component: () => import('@/views/LicensesView.vue'), + meta: { title: 'Licenses' }, + }, + { + path: '/api-keys', + name: 'api-keys', + component: () => import('@/views/ApiKeysView.vue'), + meta: { title: 'API Keys' }, + }, + { + path: '/webhooks', + name: 'webhooks', + component: () => import('@/views/WebhooksView.vue'), + meta: { title: 'Webhooks' }, + }, + { + path: '/offline-licenses', + name: 'offline-licenses', + component: () => import('@/views/OfflineLicensesView.vue'), + meta: { title: 'Offline Licenses' }, + }, + ], +}) + +router.beforeEach((to, from, next) => { + const authStore = useAuthStore() + if (!authStore.apiKey) { + authStore.loadApiKey() + } + + if (to.meta.requiresAuth && !authStore.apiKey) { + next({ name: 'licenses' }) + return + } + + document.title = to.meta.title ? `${to.meta.title} - Simple License Server` : 'Simple License Server' + next() +}) + +export default router diff --git a/ui/src/stores/api.ts b/ui/src/stores/api.ts new file mode 100644 index 0000000..5063149 --- /dev/null +++ b/ui/src/stores/api.ts @@ -0,0 +1,113 @@ +import axios from 'axios' + +const managementApi = axios.create({ + baseURL: '/management', + headers: { + 'Content-Type': 'application/json', + }, +}) + +const rootApi = axios.create({ + baseURL: '/', + headers: { + 'Content-Type': 'application/json', + }, +}) + +const storedManagementKey = () => localStorage.getItem('management_api_key') || localStorage.getItem('api_key') + +const managementAuthHeaders = () => { + const apiKey = storedManagementKey() + return apiKey ? { Authorization: `Bearer ${apiKey}` } : {} +} + +managementApi.interceptors.request.use((config) => { + const apiKey = storedManagementKey() + if (apiKey) { + config.headers.Authorization = `Bearer ${apiKey}` + } + return config +}) + +export const slugAPI = { + list: (params: { include_archived?: boolean } = {}) => managementApi.get('/slugs', { params }), + get: (name: string) => managementApi.get(`/slugs/${encodeURIComponent(name)}`), + create: (data: { + name: string + max_activations: number + expiration_type: string + expiration_days?: number + fixed_expires_at?: string + offline_enabled?: boolean + offline_token_lifetime_hours?: number + }) => managementApi.post('/slugs', data), + update: ( + name: string, + data: Partial<{ + name: string + max_activations: number + expiration_type: string + expiration_days: number + fixed_expires_at: string + offline_enabled: boolean + offline_token_lifetime_hours: number + }>, + ) => managementApi.patch(`/slugs/${encodeURIComponent(name)}`, data), + delete: (name: string) => managementApi.delete(`/slugs/${encodeURIComponent(name)}`), +} + +export const apiKeysAPI = { + list: () => managementApi.get('/api-keys'), + create: (name: string) => managementApi.post('/api-keys', { name }), + revoke: (id: number) => managementApi.post(`/api-keys/${id}/revoke`), +} + +export const licensesAPI = { + list: (params: { page?: number; page_size?: number; q?: string; status?: string } = {}) => managementApi.get('/licenses', { params }), + generate: (data: { slug: string; metadata?: Record }) => rootApi.post('/generate', data, { headers: managementAuthHeaders() }), + revoke: (data: { license_key: string }) => rootApi.post('/revoke', data, { headers: managementAuthHeaders() }), +} + +export const webhooksAPI = { + list: () => managementApi.get('/webhooks'), + deliveries: (limit = 25) => managementApi.get('/webhooks/deliveries', { params: { limit } }), + create: (data: { name: string; url: string; events: string[]; enabled: boolean }) => managementApi.post('/webhooks', data), + update: ( + id: number, + data: Partial<{ name: string; url: string; events: string[]; enabled: boolean }>, + ) => managementApi.patch(`/webhooks/${id}`, data), + delete: (id: number) => managementApi.delete(`/webhooks/${id}`), +} + +export const offlineAPI = { + signingKeys: () => managementApi.get('/offline/signing-keys'), + publicKeys: () => managementApi.get('/offline/public-keys'), + createSigningKey: (name: string) => managementApi.post('/offline/signing-keys', { name }), + activateSigningKey: (id: number) => managementApi.post(`/offline/signing-keys/${id}/activate`), + retireSigningKey: (id: number) => managementApi.post(`/offline/signing-keys/${id}/retire`), +} + +export const runtimeAPI = { + activate: (data: { license_key: string; fingerprint: string; metadata?: Record }) => rootApi.post('/activate', data), + validate: (data: { license_key: string; fingerprint: string }) => rootApi.post('/validate', data), + deactivate: (data: { license_key: string; fingerprint: string; reason?: string }) => rootApi.post('/deactivate', data), +} + +export const provisioningAPI = { + generate: (data: { slug: string; metadata?: Record }, serverAPIKey: string, idempotencyKey?: string) => { + const headers: Record = { + Authorization: `Bearer ${serverAPIKey}`, + } + if (idempotencyKey) { + headers['Idempotency-Key'] = idempotencyKey + } + return rootApi.post('/generate', data, { headers }) + }, + revoke: (data: { license_key: string }, serverAPIKey: string) => { + return rootApi.post('/revoke', data, { + headers: { + Authorization: `Bearer ${serverAPIKey}`, + }, + }) + }, +} diff --git a/ui/src/stores/auth.ts b/ui/src/stores/auth.ts new file mode 100644 index 0000000..b329716 --- /dev/null +++ b/ui/src/stores/auth.ts @@ -0,0 +1,33 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' + +export const useAuthStore = defineStore('auth', () => { + const apiKey = ref(null) + const isLoading = ref(false) + + const loadApiKey = () => { + const stored = localStorage.getItem('management_api_key') || localStorage.getItem('api_key') + if (stored) { + apiKey.value = stored + } + } + + const setApiKey = (key: string) => { + apiKey.value = key + localStorage.setItem('management_api_key', key) + } + + const clearApiKey = () => { + apiKey.value = null + localStorage.removeItem('management_api_key') + localStorage.removeItem('api_key') + } + + return { + apiKey, + isLoading, + loadApiKey, + setApiKey, + clearApiKey, + } +}) diff --git a/ui/src/style.css b/ui/src/style.css new file mode 100644 index 0000000..a3e1309 --- /dev/null +++ b/ui/src/style.css @@ -0,0 +1,137 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html, +body, +#app { + min-height: 100%; +} + +body { + margin: 0; + font-family: Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji"; + background: #0f131d; + color: #dfe2f1; +} + +button, +input, +textarea, +select { + font: inherit; +} + +.metadata-label { + font-family: "Space Grotesk", Inter, ui-sans-serif, system-ui, sans-serif; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.ghost-border { + border: 1px solid rgba(65, 71, 81, 0.15); +} + +.surface-card { + background: #1c1f2a; +} + +.surface-low { + background: #171b26; +} + +.code-block { + background: #0b1320; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; +} + +.field-control { + width: 100%; + border-radius: 0.375rem; + background: #1c1f2a; + color: #dfe2f1; + outline: 1px solid rgba(65, 71, 81, 0.18); + padding: 0.75rem 1rem; + transition: outline-color 0.15s ease, box-shadow 0.15s ease, background-color 0.15s ease; +} + +.field-control::placeholder { + color: #6f7a91; +} + +.field-control:focus { + background: #262a35; + outline-color: rgba(164, 201, 255, 0.5); + box-shadow: 0 0 0 4px rgba(164, 201, 255, 0.08); +} + +.field-control-with-icon { + padding-left: 2.25rem; +} + +.primary-action { + border-radius: 0.375rem; + background: #a4c9ff; + color: #06142c; + font-weight: 800; + transition: background-color 0.15s ease; +} + +.primary-action:hover { + background: #60a5fa; +} + +.secondary-action { + border-radius: 0.375rem; + background: #1c1f2a; + color: #dfe2f1; + font-weight: 700; + transition: background-color 0.15s ease, color 0.15s ease; +} + +.secondary-action:hover { + background: #262a35; +} + +.sortable-header { + display: inline-flex; + align-items: center; + gap: 0.375rem; + color: #8f98ad; + font: inherit; + letter-spacing: inherit; + text-transform: inherit; + transition: color 0.15s ease; +} + +.sortable-header:hover { + color: #dfe2f1; +} + +.fade-enter-active, +.fade-leave-active { + transition: opacity 0.2s ease; +} + +.fade-enter-from, +.fade-leave-to { + opacity: 0; +} + +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: #414751; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: #5b6472; +} diff --git a/ui/src/views/ApiKeysView.vue b/ui/src/views/ApiKeysView.vue new file mode 100644 index 0000000..c9483ad --- /dev/null +++ b/ui/src/views/ApiKeysView.vue @@ -0,0 +1,123 @@ + + + diff --git a/ui/src/views/LicensesView.vue b/ui/src/views/LicensesView.vue new file mode 100644 index 0000000..748d954 --- /dev/null +++ b/ui/src/views/LicensesView.vue @@ -0,0 +1,592 @@ + + + diff --git a/ui/src/views/OfflineLicensesView.vue b/ui/src/views/OfflineLicensesView.vue new file mode 100644 index 0000000..ef166a9 --- /dev/null +++ b/ui/src/views/OfflineLicensesView.vue @@ -0,0 +1,183 @@ + + + diff --git a/ui/src/views/SlugsView.vue b/ui/src/views/SlugsView.vue new file mode 100644 index 0000000..56181f7 --- /dev/null +++ b/ui/src/views/SlugsView.vue @@ -0,0 +1,288 @@ + + + diff --git a/ui/src/views/WebhooksView.vue b/ui/src/views/WebhooksView.vue new file mode 100644 index 0000000..abbe9a0 --- /dev/null +++ b/ui/src/views/WebhooksView.vue @@ -0,0 +1,275 @@ + + + diff --git a/ui/tailwind.config.js b/ui/tailwind.config.js new file mode 100644 index 0000000..3d9f027 --- /dev/null +++ b/ui/tailwind.config.js @@ -0,0 +1,26 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./index.html", + "./src/**/*.{vue,js,ts,jsx,tsx}", + ], + theme: { + extend: { + colors: { + primary: { + 50: '#f0f9ff', + 100: '#e0f2fe', + 200: '#bae6fd', + 300: '#7dd3fc', + 400: '#38bdf8', + 500: '#0ea5e9', + 600: '#0284c7', + 700: '#0369a1', + 800: '#075985', + 900: '#0c4a6e', + }, + }, + }, + }, + plugins: [], +} diff --git a/ui/vite.config.js b/ui/vite.config.js new file mode 100644 index 0000000..dbd6b8c --- /dev/null +++ b/ui/vite.config.js @@ -0,0 +1,16 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import { fileURLToPath, URL } from 'node:url' + +export default defineConfig({ + base: '/ui/', + build: { + cssMinify: 'esbuild', + }, + plugins: [vue()], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + }, +}) From 1cae9da6671de0e6b5306e3c0edb5e1a9ff6dee7 Mon Sep 17 00:00:00 2001 From: Alex Wichmann Date: Mon, 4 May 2026 20:50:56 +0200 Subject: [PATCH 4/5] update mod and sum --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 5bce380..ce18ec5 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.25.0 require ( github.com/jackc/pgx/v5 v5.9.2 - golang.org/x/crypto v0.45.0 + golang.org/x/crypto v0.50.0 golang.org/x/time v0.15.0 ) @@ -12,6 +12,6 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/text v0.36.0 // indirect ) diff --git a/go.sum b/go.sum index ed4406c..2af8e14 100644 --- a/go.sum +++ b/go.sum @@ -16,12 +16,12 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 459716b32fdf9ad7c5a7feee2b803e355db15c4e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 18:51:32 +0000 Subject: [PATCH 5/5] Bump postcss in /ui in the npm_and_yarn group across 1 directory Bumps the npm_and_yarn group with 1 update in the /ui directory: [postcss](https://github.com/postcss/postcss). Updates `postcss` from 8.4.49 to 8.5.14 - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/compare/8.4.49...8.5.14) --- updated-dependencies: - dependency-name: postcss dependency-version: 8.5.14 dependency-type: direct:development dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] --- ui/package-lock.json | 98 +++----------------------------------------- ui/package.json | 2 +- 2 files changed, 6 insertions(+), 94 deletions(-) diff --git a/ui/package-lock.json b/ui/package-lock.json index ee6da4a..2bb5ab3 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -18,7 +18,7 @@ "@vitejs/plugin-vue": "^6.0.6", "autoprefixer": "^10.4.20", "esbuild": "^0.28.0", - "postcss": "^8.4.49", + "postcss": "^8.5.14", "tailwindcss": "^3.4.17", "vite": "^8.0.10" } @@ -756,9 +756,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -776,9 +773,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -796,9 +790,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -816,9 +807,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -836,9 +824,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -856,9 +841,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1013,34 +995,6 @@ "source-map-js": "^1.2.1" } }, - "node_modules/@vue/compiler-sfc/node_modules/postcss": { - "version": "8.5.13", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.13.tgz", - "integrity": "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==", - "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.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, "node_modules/@vue/compiler-ssr": { "version": "3.5.33", "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.33.tgz", @@ -2098,9 +2052,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2122,9 +2073,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2146,9 +2094,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2170,9 +2115,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2490,10 +2432,9 @@ } }, "node_modules/postcss": { - "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", - "dev": true, + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "funding": [ { "type": "opencollective", @@ -2510,7 +2451,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -3103,35 +3044,6 @@ } } }, - "node_modules/vite/node_modules/postcss": { - "version": "8.5.13", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.13.tgz", - "integrity": "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==", - "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.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, "node_modules/vue": { "version": "3.5.33", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.33.tgz", diff --git a/ui/package.json b/ui/package.json index 082c367..ebe76a8 100644 --- a/ui/package.json +++ b/ui/package.json @@ -19,7 +19,7 @@ "@vitejs/plugin-vue": "^6.0.6", "autoprefixer": "^10.4.20", "esbuild": "^0.28.0", - "postcss": "^8.4.49", + "postcss": "^8.5.14", "tailwindcss": "^3.4.17", "vite": "^8.0.10" }